微信全文搜索優化之路

歡迎你們前往 騰訊雲技術社區,獲取更多騰訊海量技術實踐乾貨哦~

做者: 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

相關文章
相關標籤/搜索