微信全文搜尋優化之路

騰訊雲加社群發表於2017-10-20
歡迎大家前往騰訊雲技術社群,獲取更多騰訊海量技術實踐乾貨哦~

作者: jiaminchen,微信終端開發團隊的一員
本文首次發表在《程式設計師》雜誌 2017 年 09 月期。

前言

基於本地資料的全文搜尋(Full-Text-Search,FTS)在移動應用上扮演著重要的角色。與基於服務端提供的搜尋服務不同,移動端受硬體條件限制,尤其在資料量相對較大的情況下,搜尋效能問題表現得十分突出。本文以移動平臺廣泛採用的SQLite FTS Extension為例,介紹了移動平臺FTS的基本原理,結合微信安卓客戶端自身實踐,重點講述微信在FTS上的一些效能優化經驗。

SQLite FTS Extension

SQLite FTS Extension是SQLite為全文搜尋開發的一個外掛,它是內嵌在標準的SQLite分佈版本當中,它具有如下的特點:

搜尋速度快:使用倒排索引加速查詢過程

穩定性好:目前SQLite在移動端的穩定性比較好,FTS Extension就是SQLite的基礎上搭建的

接入簡單:Android和IOS平臺本身就支援SQLite,並且FTS Extension的使用就和正常使用SQLite表一樣。

相容性好:受益於SQLite本身相容性很好,SQLite FTS Extension也有很好的相容性。

目前SQLiteFTSExtension釋出了5個版本,我簡單說下三個主流的版本。

FTS3:基礎版本,具有完整的FTS特性,支援自定義分詞器,庫函式包括Offsets,Snippet。

FTS4:在FTS3的基礎上,效能有較大優化,增加相關性函式計算MatchInfo。

FTS5:和FTS4有較大變動,儲存格式上有較大改進,最明顯就是Instance-List的分段儲存,能夠支援更大的Instance-List的儲存;並且開放ExtensionApi,支援自定義輔助函式。FTS5釋出於2015年中。

儲存架構

微信全文搜尋在2014 年底上線,最初主要服務於聯絡人和聊天記錄的業務搜尋。在方案設計之初,為了讓這個功能有很好的體驗,同時考慮到未來接入業務的會不斷增多,我們設計目標是:

1. 搜尋速度快

微信全文搜尋使用SQLite FTS4 Extension,通過倒排索引提高搜尋速度。

2. 業務獨立性

微信的核心業務是聯絡人和訊息,而微信全文搜尋無論是在建立索引、更新索引或者刪除索引時,都需要處理大量資料,為了使得全文搜尋不影響微信的核心業務,採用如下的儲存架構:

獨立DB、讀寫分離:微信全文搜尋在整體架構上獨立於主業務,搜尋DB也是獨立於主業務DB;當主業務資料發生更新時,主業務通過EventBus方式通知搜尋對應的業務資料處理模組,業務資料處理模組會通過一個獨立的ReadOnly資料庫連線接訪問主業務資料庫,不和主業務儲存層共享資料庫連線。

減少資料庫操作:在搜尋模組中,會有專門處理業務資料的模組,對一些複雜的資料結構做一些特殊的處理。例如對於一個500成員的群聊,如果把500個群成員分次插入搜尋DB當中,會造成過多的資料庫操作。所以,微信會把所有的群成員拼接為單個字串,插入搜尋DB中。

熱資料延遲更新: 針對更新頻率非常高的熱資料,採用延遲更新的策略。所有的索引資料分為正常資料和髒資料。當資料發生更新時,先把對應的資料標記為髒資料,然後有一個定時器,每隔10分鐘,把資料更新到索引中。

3. 可擴充套件性高

高可擴充套件性要求搜尋表結構和業務解耦。SQLite FTS官網上的例子,都是以單索引表的方式,每一列對應業務的某一個屬性,當對應業務發生變化,需要修改索引表的結構。為了解決業務變化而帶來的表結構修改問題,微信把業務屬性數字化,設計如下的表結構:

IndexTable負責全文搜尋的索引建立,它和邏輯無關,當搜尋關鍵詞時,只需要找到對應的DocId即可。MetaTable負責業務邏輯的過濾,通過Type和SubType來過濾對應業務的資料,最後輸出BusItemId。

搜尋優化

