重構實踐:基於騰訊雲Elasticsearch搭建QQ郵箱全文檢索

​導語 | 隨着用戶郵件數量越來越多,郵件搜索已是郵箱的基本功能。QQ 郵箱於 2008 年推出的自研搜索引擎面臨着存儲機器逐漸老化,存儲機型面臨淘汰的境況。因此,需要搭建一套新的全文檢索服務,遷移存儲數據。本文將介紹 QQ 郵箱全文檢索的架構、實現細節與搜索調優。文章作者:幹勝,騰訊後臺研發工程師。

 

一、重構背景

 

QQ 郵箱的全文檢索服務於2008年開始提供,使用中文分詞算法和倒排索引結構實現自研搜索引擎。設計有二級索引,熱數據存放於正排索引支持實時檢索,冷數據存放於倒排索引支持分詞搜索。在使用舊全文檢索過程中存在以下問題:

 

  • 機器老化、磁盤損壞導致丟數據;

  • 業務邏輯複雜,代碼龐大晦澀,難以維護;

  • 使用定製化kv存儲,已無人維護;

  • 不存儲原文,無法實現原生高亮;

  • 未索引超大附件名。

 

舊的全文檢索在使用中長期存在上述問題,恰逢舊的存儲機器裁撤,藉此機會重構 QQ 郵箱的全文檢索後臺服務。

 

二、新全文檢索架構

 

Elasticsearch 是一個分佈式的搜索引擎,支持存儲、搜索和數據分析,有良好的擴展性、穩定性和可維護性,在搜索引擎排名中蟬聯第一。

 

ES 的底層存儲引擎是 Lucene,ES 在 Lucene 的基礎上提供分佈式集羣的能力以確保可靠性、提供 REST API 以確保可用性。

 

Lucene 底層使用倒排索引提供搜索能力,使用 LSM tree 合併處理 Doc 加快索引速度,使用 Translog 持久化數據,實現方式與郵箱舊全文檢索相似。

 

爲了快速搭建出一套新全文檢索後臺並完成遷移,QQ 郵箱全文檢索的重構選擇 Elasticsearch 作爲搜索引擎,同時響應自研上雲號召,一步到位直接使用騰訊雲 ES 構建搜索服務。

 

1. 郵件搜索特點

 

郵箱的發信和收信行爲都會觸發寫全文檢索,而搜索行爲會觸發讀全文檢索,呈現明顯的寫多讀少。

 

區別於互聯網搜索,郵件檢索有自己的特點:

 

 搜索範圍準確度排序
互聯網搜索 整個互聯網 容忍少量漏搜或多搜 按相關度排序
郵件檢索 用戶自己的郵箱 要求精確結果 按時間排序,同時支持按發件人、時間、已讀未讀進行分類

 

2. 全文檢索後臺架構

 

郵箱全文檢索模塊 fullsearch 的整體架構如上圖所示,fullsearch 承擔的功能是收錄用戶的郵件、記事等內容並提供查詢。fullsearch 模塊下游直接對接騰訊雲 ES,內網通過 http 請求訪問 ES 的 REST API。模塊上游的請求分爲兩類:

 

(1)增、刪、改

 

入信、發信、刪信等行爲會觸發更改 ES 內的 doc,入信、發信對實時性要求一般但可靠性要求較高,而刪信行爲不要求實時性。這類操作都可以異步處理。

 

(2)查

 

搜索行爲包括郵件普通搜索、郵件高級搜索,將來還有郵箱內全品類搜索。這類搜索行爲要求較高的實時性和準確性,需要同步處理。

 

 

fullsearch 內部設計如下:

 

  • 使用 HTTP 協議與騰訊雲ES通訊,傳輸 json 格式數據,郵箱後臺廣泛使用的 protobuf 數據結構能輕鬆轉換爲 json 格式;

  • esproxy 使用 curl 連接池代理郵箱後臺與ES的http連接,以提升網絡連接速度;

  • 使用 MQ 對增、刪、改這類異步請求進行削峯,以保護下游 ES。同時利用 MQ 延時和重試功能,確保請求被成功處理;

  • 對搜索結果進行過濾,避免搜索結果列表出現已刪除郵件。在 ES 故障時,提供另一種搜索機制兜底。

 

三、新全文檢索的實現細節

 

利用郵箱後臺現有的組件庫,如 svrkit rpc 框架、protobuf 數據結構、自研 MQ 等能快速將上述 fullsearch 模塊搭建出來,但實現過程中遇到以下幾個實際問題。

 

