一檢索層:多源搜索融合架構(gòu)與實(shí)踐)
UniSearch我如何用一套統(tǒng)一檢索層把三個(gè)“各自為政”的搜索系統(tǒng)揉成一個(gè)接到這個(gè)“UniSearch”項(xiàng)目的時(shí)候我一開(kāi)始是想拒絕的并不是因?yàn)榧夹g(shù)有多難而是我知道這類“統(tǒng)一搜索”的需求十有八九不是在做一個(gè)新搜索引擎而是在給一堆歷史包袱擦屁股。果不其然業(yè)務(wù)方那邊的情況非常典型訂單數(shù)據(jù)在MySQL里文檔和日志在Elasticsearch里商品和庫(kù)存部分?jǐn)?shù)據(jù)散落在幾個(gè)內(nèi)部API里用戶只想在一個(gè)輸入框里搜出所有東西并且希望結(jié)果按相關(guān)度混排。UniSearch這個(gè)名字聽(tīng)起來(lái)很唬人落地就是一個(gè)把各種數(shù)據(jù)源串起來(lái)的“總調(diào)度層”只是這個(gè)調(diào)度層要處理的坑遠(yuǎn)比想象中多。如果你也在做類似的事或者正準(zhǔn)備碰企業(yè)內(nèi)部搜索聚合這類需求這篇東西可以幫你少走很多彎路。我講的是自己實(shí)際落地的一套實(shí)現(xiàn)覆蓋從整體架構(gòu)選型、索引策略、查詢分發(fā)、結(jié)果融合排序到上線后各種疑難雜癥的排查實(shí)錄。所有方案都基于我踩坑后的最終版不是說(shuō)它唯一正確但至少是能扛得住線上流量和業(yè)務(wù)方花式蹂躪的一種設(shè)計(jì)。1. 統(tǒng)一搜索到底在解決什么問(wèn)題先說(shuō)一個(gè)最容易混淆的認(rèn)知UniSearch不是搜索引擎它是一層“調(diào)度和融合中間件”。搜索引擎關(guān)心的是怎么從海量文檔里快速撈相關(guān)文檔而UniSearch關(guān)心的是怎么把已經(jīng)存在于不同系統(tǒng)里的檢索能力以統(tǒng)一的接口暴露出去。你可以把它理解成公司前臺(tái)的接待員——你不會(huì)直接去找財(cái)務(wù)、去找法務(wù)、去找技術(shù)部而是把訴求告訴前臺(tái)由前臺(tái)判斷該找誰(shuí)然后把多個(gè)部門(mén)的答復(fù)整理成一份簡(jiǎn)報(bào)交給你。1.1 三種典型接入場(chǎng)景拆解第一類場(chǎng)景是結(jié)構(gòu)化數(shù)據(jù)源的直查。比如MySQL或PostgreSQL里的訂單表、用戶表檢索條件通常很固定按訂單號(hào)、按用戶名、按時(shí)間范圍、按狀態(tài)。這部分完全不值得自建倒排索引直接通過(guò)關(guān)鍵詞解析生成SQL條件即可難點(diǎn)只在于如何把用戶輸入的模糊關(guān)鍵詞安全且高效地轉(zhuǎn)換成查詢條件。第二類場(chǎng)景是全文檢索引擎的代理。Elasticsearch這類系統(tǒng)本身已經(jīng)做得很好UniSearch在這里的角色不是替代它而是把ES的DSL翻譯成內(nèi)部統(tǒng)一的查詢模型避免上游系統(tǒng)直接與ES耦合。這樣做的直接好處是哪天你想從ES換成OpenSearch或者把一部分索引遷到ZincSearch上游業(yè)務(wù)代碼一行都不用改。第三類場(chǎng)景是實(shí)時(shí)API的在線聚合。商品庫(kù)存狀態(tài)、配送進(jìn)度這類數(shù)據(jù)時(shí)效性極高而且經(jīng)常不在你自己掌控的數(shù)據(jù)庫(kù)里只能通過(guò)供應(yīng)商或兄弟團(tuán)隊(duì)提供的HTTP接口實(shí)時(shí)獲取。UniSearch需要在這種場(chǎng)景下做并發(fā)請(qǐng)求、超時(shí)控制和結(jié)果合并。1.2 為什么必須做統(tǒng)一查詢模型我見(jiàn)過(guò)很多團(tuán)隊(duì)做統(tǒng)一搜索最后做成了一個(gè)“大雜燴”網(wǎng)關(guān)每個(gè)數(shù)據(jù)源各寫(xiě)一套查詢邏輯對(duì)外暴露的參數(shù)五花八門(mén)調(diào)用方得看文檔才能搞清楚什么時(shí)候傳filters什么時(shí)候傳q什么時(shí)候要拼search_type。這種方案上線半年后基本就進(jìn)入維護(hù)地獄。真正的UniSearch項(xiàng)目第一步不是寫(xiě)代碼而是定義一套“萬(wàn)能查詢中間格式”。這套格式一般包含query關(guān)鍵詞、filters結(jié)構(gòu)化過(guò)濾條件、sort排序規(guī)則、from/size分頁(yè)、highlight高亮需求、aggs聚合需求這幾個(gè)核心部分。所有數(shù)據(jù)源適配器只認(rèn)這一套格式對(duì)外也只暴露這一個(gè)格式。Elasticsearch適配器負(fù)責(zé)把它翻譯成DSLMySQL適配器負(fù)責(zé)把它拆解成WHERE條件API適配器負(fù)責(zé)把它映射成對(duì)方接口的請(qǐng)求參數(shù)。這才是“統(tǒng)一”二字的精髓。2. 架構(gòu)分層與核心技術(shù)選型確定要做統(tǒng)一查詢模型之后接下來(lái)的任務(wù)就是搭架子。UniSearch整體分為三層接入層、編排層、適配層。這三層各管各的事互不越界。2.1 接入層標(biāo)準(zhǔn)REST接口與參數(shù)設(shè)計(jì)接入層是一層薄薄的HTTP API核心就是參數(shù)校檢和統(tǒng)一鑒權(quán)。我建議接口體設(shè)計(jì)成這樣{ query: 華為手機(jī), filters: { status: ON_SALE, category_id: [12, 13, 14] }, sort: _score, from: 0, size: 20, highlight: true, data_sources: [mysql_products, es_docs, api_stock] }這個(gè)設(shè)計(jì)里有一個(gè)容易被人忽略的點(diǎn)data_sources字段。默認(rèn)情況下UniSearch會(huì)查所有已注冊(cè)的數(shù)據(jù)源但很多調(diào)用方其實(shí)只關(guān)心其中某幾個(gè)。顯式聲明之后編排層能省掉一大半無(wú)意義的查詢分發(fā)。鑒權(quán)方面我只推薦兩種內(nèi)部系統(tǒng)用簽名校驗(yàn)外部系統(tǒng)走OAuth2的client credentials模式。不要用簡(jiǎn)單的API Key打在URL上線上日志一刷全是泄露的憑證。2.2 編排層查詢分發(fā)與并行調(diào)度編排層是整個(gè)UniSearch的心臟負(fù)責(zé)決定“這個(gè)查詢要怎么拆”。一個(gè)查詢進(jìn)來(lái)后先做意圖識(shí)別——業(yè)務(wù)簡(jiǎn)單時(shí)可以用基于規(guī)則的方式來(lái)識(shí)別比如命中某些正則就路由到對(duì)應(yīng)數(shù)據(jù)源復(fù)雜度上來(lái)之后可以訓(xùn)練一個(gè)輕量級(jí)分類器來(lái)做這件事。我這次沒(méi)有上分類模型因?yàn)闃I(yè)務(wù)場(chǎng)景足夠收斂凡是帶status過(guò)濾的基本都查訂單和商品凡是帶highlight的在ES查詢里一定用得上。分發(fā)策略上我最想強(qiáng)調(diào)一點(diǎn)并行永遠(yuǎn)比串行好但要有下限保護(hù)。所有數(shù)據(jù)源同時(shí)發(fā)起請(qǐng)求等待最慢的那個(gè)返回后統(tǒng)一合并。這樣響應(yīng)時(shí)間等于最慢數(shù)據(jù)源的耗時(shí)而不是所有數(shù)據(jù)源耗時(shí)之和。但要給每個(gè)數(shù)據(jù)源的請(qǐng)求設(shè)置超時(shí)上限比如統(tǒng)一1000毫秒超時(shí)的數(shù)據(jù)源直接降級(jí)——返回空結(jié)果并記錄一條warning日志而不是讓整個(gè)UniSearch都跟著卡死。2.3 適配層五種數(shù)據(jù)源連接器的實(shí)現(xiàn)要點(diǎn)我這次接的適配器包括MySQL、Elasticsearch、HTTP API、Redis緩存和CSV靜態(tài)文件每種都有各自的實(shí)現(xiàn)要點(diǎn)MySQL連接器要處理的是動(dòng)態(tài)SQL拼接。關(guān)鍵詞中的特殊字符必須用參數(shù)綁定而不是字符串拼接防止注入。排序字段要做白名單校驗(yàn)絕不允許把前端傳來(lái)的字符串直接拼進(jìn)ORDER BY。Elasticsearch連接器要做的是查詢語(yǔ)法翻譯。用戶輸入的name:華為 AND price:[1000 TO 3000]這種Lucene語(yǔ)法需要由適配器解析后在內(nèi)部轉(zhuǎn)換成結(jié)構(gòu)化Query對(duì)象而不是直接把字符串透?jìng)鹘oES的query_string查詢否則用戶一旦傳錯(cuò)語(yǔ)法ES直接返回400。HTTP API連接器最麻煩的是參數(shù)映射和重試。不同系統(tǒng)對(duì)參數(shù)命名習(xí)慣完全不同有的叫keyword有的叫search_text有的叫q。適配器里維護(hù)一份字段映射表通過(guò)配置而不是硬編碼來(lái)管理。Redis連接器在這里不是做緩存而是做熱搜榜和聯(lián)想詞數(shù)據(jù)源。如果業(yè)務(wù)里需要“搜索框下面顯示熱門(mén)搜索詞”這種功能直接從Redis的ZSET里取top N即可。CSV連接器適合處理那種低頻更新的基礎(chǔ)數(shù)據(jù)比如產(chǎn)品規(guī)格表、地區(qū)代碼表。每當(dāng)文件更新后把數(shù)據(jù)加載進(jìn)內(nèi)存建立簡(jiǎn)單的倒排索引查詢時(shí)直接在內(nèi)存里跑。2.4 數(shù)據(jù)一致性方案雙寫(xiě)與事件同步做統(tǒng)一搜索時(shí)一個(gè)無(wú)法回避的問(wèn)題如果一個(gè)數(shù)據(jù)源里的數(shù)據(jù)變了UniSearch怎么感知我采用的方案是“業(yè)務(wù)主庫(kù)負(fù)責(zé)寫(xiě)UniSearch負(fù)責(zé)讀”的事件驅(qū)動(dòng)模式。具體做法是業(yè)務(wù)系統(tǒng)在數(shù)據(jù)變更時(shí)發(fā)送MQ消息UniSearch消費(fèi)消息后更新對(duì)應(yīng)的ES索引或清理Redis緩存。這里有一個(gè)大坑如果只依賴MQ很容易出現(xiàn)消息丟失導(dǎo)致數(shù)據(jù)不一致。所以我還保留了一個(gè)每天凌晨的全量對(duì)賬任務(wù)比對(duì)MySQL和ES里的記錄數(shù)及關(guān)鍵字段的校驗(yàn)值發(fā)現(xiàn)不一致的自動(dòng)重新同步。提示沒(méi)有對(duì)賬機(jī)制的統(tǒng)一搜索本質(zhì)上就是一個(gè)定時(shí)會(huì)“出現(xiàn)臟數(shù)據(jù)”的搜索。生產(chǎn)環(huán)境上線之前一定要把全量對(duì)賬定時(shí)任務(wù)安排上。3. 核心流程的工程化實(shí)現(xiàn)理念講得再多最后還是要落到代碼。我按一個(gè)真實(shí)的查詢請(qǐng)求從進(jìn)來(lái)到返回的完整鏈路把每個(gè)關(guān)鍵環(huán)節(jié)的工程實(shí)現(xiàn)都拆開(kāi)講一遍。3.1 查詢預(yù)處理同義詞、糾錯(cuò)、分詞策略原始查詢?cè)~不能直接拿去查數(shù)據(jù)源。首先要做的是查詢預(yù)處理。其中同義詞擴(kuò)展能讓“筆記本”同時(shí)命中“筆記本電腦”和“l(fā)aptop”。實(shí)現(xiàn)上可以通過(guò)配置一個(gè)同義詞詞典在查詢時(shí)做檢索詞擴(kuò)展。其次是糾錯(cuò)這里我用的是編輯距離算法配合業(yè)務(wù)詞表庫(kù)當(dāng)用戶輸入“華偽手機(jī)”時(shí)能識(shí)別出“華為手機(jī)”才是正確的查詢?cè)~。需要注意的是糾錯(cuò)一定要區(qū)分“建議糾正”和“自動(dòng)糾正”前者是在搜索結(jié)果下方提示“您是不是要找華為手機(jī)”后者是直接拿修正后的詞去查詢。我個(gè)人的經(jīng)驗(yàn)是默認(rèn)用建議式糾正只有用戶明確點(diǎn)擊后再執(zhí)行新查詢自動(dòng)糾正太容易翻車。分詞策略上有一個(gè)重要原則索引期分詞和查詢期分詞必須保持一致否則會(huì)出現(xiàn)明明數(shù)據(jù)在那、卻搜不出來(lái)的情況。比如MySQL數(shù)據(jù)源針對(duì)中文商品名做檢索如果索引時(shí)期用的是普通的ngram分詞查詢時(shí)期的預(yù)處理分詞也必須支持同種切法。如果一致性無(wú)法保證寧可用簡(jiǎn)單的contains匹配也不要上一個(gè)花哨的、不可控的分詞算法。3.2 多路結(jié)果的歸一化與字段對(duì)齊并行查詢返回的四五份結(jié)果形態(tài)各異需要把他們轉(zhuǎn)成統(tǒng)一的結(jié)果對(duì)象。這個(gè)過(guò)程我稱之為歸一元組。具體的歸一化規(guī)則如下表數(shù)據(jù)源類型原始返回結(jié)構(gòu)歸一化后的關(guān)鍵字段MySQL行記錄每行是一個(gè)dictid,title,summary,url,score,sourceElasticsearch命中_source及highlightid,title,summary,url,score,sourceHTTP API返回值自定義JSON結(jié)構(gòu)id,title,summary,url,score,sourceRedis熱詞ZSET member和scoreid為空title為熱詞score為熱度CSV內(nèi)存索引內(nèi)存對(duì)象id,title,summary,url,score,source這里有一個(gè)細(xì)節(jié)值得注意score的歸一化。MySQL和API適配器本來(lái)是不產(chǎn)生相關(guān)性分?jǐn)?shù)的我通常會(huì)在適配器里根據(jù)命中字段的重要程度去手動(dòng)計(jì)算一個(gè)基礎(chǔ)分值。比如命中標(biāo)題給3分命中摘要給1分命中標(biāo)簽給2分這個(gè)邏輯放在適配層做。3.3 融合排序策略加權(quán)、歸一化與去重所有數(shù)據(jù)源的結(jié)果匯集到一起之后面臨的難題是這些分?jǐn)?shù)并不可直接比較。ES返回0.8的分?jǐn)?shù)和MySQL里我手算的3分量綱完全不同。直接按原始分?jǐn)?shù)排序沒(méi)有任何意義。所以我采用了先歸一化、再加權(quán)、再融合的三步策略。歸一化可以用min-max歸一化或者使用Wilson下限來(lái)平滑處理避免單條極端分?jǐn)?shù)帶偏整體。加權(quán)則需要按照業(yè)務(wù)定義數(shù)據(jù)源權(quán)重。比如訂單這個(gè)數(shù)據(jù)源權(quán)重是1.0商品是0.9文檔是0.6熱詞聯(lián)想是0.3最終綜合得分的公式大致是final_score normalized_score(source) * source_weight去重策略上面我處理的是跨數(shù)據(jù)源重復(fù)的情況。比如商品和文檔同時(shí)引用了同一篇文章的地址或訂單和商品數(shù)據(jù)源里回到同一個(gè)商品ID。去重邏輯是基于ID加URL的聯(lián)合唯一鍵來(lái)判斷。保留哪條優(yōu)先級(jí)順序是API結(jié)果 MySQL ES CSV高優(yōu)先級(jí)的保留其余的被排除。3.4 分頁(yè)與深翻頁(yè)問(wèn)題的工程解法統(tǒng)一搜索還有個(gè)細(xì)節(jié)讓很多人頭疼分頁(yè)。Elasticsearch超過(guò)1萬(wàn)條深翻頁(yè)時(shí)會(huì)報(bào)錯(cuò)MySQL深度分頁(yè)也慢得離譜而API接口壓根不支持from加size特別大的查詢。所以UniSearch的默認(rèn)分頁(yè)上限是100頁(yè)每頁(yè)最多50條。超過(guò)這個(gè)范圍的查詢有兩種降級(jí)方案可選一種是返回提示讓用戶收窄條件另一種是利用ES的search_after做深翻頁(yè)游標(biāo)同時(shí)MySQL側(cè)改用“最后一個(gè)可見(jiàn)ID”來(lái)做keyset分頁(yè)。我的建議是優(yōu)先用方式一畢竟正經(jīng)用戶不會(huì)翻到100頁(yè)以后。3.5 結(jié)果緩存與性能兜底在緩存層上我用了兩級(jí)緩存策略。L1是進(jìn)程內(nèi)緩存使用TTL為30秒適合命中熱點(diǎn)查詢L2是Redis緩存TTL為5分鐘。查詢請(qǐng)求進(jìn)來(lái)先查L(zhǎng)1沒(méi)命中再查L(zhǎng)2最后才真正分發(fā)到底層數(shù)據(jù)源。但緩存粒度不應(yīng)該是整個(gè)查詢結(jié)果而應(yīng)該是數(shù)據(jù)源級(jí)別的結(jié)果片段。因?yàn)楫?dāng)四個(gè)數(shù)據(jù)源里只有一個(gè)數(shù)據(jù)變了如果緩存的是合并后的完整結(jié)果其他三個(gè)數(shù)據(jù)源的數(shù)據(jù)會(huì)被無(wú)謂地過(guò)期。單獨(dú)緩存各數(shù)據(jù)源返回的原始片段再在編排層做融合可以最大程度提高緩存命中率。4. 上線調(diào)試與問(wèn)題排查實(shí)錄UniSearch這種糅合了多個(gè)系統(tǒng)的中間層一旦出問(wèn)題表象往往是一坨亂麻。這一章把我線上遇到過(guò)的典型問(wèn)題和排查思路整理出來(lái)你可以當(dāng)做一個(gè)速查表來(lái)用。4.1 請(qǐng)求超時(shí)罪魁禍?zhǔn)淄恰按小钡谝淮紊暇€壓測(cè)時(shí)P99延遲高達(dá)4.8秒這顯然不可接受。排查發(fā)現(xiàn)問(wèn)題不在某個(gè)數(shù)據(jù)源性能差而在于我的初版編排邏輯是按順序調(diào)用各個(gè)適配器。每個(gè)適配器平均耗時(shí)200毫秒串行調(diào)五個(gè)就是1秒加上網(wǎng)絡(luò)開(kāi)銷和數(shù)據(jù)庫(kù)慢查詢延遲自然爆炸。改造方案是把串行調(diào)用改成Java虛擬線程或Python asyncio并發(fā)調(diào)度整體吞吐從每秒300漲到2000。如果你用的是Java項(xiàng)目CompletableFuture或者虛擬線程都行如果是Gogoroutine加channel天然支持Python的話我會(huì)夸一句asyncio。但我不推薦用裸線程池因?yàn)榫€程池耗盡太容易造成雪崩。經(jīng)驗(yàn)編排層強(qiáng)制要求所有數(shù)據(jù)源調(diào)用必須并行發(fā)起并且設(shè)置統(tǒng)一的超時(shí)上限這個(gè)是UniSearch性能的第一命門(mén)。4.2 數(shù)據(jù)源返回慢不能拖死整體壓測(cè)中還發(fā)現(xiàn)如果其中一個(gè)API數(shù)據(jù)源響應(yīng)卡頓幾秒所有請(qǐng)求都會(huì)被阻塞。后來(lái)我把超時(shí)控制做成“逐源設(shè)置”的配置項(xiàng)默認(rèn)統(tǒng)一1000毫秒但對(duì)關(guān)鍵核心數(shù)據(jù)源可以放寬到1500毫秒對(duì)非關(guān)鍵數(shù)據(jù)源比如熱詞、聯(lián)想只給300毫秒。超時(shí)的數(shù)據(jù)源不再等待直接降級(jí)為空結(jié)果在返回體里帶上timeout_sources字段告訴前端這次查詢哪些源超時(shí)了。這樣即使某一個(gè)下游系統(tǒng)故障主鏈路依然是穩(wěn)定的。4.3 數(shù)據(jù)源返回慢不能拖死整體調(diào)參時(shí)發(fā)現(xiàn)ES查詢耗時(shí)最大的瓶頸來(lái)自兩次查詢一次普通請(qǐng)求一次獲取total命中數(shù)。ES默認(rèn)會(huì)在每次查詢時(shí)計(jì)算精確總數(shù)當(dāng)索引體量很大的時(shí)候這種計(jì)算代價(jià)很高。我把ES的track_total_hits設(shè)置為false返回總數(shù)改為近似值查詢性能提升35%。對(duì)搜索結(jié)果而言100頁(yè)以內(nèi)的總數(shù)用近似值完全能滿足需求沒(méi)必要讓精確計(jì)數(shù)拖慢整體響應(yīng)。4.4 Redis緩存穿透與更新風(fēng)暴上線初期緩存穿透非常嚴(yán)重。熱詞數(shù)據(jù)源本身數(shù)據(jù)量不大但是用戶搜索時(shí)經(jīng)常輸入一個(gè)冷門(mén)詞結(jié)果Redis沒(méi)緩存直接穿透到后端而MySQL又拿不到結(jié)果就變成“緩存擊穿空結(jié)果”的雙重浪費(fèi)。我的解法除了常規(guī)的空值緩存之外還加了一層布隆過(guò)濾器在業(yè)務(wù)側(cè)維護(hù)一個(gè)基于所有已知有效查詢?cè)~的布隆過(guò)濾器。攔截掉明顯不存在的查詢MySQL的壓力直接下降了一個(gè)數(shù)量級(jí)。4.5 結(jié)果排序不穩(wěn)定導(dǎo)致投訴有一次業(yè)務(wù)方反饋同一個(gè)關(guān)鍵詞搜索的結(jié)果過(guò)幾分鐘再搜就發(fā)現(xiàn)順序變了。查下來(lái)是融合排序時(shí)分段歸一化算法的問(wèn)題對(duì)于數(shù)量很少的分片結(jié)果min-max歸一化會(huì)放大微小差異同一句話在不同時(shí)間點(diǎn)檢索甚至只因ES分片間的tie_breaker不同就讓分?jǐn)?shù)發(fā)生細(xì)微抖動(dòng)。最后我把歸一化的下限做了鉗制設(shè)置MIN_SCORE0.1保證所有結(jié)果的基礎(chǔ)分不低于0.1就不會(huì)出現(xiàn)因?yàn)槲⑿〔▌?dòng)導(dǎo)致的排序漂移。4.6 日志與追蹤統(tǒng)一檢索必須帶TraceId這是我覺(jué)得整塊項(xiàng)目里最值得做的事情之一。UniSearch一次查詢會(huì)觸達(dá)多個(gè)數(shù)據(jù)源一旦線上有問(wèn)題沒(méi)有日志鏈路就是在黑暗里摸大象。我強(qiáng)制要求日志系統(tǒng)里集成TraceId從接入層生成貫穿編排層、適配層直到底層數(shù)據(jù)源。每個(gè)數(shù)據(jù)源的耗時(shí)、返回條數(shù)、異常堆棧都掛在這個(gè)TraceId下。后續(xù)排查的時(shí)候只需要用TraceId搜索一次完整的調(diào)用鏈就能定位問(wèn)題。這個(gè)習(xí)慣我從一開(kāi)始就建了中間有一次線上故障能十分鐘之內(nèi)定位全靠它。5. 工具、參數(shù)與調(diào)優(yōu)細(xì)節(jié)總結(jié)最后把我在整個(gè)把UniSearch項(xiàng)目中實(shí)際使用過(guò)的工具和一些重要的調(diào)優(yōu)參數(shù)整理一下。不是每一家公司的技術(shù)棧一樣所以工具選型僅作參考。5.1 基礎(chǔ)設(shè)施選型參考組件選用方案理由接入層APIFastAPI / Spring Boot高并發(fā)性能好社區(qū)資源豐富異步編排Python asyncio / Java CompletableFuture天然支持并發(fā)調(diào)用全文檢索引擎Elasticsearch 8.x成熟穩(wěn)定生態(tài)完善業(yè)務(wù)數(shù)據(jù)庫(kù)MySQL 8.0業(yè)務(wù)主存儲(chǔ)穩(wěn)定可靠緩存Redis 7.x支持ZSET熱榜及多級(jí)緩存MQ消息RabbitMQ支撐雙寫(xiě)事件同步監(jiān)控Prometheus Grafana覆蓋接口QPS、P99時(shí)延、各適配器狀態(tài)5.2 一鍵查詢鏈路耗時(shí)監(jiān)控每個(gè)適配器的查詢耗時(shí)必須拆開(kāi)觀察。我在Grafana里建了十幾個(gè)看板但日常最常用的是三個(gè)總接口請(qǐng)求量及響應(yīng)時(shí)間曲線、各數(shù)據(jù)源平均耗時(shí)對(duì)比柱狀圖、各數(shù)據(jù)源超時(shí)次數(shù)統(tǒng)計(jì)表。每次發(fā)布新版本之后第一件事不是看功能對(duì)不對(duì)而是看適配器耗時(shí)有沒(méi)有異常波動(dòng)。如果某個(gè)源的平均耗時(shí)從200毫秒突增到800毫秒不管業(yè)務(wù)方怎么說(shuō)“沒(méi)問(wèn)題”都要主動(dòng)去查一下對(duì)方系統(tǒng)的負(fù)載。5.3 上線前必做的三件準(zhǔn)備工作第一件梳理清楚哪些數(shù)據(jù)源是強(qiáng)依賴、哪些是弱依賴。強(qiáng)依賴掛了整個(gè)搜索不可用弱依賴掛了可以容忍。這個(gè)梳理結(jié)果決定了超時(shí)和降級(jí)方案怎么設(shè)計(jì)。第二件建立一套全鏈路的自動(dòng)化回歸用例。針對(duì)每個(gè)數(shù)據(jù)源準(zhǔn)備標(biāo)準(zhǔn)查詢集預(yù)期結(jié)果集鎖定在固定基線里。每次代碼改動(dòng)先跑這組用例避免只修了A數(shù)據(jù)源卻把B數(shù)據(jù)源的適配邏輯弄壞了。第三件準(zhǔn)備好對(duì)賬腳本。我建議做兩級(jí)對(duì)賬數(shù)據(jù)記錄數(shù)對(duì)賬和字段級(jí)對(duì)賬。先比數(shù)量再比關(guān)鍵字段的哈希一致性這是防數(shù)據(jù)漂移的兜底。提示不要在下班前一小時(shí)首次灰度發(fā)布統(tǒng)一搜索項(xiàng)目。第一晚大概率會(huì)有各種數(shù)據(jù)源連接和權(quán)限的幺蛾子留足排查時(shí)間比什么都重要。6. 下一階段演進(jìn)方向UniSearch做到這里已經(jīng)能穩(wěn)定支撐日常查詢了。但如果業(yè)務(wù)量再往上走有幾個(gè)演進(jìn)點(diǎn)是我明確計(jì)劃要做的。第一個(gè)是引入真正的個(gè)人化排序基于用戶的歷史點(diǎn)擊、收藏行為訓(xùn)練一個(gè)輕量級(jí)的learning to rank模型把融合排序從手工加權(quán)升級(jí)為模型打分。第二個(gè)是增加搜索熱力統(tǒng)計(jì)從Redis熱詞出發(fā)把用戶每天的搜索詞、點(diǎn)擊結(jié)果、跳失率匯入數(shù)據(jù)倉(cāng)庫(kù)形成業(yè)務(wù)洞察報(bào)表。第三個(gè)是更強(qiáng)的同義詞動(dòng)態(tài)挖掘現(xiàn)在用的靜態(tài)詞表維護(hù)成本高后續(xù)計(jì)劃基于用戶搜索日志里的共現(xiàn)詞挖掘同義詞實(shí)現(xiàn)詞表的自我更新。我個(gè)人在這整件事中最大的體會(huì)是做統(tǒng)一搜索難點(diǎn)從來(lái)不在搜索本身而在“統(tǒng)一”二字。統(tǒng)一意味著你要面對(duì)各種歷史系統(tǒng)的怪癖面對(duì)無(wú)法控制的字段命名面對(duì)每一個(gè)數(shù)據(jù)源獨(dú)有的查詢語(yǔ)法和限制。與其想著一套代碼適配全部不如把適配層的邊界劃清楚把統(tǒng)一查詢模型設(shè)計(jì)得足夠通用。只要這兩件事做踏實(shí)UniSearch這個(gè)名字才真正配得上。最后再分享一個(gè)小技巧上線那天先在測(cè)試環(huán)境用真實(shí)用戶搜索日志回放壓測(cè)一遍不要用模擬流量因?yàn)橹挥姓鎸?shí)搜索日志才會(huì)暴露出那些你完全想不到的奇葩查詢?cè)~和冷門(mén)字段組合這一條比任何架構(gòu)設(shè)計(jì)都更能幫你省去后續(xù)的救火時(shí)間。