微信全文搜尋於2014年1月26日5.4版本上線,到2017年春節後的6.5.7版本,總體使用者量從4億增加到9億,重度使用者數量也大幅度增長,微信本地搜尋的資料量也大幅度增長,造成了搜尋速度不斷下降,使用者投訴不斷增加。我們統計過,從微信5.4版本到6.5.7版本,微信全文搜尋各個任務的平均搜尋時間增長超過10倍,給微信全文搜尋帶來巨大挑戰。

為了優化搜尋時長,先看下搜尋的流程圖:

通過每個階段的耗時,發現在取資料階段,時間佔比達到80%以上,並且搜尋的結果集資料量越大,時間佔比越高,最高可以達到95%。取資料階段是一個迴圈的過程,所以優化一個迴圈需要從兩方面著手,減少單次迴圈耗時和減少總體迴圈次數。

減少單次迴圈執行耗時

深入SQLite FTS4 Extension原始碼,發現FTS4的庫函式Offsets耗時佔單次迴圈執行耗時70%以上,並且資料量越大耗時越長。

FTS4庫函式Offsets:用於把詞語偏移轉為位元組偏移,微信當中使用位元組做結果排序和結果高亮。

函式輸入:

  • Query:使用者查詢的關鍵詞

  • 命中Doc:關鍵詞所命中的文件。文件就是全文搜尋中的基本單位,可以是一個網頁,一篇文章或者是一條聊天記錄

  • 目標詞語偏移:在搜尋階段,通過關鍵詞查詢搜尋索引可以拿到目標詞語偏移

函式輸出:

  • 目標位元組偏移:表示關鍵詞在命中Doc中的位元組偏移。

例如:

Query=我 命中Doc=我和我弟弟去逛街 目標詞語偏移=0、2

把命中Doc經過分詞器分詞,可以得到下表:

最後計算可以得出目標位元組偏移=0、6

下圖是Offsets函式處理命中Doc位元組數和耗時的關係:

Offsets函式的處理過程中包括分詞,所以第一步就優化分詞器。

要優化分詞器,分詞規則是重中之重。微信的分詞規則為英文和數字合併分詞,非英文和數字單獨分詞。舉個例子,如對於暱稱“Hello520中國”,分詞結果為“Hello”、“520”、“中”、“國”。這個分詞規則的原因主要是在微信對全文搜尋的結果排序需求主要是其他的屬性排序,並非依據文件的相關性排序。即,全文搜尋部分只需要找到存在關鍵詞的文件,並不關心文件中存在幾個關鍵詞。而且使用者的輸入Query大部分情況都不能組成詞語,存在方言,所以把整個詞語全部拆開建立索引是符合需求的。

微信全文搜尋最早開發於2013年底,FTS4是SQLite FTS Extension的最高版本,但是FTS4自帶的分詞器不能很好的支援中文,只能使用ICU分詞器,當時ICU分詞器的接入比較簡單,對中文支援較好,所以使用了ICU分詞器。

對於暱稱“Hello520中國”輸出分詞器中,開始是UTF8編碼,分詞器會做一次轉化為Unicode編碼,接著查詢詞典,最後進行後處理得到分詞結果。從輸入輸出中可以發現,轉化編碼和查詢詞典這兩步其實是多餘的,所以微信捨棄ICU分詞器,自定義了Simple分詞器。

Simple分詞直接處理的UTF8編碼的Doc內容,通過單個char,判斷當前字元的Unicode編碼範圍和Unicode編碼長度,根據不同的情況做出不同的處理。

經過分詞器優化後Offsets函式耗時在處理10萬Byte的耗時降低為21ms,但是這樣的優化還不夠,當處理超過10個10W結果Doc時,仍然會超過200ms,所以有了下一步的優化。

在移動端由於螢幕的限制,往往在最後顯示搜尋結果時,只會高亮少量命中的關鍵詞,而Offsets函式會計算命中Doc中所有目標詞語偏移,所以需要對Offsets函式進行改造。

最開始我嘗試的方案是直接修改Offsets函式原始碼,發現FTS4對API的封裝比較難使用,Offsets函式的依賴也比較多,修改出來的程式碼很難維護,可讀性也不好,所以需要尋找新的方法來優化。在一番研究以後,我發現FTS5支援自定義輔助函式,並且有比較好的API的封裝,所以最後使用FTS5自定義輔助函式(MMHighLight)重新實現Offsets函式的功能,並加入優化邏輯。