1. 號段索引 or uin索引

 

第一個要解決的是如何分配索引的問題。最初爲了實現 ES 內的數據按 uin 進行隔離,每個 uin 建一個索引。

 

隨着用戶數量上來後,ES 提示分片數量達到上限,不可創建新的索引。這是因爲 ES 集羣對每個索引都會維護映射和狀態信息,索引和分片數量過多會導致佔用大量內存。詳情可參考文檔


ES 官方建議將結構相同的數據放入一個索引,既然不能按 uin 建索引,那可不可以建一個索引容納全量用戶的數據呢?答案是否定的,分片數量過多也會對內存有很大的開銷。

 

ES 的索引概念相當於 MySql 的表概念,一個索引對應一張表,類似 MySql 可以分表,ES 也可以拆分索引。

 

所以一個折中的方案是(如下圖),按 uin 尾號號段(如果號段數據不均勻可以按 uin 哈希)分別建立若干個索引,每個索引內設置少量分片。隨着郵件數量上漲,每個索引內的數據量也將上漲,將來可以通過擴展分片數量解決。

 

 

所有搜索操作都帶上號段索引,如"428/_search",可達到相對較快的搜索速度,但無法達到按 uin 建索引的搜索速度,因爲搜索速度取決於每個索引內的 doc 數量。有沒有辦法讓號段索引的搜索速度媲美 uin 索引的速度呢?

 

ES 官方提供了一個索引設置選項"index.sort",該選項可以使索引內的 doc 在存儲時按照某幾個字段的升序或降序進行順序存儲。如果設置 doc 按 uin 順序存儲,在搜索時就能將搜索範圍縮小到屬於某個 uin 的 doc 存儲範圍,這將顯著提升搜索速度。

 

與此同時會帶來一個負面影響,在增、刪、改 doc 時,由於要重排 doc 順序,這些操作的速度將下降 1/3,需要根據業務特點做權衡。

 

值得注意的是,這個選項只能在新建索引的時候開啓,開啓後不可改變,故需要提前壓測來權衡是否開啓該選項。

 

2. 郵件正文 to ES字段

 

如果想讓郵件內容被索引到,一般會將郵件主題、正文、附件等分別添加到 doc 的一個字段,並將該字段設置爲 type:text。郵件正文被放進 ES 的 text 字段之前,需要做一些預處理,來保證將來的檢索質量。

 

郵箱全文檢索會收錄郵件、記事本和在線文檔的數據。如下圖以郵件正文爲例,郵件正文一般是一段 html,如果將 html 收錄進 ES 太浪費存儲空間,而且會干擾高亮的識別,所以需要提取郵件正文的純文本。

 

 

同時,郵件的超大附件信息被放在了正文裏,如果搜索超大附件名則需要去搜正文而不是搜附件,這不符合用戶使用常識。

 

另外,有一些 html 節點內包含大量亂碼或 url,屬性爲 display:none,比如郵箱的超大附件,這些亂碼文本也是需要剔除掉的。  

 

<span style="display:none;">:http://wx.mail.qq.com/ftn/download?func=3&k=c7991f38b1d109adf4ea5216042ca62df1e0f2a0b6a2ba26e6a261631539efd62535&key=c7991f38b1d109adf4ea4d38603464390ec0623866a2ba26e6a261631539efd62535&code=8fc8b4d9</span>

 

要解決上述問題,可以從解析 html 節點入手:

 

  • 提取純文本節點並累加,即可過濾所有 html 標籤;

  • 識別含有超大附件的節點,並提取超大附件名;

  • 過濾屬性爲 display:none 的節點。

 

此時問題就變成尋找一個符合要求的html解析器,把 htmlbody 解析爲 dom 樹。常見的 xml 解析器有 rapidxml、tinyxml 和 pugixml。

 

筆者選擇的是pugixml,優點是速度快、易於使用且支持 xpath,缺點是解析較爲嚴格、遇到不規範的 html 會拋異常。

 

如下圖所示,筆者對 pugixml 進行了一番改造,使之增強對 html 的兼容性。在 pugixml 出現異常時,使用速度稍慢些的 ekhtml 解析器作爲兜底。

 

 

3. ProtoBuf to Json

 

fullsearch 模塊調用騰訊雲 ES 的 REST API 使用json數據包進行交互,有大量的打包 json 和解析 json 的操作。而郵箱後臺廣泛使用的數據結構是 protobuf,這就需要完成 protobuf 到 json 的互相轉換。