輸入:Query=我 命中Doc=我和我弟弟去逛街 目標詞語偏移=0、2 目標返回個數=1

分詞器分步回撥,當分詞器第一次返回“我”,符合目標詞語偏移的第一個0,並且此時已經滿足目標返回個數1個,函式直接返回目標位元組偏移=0。

減少總體迴圈次數

減少取資料階段的總體迴圈次數,比較容易想到的就是在SQL層做資料的分頁返回,分頁返回就意味著需要在DB層排序,在DB層排序的決定因素就是排序因子。但是微信全文搜尋面對的業務排序因子多並且複雜,無法直接使用SQL中的ORDER BY,所以需要通過一個中間函式轉化,把所有的排序因子通過一個可比較的數字體現,最後再使用ORDER BY排序。

這裡簡單說下,比較複雜的排序因子如下:

時間分段排序:時間範圍在半年內,排序因子取決於下一級排序因子,時間範圍在半年外,取決於時間的遠近。

函式結果排序:排序因子是一個函式計算的結果,不是一個直接的資料庫Column,並且函式計算結果不可直接使用ORDER BY,例如字串形式的數字。

通過以上的分析,減少總體迴圈次數的核心點就在於,把Java層的排序轉移到SQL層去做,優點如下:

  1. 減少I/O

  2. 減少C層到Java層的資料拷貝

所以這裡關鍵的實現點在於中間轉化函式的實現,微信的中間轉化函式MMRank是通過FTS5的輔助函式實現的。

MMRank的實現原理就是通過把所有的排序因子轉化到一個64位的Long數值當中,高優先順序的排序因子置高位,低優先順序的排序因子置低位。最後的SQL如下:

特殊優化——聊天記錄搜尋優化

微信全文搜尋中有一個比較特殊的搜尋任務,就是聊天記錄。

如圖所示:

圖中的紅色圈內的數字表示,此會話中,包含關鍵字“我”的聊天記錄的個數,而會話的排序規則就是會話的活躍時間。
微信聊天記錄的搜尋有一下兩個特點:

  1. 有統計屬性

  2. 數量非常多(單關鍵詞命中最高可達到20萬條)

從搜尋流程圖中可以看出,微信最初採用的方案是在Java層統計個數和排序,此方法在大資料的情況下不可取。鑑於之前分析過減少迴圈次數可以通過分頁返回,其核心點在於把排序從Java層轉移到SQL層,所以就有了優化方案一。

優化方案一:Group By

實現SQL如下:

此方案通過Group By在SQL層直接統計出命中聊天記錄的個數,並按照最近的時間排序,但是也有明顯的缺陷:

  1. 無法使用索引加速:當GroupBy和OrderBy同時使用是,OrderBy中必須包含GroupBy的欄位才可以命中索引,原因是使用GroupBy會生成中間子表。

  2. 全量計算:GroupBy在SQL層統計命中聊天記錄個數是統計了所有會話,上圖中只需要統計3個會話,浪費了大量資源。

優化方案二:分步計算

鑑於方案一全量計算的問題,採用分步計算的方式。

第一步:找出最近活躍的3個會話

得到會話conv1,conv2,conv3,然後執行以下SQL,可以分別得到三個會話的命中個數

但是這種方法也存在問題,需要執行多條SQL。

優化方案三:MessageCount

鑑於方案二需要多條SQL的問題,可以通過自定義聚合函式實現一次性統計。執行步驟如下:

第一步:找出最近活躍的3個會話

得到會話conv1,conv2,conv3,然後執行以下SQL

可以一次性得到三個會話的命中個數。

最後

經過優化後,微信全文搜尋全體使用者各個任務平均耗時都在50ms以下,而重度使用者各個任務的平均搜尋耗時都在200ms以下,平均時間優化的幅度達到5倍以上。

後續還有很多值得優化的地方,例如,在計算高亮時,如果在DocList的資料結構中,直接加入位元組偏移,那麼還可以節省一部分時間。

最後希望我的分享能夠對大家有些價值,歡迎留言交流。

相關閱讀

微信“ 15。。。。。。。。。”來龍去脈
騰訊的一個應用服務,讓全國簡訊詐騙發案率下降74%…
微信OCR(2):深度序列學習助力文字識別

此文已由作者授權騰訊雲技術社群釋出,轉載請註明文章出處
原文連結:https://cloud.tencent.com/community/article/381004


相關文章