如果手動判斷 protobuf/json 是否存在某個字段,再使用 rapidjson 或 jsoncpp 進行解包和封包,則太繁瑣且容易出錯。

 

 

這裏選擇直接讓 protobuf 字段與 json 字段進行映射,使用 protobuf 自帶的工具 MessageToJsonString 和 JsonStringToMessage進行 protobuf 和 json 的相互轉換,使用起來十分方便。

 

 

四、搜索調優

 

1. 調優背景

 

新全文檢索搭建上線後測試遷移了一批郵件,收到一些關於搜索結果不精確的反饋:

 

  • 搜出大量有關郵件,但想找的郵件不在列表第一頁;

  • 搜不出郵件;

  • 無法通過訂單號精確查找郵件。

 

初步分析,主要由以下幾個原因造成:

 

  • 模糊搜索結果雖能按相關度排序,但前端顯示結果按時間倒序排序,導致相關度高的結果不一定排在第一頁;

  • 將模糊搜索替換爲精確搜索後,搜索過於嚴格,導致搜不出郵件;

  • 無法知道用戶的意圖是精確搜索還是模糊搜索,導致不能用一種搜索模式滿足所有用戶搜索意圖;

  • 訂單號一般由字母+數組組成,分詞器處理訂單號時,由於默認的分詞規則,會丟棄單字母或單數字,導致無法精確匹配。

 

下面首先詳細介紹 ES 的搜索機制,然後通過案例分析對 ES 搜索做一定的優化。

 

2. ES搜索機制

 

ES 的全文搜索查詢主要分爲兩種:match 和 match_phrase,它們的搜索機制是:

 

  • 入信時,ES 分詞器先對 doc 中 type:text 字段進行分詞,默認記錄下每個分詞的詞頻和詞語在原文中的位置,存在倒排索引中;

  • 搜索時,對搜索關鍵字進行分詞,根據關鍵字分詞在倒排索引中查到每個分詞的 docid 列表。如果 match(operator=or),則停止搜索並返回 docid 列表;

  • 對第二步每個分詞的 docid 列表求交集得到新的 docid 列表,使得列表中每個 docid 都出現所有分詞。如果是 match 搜索,則停止搜索並返回 docid 列表;

  • 比較第三步每個 docid 中所有分詞的相對位置,是否與第一步中原文分詞的相對位置相同,過濾掉相對位置不同的 docid,結束搜索。這一步是 match_phrase 纔有的,且會小幅增加搜索耗時。

 

來看一個例子,搜索關鍵字"銀行賬單",ik_smart 分詞列表爲["銀行", "賬單"]。

 

  • match_phrase 搜索最嚴格,要求"銀行"、“賬單”同時出現且相鄰,只能匹配一篇文章;

  • match(operator=or) 只要求出現"銀行"、“賬單”即可,能匹配所有文章;

  • match(operator=and) 要求同時出現"銀行"、“賬單”,但對詞語間隔不做要求,匹配數量介於兩者之間,搜索結果精度優於 match(operator=or)。

 

3. 兩級搜索

 

fullsearch 模塊使用 match_phrase 處理精確搜索,使用 match(operator=and) 處理模糊搜索。

 

爲了最大化滿足不同用戶對精確搜索和模糊搜索的需求,先用 match_phrase 精確搜索,搜不到內容再用 match 模糊搜索。

 

統計顯示精確搜索搜到內容佔搜索請求的比例達到 90%,且模糊搜索的耗時遠小於精確搜索,兩次搜索不會增加太多等待時間。

 

模糊搜索可能搜到大量結果,按時間倒序後,相關度高的結果可能排在後面,造成不好的搜索體驗。這裏可以對模糊搜索的結果進行剪枝,去除低評分的結果,使得相關度高的結果適當靠前。

 

另外,可通過調整不同字段的權值(boost)來調整搜索評分。按照多數用戶的搜索習慣,適當調高主題搜索權重。

 

未來,郵箱還將在搜索框集成查詢語法,讓用戶自定義搜索條件(and、or、not)。

 

4. 調整match_phrase

 

使用 Kibana 的調試工具可以很方便地獲取一段文字被分詞器處理後的 token 列表,如下圖,token 列表中每個 token 都是一個分詞。在上文 ES 搜索機制中提到,match_phrase 會確保搜索關鍵字 token 列表中的詞語、詞語間隔和詞語順序,與原文分詞後的 token 列表相同。

 

(1)測試案例

 

下面來看一個案例,原文是“一品城府7-1-501”,搜索關鍵字“一品城府501”無法精確搜索。

 

(2)分析原因

 

如下圖,搜索關鍵字分詞 token 列表中的詞語、詞語順序與原文相同,但詞語間隔不對,則 match_phrase 失敗。

 

 

(3)解決思路

 

match_phrase 有個參數 slop,設置 slop 值能容忍一定的 token 列表詞語間隔。在4.2節第四步分詞匹配時會不斷變換分詞位置,可以只過濾掉詞語間隔超過 slop 的 docid。

 

這個案例中,match_phrase.slop 值設爲 4 可解決問題。但設置 slop 值將增大匹配工作量,如果 slop 過大將嚴重拖慢搜索速度,一般 slop 設置爲 5 以內。

 

5. 改造分詞器

 

(1)測試案例

 

測試時,有一類反饋比較集中,搜索字母+數字(如訂單號)搜不出結果。看一個案例,原文是“AL0927_618”,搜索關鍵字“AL0927”,無論使用精確搜索還是模糊搜索都搜不出內容。

 

(2)分析原因

 

因爲關鍵字的“tokenal0927”不在原文 token 列表中,不滿足 4.2 節搜索機制中第三步匹配條件。這個問題其實是分詞器的缺陷,ik 分詞的 github 上有人提過類似案例,但無人迴應。

 

 

(3)解決思路

 

對比上圖中原文和關鍵字 token 列表,如果搜索時關鍵字分詞 token 列表中不出現關鍵字本身(al0927),就能成功實現 match_phrase 匹配。有兩種實現方案:

 

  1. 將搜索關鍵字做個預處理,從 al0927 變爲 al 空格 0927;

  2. 尋找一個新的分詞器,使得 al0927 的分詞列表只含有 al、0927。

 

觀察上圖 ik_max_word 分詞器處理後的 token 列表,token 列表中類型爲 LETTER 的 token 就是關鍵字本身,是不是過濾 LETTER 類型 token 就能解決問題?

 

在測試驗證後,筆者選擇第二種方案,基於 ik 分詞器進行改造,過濾 token 列表中類型爲 LETTER 類型的 token,新分詞器命名爲 xm_ik_max_word。

 

 

新分詞器的效果如上圖所示,這時搜索 AL0927 就能夠實現精確匹配。改造後的分詞器解決了使用 ik 分詞無法對字母+數字關鍵字精確搜索的問題。

 

6. 使用空格分詞器

 

(1)測試案例

 

使用改造後的 xm_ik_max_word 分詞器後解決了大部分訂單號搜索的問題,但測試中出現一個無法精確搜索的案例,搜索關鍵字“20X07131A”。

 

(2)分析原因

 

 

使用不同分詞器對 20X07131A 處理的分詞 token 列表如上表所示,ik_max_word 和 xm_ik_max_word 分詞器處理後會丟失末尾 a,因爲字母 a 是 ES 默認的 stop words。

 

如果使用 xm_ik_max_word 分詞器精確搜索,可能會匹配上 20X07131A、20X07131AB、20X07131B 等,出現很多無關結果。

 

(3)解決思路

 

由於原文被 index 到 ES 時使用的是 ik_max_word 分詞,保留了 LETTER 類型 token,對於訂單類型搜索則可以讓搜索關鍵字分詞後只剩下 LETTER 類型 token。

 

筆者使用的是 whitespace 分詞器,讓用戶來決定分詞方式。whitespace 會對搜索關鍵字按空格分詞,並自動完成小寫轉換和特殊字符處理。如上表,whitespace 分詞器的 token 列表能精確匹配上 20X07131A 所在的原文。

 

五、結語

 

藉助騰訊雲ES作爲搜索平臺,可以很快完成一套全文檢索服務的搭建。騰訊雲ES作爲Paas,可以方便地進行擴縮容與維護。

 

隨着ES版本迭代,ES支持越來越多的功能配置,需要根據業務特點來決定索引階段與搜索階段使用的配置。

 

郵箱的全文檢索業務在切換到騰訊雲ES後,平穩地完成了後臺搜索平臺的遷移,並解決了舊全文檢索存在的問題。

 

ES內置的ik分詞器無法滿足某些業務使用需求時,可以對ik分詞器做改造,或更換別的分詞器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章