本文由微信開發團隊工程師“ qiuwenchen”分享,原題“iOS微信全文搜尋技術優化”,有修訂。
1、引言
全文搜尋是使用倒排索引進行搜尋的一種搜尋方式。倒排索引也稱為反向索引,是指對輸入的內容中的每個Token建立一個索引,索引中儲存了這個Token在內容中的具體位置。全文搜尋技術主要應用在對大量文字內容進行搜尋的場景。
微信終端涉及到大量文字搜尋的業務場景主要包括:im聯絡人、im聊天記錄、收藏的搜尋。
這些功能從2014年上線至今,底層技術已多年沒有更新:
1)聊天記錄使用的全文搜尋引擎還是SQLite FTS3,而現在已經有SQLite FTS5;
2)收藏首頁的搜尋還是使用簡單的Like語句去匹配文字;
3)聯絡人搜尋甚至用的是記憶體搜尋(在記憶體中遍歷所有聯絡人的所有屬性進行匹配)。
隨著使用者在微信上積累的im聊天資料越來越多,提升微信底層搜尋技術的需求也越來越迫切。於是,在2021年我們對微信iOS端的全文搜尋技術進行了一次全面升級,本文主要記錄了本次技術升級過程中的技術實踐。
學習交流:
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架原始碼:https://github.com/JackJiang2...
(本文同步釋出於:http://www.52im.net/thread-38...)
2、系列文章
本文是專題系列文章中的第4篇:
《IM全文檢索技術專題(一):微信移動端的全文檢索優化之路》
《IM全文檢索技術專題(二):微信移動端的全文檢索多音字問題解決方案》
《IM全文檢索技術專題(三):網易雲信Web端IM的聊天訊息全文檢索技術實踐》
《IM全文檢索技術專題(四):微信iOS端的最新全文檢索技術優化實踐》(* 本文)
3、全文檢索引擎的選型
iOS客戶端可以使用的全文搜尋引擎並不多,主要有:
1)SQLite三個版本的FTS元件(FTS3和4、FTS5);
2)Lucene的C++實現版本CLucene;
3) Lucene的C語言橋接版本Lucy。
這裡給出了這些引擎在事務能力、技術風險、搜尋能力、讀寫效能等方面的比較(見下圖)。
1)在事務能力方面:
Lucene沒有提供完整的事務能力,因為Lucene使用了多檔案的儲存結構,它沒有保證事務的原子性。
而SQLite的FTS元件因為底層還是使用普通的表來實現的,可以完美繼承SQLite的事務能力。
2)在技術風險方面:
Lucene主要應用於服務端,在客戶端沒有大規模應用的案例,而且CLucene和Lucy自2013年後官方都停止維護了,技術風險較高。
SQLite的FTS3和FTS4元件則是屬於SQLite的舊版本引擎,官方維護不多了,而且這兩個版本都是將一個詞的索引存到一條記錄中,極端情況下有超出SQLite單條記錄最大長度限制的風險。
SQLite的FTS5元件作為最新版本引擎也已經推出超過六年了,在安卓微信上也已經全量應用,所以技術風險是最低的。
3)在搜尋能力方面:
Lucene的發展歷史比SQLite的FTS元件長很多,搜尋能力相比也是最豐富的。特別是Lucene有豐富的搜尋結果評分排序機制,但這個在微信客戶端沒有應用場景。因為我們的搜尋結果要麼是按照時間排序,要麼是按照一些簡單的自定義規則排序。
在SQLite幾個版本的引擎中,FTS5的搜尋語法更加完備嚴謹,提供了很多介面給使用者自定義搜尋函式,所以搜尋能力也相對強一點。
4)在讀寫效能方面:
下面3個圖是用不同引擎對100萬條長度為10的隨機生成中文語句生成Optimize狀態的索引的效能資料,其中每個語句的漢字出現頻率按照實際的漢字使用頻率。
從上面的3張圖可以看到:Lucene讀取命中數量的效能比SQLite好很多,說明Lucene索引的檔案格式很有優勢,但是微信沒有隻讀取命中數量的應用場景。Lucene的其他效能資料跟SQLite的差距不明顯。SQLite FTS3和FTS5的大部分效能很接近,FTS5索引的生成耗時比FTS3高一截,這個有優化方法。
綜合考慮這些因素:我們選擇SQLite FTS5作為iOS微信全文搜尋的搜尋引擎。
4、引擎層優化1:實現FTS5的Segment自動Merge機制
SQLite FTS5會把每個事務寫入的內容儲存成一個獨立的b樹,稱為一個segment,segment中儲存了本次寫入內容中的每個詞在本次內容中行號(rowid)、列號和欄位中的每次出現的位置偏移,所以這個segment就是該內容的倒排索引。
多次寫入就會形成多個segment,查詢時就需要分別查詢這些segment再彙總結果,從而segment數量越多,查詢速度越慢。
為了減少segment的數量,SQLite FTS5引入了merge機制。新寫入的segment的level為0,merge操作可以把level為i的現有segment合併成一個level為i+1的新的segment。
merge的示例如下:
FTS5預設的merge操作有兩種:
1)automerge:某一個level的segment達到4時就開始在寫入內容時自動執行一部分merge操作,稱為一次automerge。每次automerge的寫入量跟本次更新的寫入量成正比,需要多次automerge才能完整合併成一個新segment。Automerge在完整生成一個新的segment前,需要多次裁剪舊的segment的已合併內容,引入多餘的寫入量;
2)crisismerge:本次寫入後某一個level的segment數量達到16時,一次性合併這個level的segment,稱為crisismerge。
FTS5的預設merge操作都是在寫入時同步執行的,會對業務邏輯造成效能影響,特別是crisismerge會偶然導致某一次寫入操作特別久,這會讓業務效能不可控(之前的測試中FTS5的建索引耗時較久,也主要因為FTS5的merge操作比其他兩種引擎更加耗時)。
我們在WCDB中實現FTS5的segment自動merge機制,將這些merge操作集中到一個單獨子執行緒執行,並且優化執行引數。
具體做法如下:
1)監聽有FTS5索引的資料庫每個事務變更到的FTS5索引表,拋通知到子執行緒觸發WCDB的自動merge操作;
2)Merge執行緒檢查所有FTS5索引表中segment數超過 1 的level執行一次merge;
3)Merge時每寫入16頁資料檢查一次有沒有其他執行緒的寫入操作因為merge操作阻塞,如果有就立即commit,儘量減小merge對業務效能的影響。
自動merge邏輯執行的流程圖如下:
限制每個level的segment數量為1,可以讓FTS5的查詢效能最接近optimize(所有segment合併成一個)之後的效能,而且引入的寫入量是可接受的。假設業務每次寫入量為M,寫入了N次,那麼在merge執行完整之後,資料庫實際寫入量為MN(log2(N)+1)。業務批量寫入,提高M也可以減小總寫入量。
效能方面,對一個包含100w條中文內容,每條長度100漢字的fts5的表查詢三個詞,optimize狀態下耗時2.9ms,分別限制每個level的segment數量為2、3、4時的查詢耗時分別為4.7ms、8.9ms、15ms。100w條內容每次寫入100條的情況下,按照WCDB的方案執行merge的耗時在10s內。
使用自動Merge機制,可以在不影響索引更新效能的情況下,將FTS5索引保持在最接近Optimize的狀態,提高了搜尋速度。
5、引擎層優化2:分詞器優化
5.1 分詞器效能優化
分詞器是全文搜尋的關鍵模組,它實現將輸入內容拆分成多個Token並提供這些Token的位置,搜尋引擎再對這些Token建立索引。SQLite的FTS元件支援自定義分詞器,可以按照業務需求實現自己的分詞器。
分詞器的分詞方法可以分為按字分詞和按詞分詞。前者只是簡單對輸入內容逐字建立索引,後者則需要理解輸入內容的語義,對有具體含義的片語建立索引。相比於按字分詞,按詞分詞的優勢是既可以減少建索引的Token數量,也可以減少搜尋時匹配的Token數量,劣勢是需要理解語義,而且使用者輸入的詞不完整時也會有搜不到的問題。
為了簡化客戶端邏輯和避免使用者漏輸內容時搜不到的問題,iOS微信之前的FTS3分詞器OneOrBinaryTokenizer是採用了一種巧妙的按字分詞演算法,除了對輸入內容逐字建索引,還會對內容中每兩個連續的字建索引,對於搜尋內容則是按照每兩個字進行分詞。
下面是用“北京歡迎你”去搜尋相同內容的分詞例子:
相比於簡單的按字分詞,這種分詞方式的優勢是可以將搜尋時匹配的Token數量接近降低一半,提高搜尋速度,而且在一定程度上可以提升搜尋精度(比如搜尋“歡迎你北京”就匹配不到“北京歡迎你”)。這種分詞方式的劣勢就是儲存的索引內容很多,基本輸入內容的每個字都在索引中儲存了三次,是一種用空間換時間的做法。
因為OneOrBinaryTokenizer用接近三倍的索引內容增長才換取不到兩倍的搜尋效能提升,不是很划算,所以我們在FTS5上重新開發了一種新的分詞器VerbatimTokenizer,這個分詞器只採用基本的按字分詞,不儲存冗餘索引內容。同時在搜尋時,每兩個字用引號引起來組成一個Phrase,按照FTS5的搜尋語法,搜尋時Phrase中的字要按順序相鄰出現的內容才會命中,實現了跟OneOrBinaryTokenizer一樣的搜尋精度。
VerbatimTokenizer的分詞規則示意圖如下:
5.2 分詞器能力擴充套件
VerbatimTokenizer還根據微信實際的業務需求實現了五種擴充套件能力來提高搜尋的容錯能力:
1)支援在分詞時將繁體字轉換成簡體字:這樣使用者可以用繁體字搜到簡體字內容,用簡體字也能搜到繁體字內容,避免了因為漢字的簡體和繁體字形相近導致使用者輸錯的問題。
2)支援Unicode歸一化:Unicode支援相同字形的字元用不同的編碼來表示,比如編碼為\ue9的é和編碼為\u65\u301的é有相同的字形,這會導致使用者用看上去一樣的內容去搜尋結果搜不到的問題。Unicode歸一化就是把字形相同的字元用同一個編碼表示。
3)支援過濾符號:大部分情況下,我們不需要支援對符號建索引,符號的重複量大而且使用者一般也不會用符號去搜尋內容,但是聯絡人搜尋這個業務場景需要支援符號搜尋,因為使用者的暱稱裡面經常出現顏文字,符號的使用量不低。
4)支援用Porter Stemming演算法對英文單詞取詞幹:取詞幹的好處是允許使用者搜尋內容的單複數和時態跟命中內容不一致,讓使用者更容易搜到內容。但是取詞幹也有弊端,比如使用者要搜尋的內容是“happyday”,輸入“happy”作為字首去搜尋卻會搜不到,因為“happyday”取詞幹變成“happydai”,“happy”取詞幹變成“happi”,後者就不能成為前者的字首。這種badcase在內容為多個英文單詞拼接一起時容易出現,聯絡人暱稱的拼接英文很常見,所以在聯絡人的索引中沒有取詞幹,在其他業務場景中都用上了。
5)支援將字母全部轉成小寫:這樣使用者可以用小寫搜到大寫,反之亦然。
這些擴充套件能力都是對建索引內容和搜尋內容中的每個字做變換,這個變換其實也可以在業務層做,其中的Unicode歸一化和簡繁轉換以前就是在業務層實現的。
但是這樣做有兩個弊端:
1)一個是業務層每做一個轉換都需要對內容做一次遍歷,引入冗餘計算量;
2)一個是寫入到索引中的內容是轉變後的內容,那麼搜尋出來的結果也是轉變後的,會和原文不一致,業務層做內容判斷的時候容易出錯。
鑑於這兩個原因,VerbatimTokenizer將這些轉變能力都集中到了分詞器中實現。
6、引擎層優化3:索引內容支援多級分隔符
SQLite的FTS索引表不支援在建表後再新增新列,但是隨著業務的發展,業務資料支援搜尋的屬性會變多,如何解決新屬性的搜尋問題呢?
特別是在聯絡人搜尋這個業務場景,一個聯絡人支援搜尋的欄位非常多。
一個直接的想法是:將新屬性和舊屬性用分隔符拼接到一起建索引。
但這樣會引入新的問題:FTS5是以整個欄位的內容作為整體去匹配的,如果使用者搜尋匹配的Token在不同的屬性,那這條資料也會命中,這個結果顯然不是使用者想要的,搜尋結果的精確度就降低了。
我們需要搜尋匹配的Token中間不存在分隔符,那這樣可以確保匹配的Token都在一個屬性內。同時,為了支援業務靈活擴充套件,還需要支援多級分隔符,而且搜尋結果中還要支援獲取匹配結果的層級、位置以及該段內容的原文和匹配詞。
這個能力FTS5還沒有,而FTS5的自定義輔助函式支援在搜尋時獲取到所有命中結果中每個命中Token的位置,利用這個資訊可以推斷出這些Token中間有沒有分隔符,以及這些Token所在的層級,所以我們開發了SubstringMatchInfo這個新的FTS5搜尋輔助函式來實現這個能力。
這個函式的大致執行流程如下:
7、應用層優化1:資料庫表格式優化
7.1 非文字搜尋內容的儲存方式
在實際應用中,我們除了要在資料庫中儲存需要搜尋的文字的FTS索引,還需要額外儲存這個文字對應的業務資料的id、用於結果排序的的屬性(常見的是業務資料的建立時間)以及其他需要直接跟隨搜尋結果讀出的內容,這些都是不參與文字搜尋的內容。
根據非文字搜尋內容的不同儲存位置,我們可以將FTS索引表的表格式分成兩種:
1)第一種方式:是將非文字搜尋內容儲存在額外的普通表中,這個表儲存FTS索引的Rowid和非文字搜尋內容的對映關係,而FTS索引表的每一行只儲存可搜尋的文字內容。
這個表格式類似於這樣:
這種表格式的優勢和劣勢是很明顯,分別是:
a)優勢是:FTS索引表的內容很簡單,不熟悉FTS索引表配置的同學不容易出錯,而且普通表的可擴充套件性好,支援新增新列;
b)劣勢是:搜尋時需要先用FTS索引的Rowid讀取到普通表的Rowid,這樣才能讀取到普通表的其他內容,搜尋速度慢一點,而且搜尋時需要聯表查詢,搜尋SQL語句稍微複雜一點。
2)第二種方式:是將非文字搜尋內容直接和可搜尋文字內容一起儲存在FTS索引表中。
表格式類似於這樣:
這種方式的優劣勢跟前一種方式恰好相反:
a)優勢是:搜尋速度快而且搜尋方式簡單;
b)劣勢是:擴充套件性差且需要更細緻的配置。
因為iOS微信以前是使用第二種表格式,而且微信的搜尋業務已經穩定不會有大變化,我們現在更加追求搜尋速度,所以我們還是繼續使用第二種表格式來儲存全文搜尋的資料。
7.2 避免冗餘索引內容
FTS索引表預設對錶中的每一列的內容都建倒排索引,即便是數字內容也會按照文字來處理,這樣會導致我們儲存在FTS索引表中的非文字搜尋內容也建了索引,進而增大索引檔案的大小、索引更新的耗時和搜尋的耗時,這顯然不是我們想要的。
FTS5支援給索引表中的列新增UNINDEXED約束,這樣FTS5就不會對這個列建索引了,所以給可搜尋文字內容之外的所有列新增這個約束就可以避免冗餘索引。
7.3 降低索引內容的大小
前面提到,倒排索引主要儲存文字中每個Token對應的行號(rowid)、列號和欄位中的每次出現的位置偏移,其中的行號是SQLite自動分配的,位置偏移是根據業務的實際內容,這兩個我們都決定不了,但是列號是可以調整的。
在FTS5索引中,一個Token在一行中的索引內容的格式是這樣的:
從中可以看出,如果我們把可搜尋文字內容設定在第一列的話(多個可搜尋文字列的話,把內容多的列放到第一列),就可以少儲存列分割符0x01和列號,這樣可以明顯降低索引檔案大小。
所以我們最終的表格式是這樣:
7.4 優化前後的效果對比
下面是iOS微信優化前後的平均每個使用者的索引檔案大小對比:
8、應用層優化2:索引更新邏輯優化
8.1 概述
為了將全文搜尋邏輯和業務邏輯解耦,iOS微信的FTS索引是不儲存在各個業務的資料庫中的,而是集中儲存到一個專用的全文搜尋資料庫,各個業務的資料有更新之後再非同步通知全文搜尋模組更新索引。
整體流程如下:
這樣做既可以避免索引更新拖慢業務資料更新的速度,也能避免索引資料更新出錯甚至索引資料損壞對業務造成影響,讓全文搜尋功能模組能夠充分獨立。
8.2 保證索引和資料的一致
業務資料和索引資料分離且非同步同步的好處很多,但實現起來也很難。
最難的問題是如何保證業務資料和索引資料的一致,也即要保證業務資料和索引資料要逐條對應,不多不少。
曾經iOS微信在這裡踩了很多坑,打了很多補丁都不能完整解決這個問題,我們需要一個更加體系化的方法來解決這個問題。
為了簡化問題,我們可以把一致性問題可以拆成兩個方面分別處理:
1)一是保證所有業務資料都有索引,這個使用者的搜尋結果就不會有缺漏;
2)二是保證所有索引都對應一個有效的業務資料,這樣使用者就不會搜到無效的結果。
要保證所有業務資料都有索引,首先要找到或者構造一種一直增長的資料來描述業務資料更新的進度,這個進度資料的更新和業務資料的更新能保證原子性。而且根據這個進度的區間能拿出業務資料更新的內容,這樣我們就可以依賴這個進度來更新索引。
在微信的業務中,不同業務的進度資料不同:
1)聊天記錄是使用訊息的rowid;
2)收藏是使用收藏跟後臺同步的updateSequence;
3)聯絡人找不到這種一直增長的進度資料(我們是通過在聯絡人資料庫中標記有新增或有更新的聯絡人的微訊號來作為索引更新進度)。
針對上述第3)點,進度資料的使用方法如下:
無論業務資料是否儲存成功、更新通知是否到達全文搜尋模組、索引資料是否儲存成功,這套索引更新邏輯都能保證儲存成功的業務資料都能成功建到索引。
這其中的一個關鍵點是資料和進度要在同個事務中一起更新,而且要儲存在同個資料庫中,這樣才能保證資料和進度的更新的原子性(WCDB建立的資料庫因為使用WAL模式而無法保證不同資料庫的事務的原子性)。
還有一個操作圖中沒有畫出,具體是微信啟動時如果檢查到業務進度小於索引進度,這種一般意味著業務資料損壞後被重置了,這種情況下要刪掉索引並重置索引進度。
對於每個索引都對應有效的業務資料,這就要求業務資料刪除之後索引也要必須刪掉。現在業務資料的刪除和索引的刪除是非同步的,會出現業務資料刪掉之後索引沒刪除的情況。
這種情況會導致兩個問題:
1)一是冗餘索引會導致搜尋速度變慢,但這個問題出現概率很小,這個影響可以忽略不計;
2)二是會導致使用者搜到無效資料,這個是要避免的。
針對上述第2)點:因為要完全刪掉所有無效索引成本比較高,所以我們採用了惰性檢查的方法來解決這個問題,具體做法是搜尋結果要顯示給使用者時,才檢查這個資料是否有效,無效的話不顯示這個搜尋結果並非同步刪除對應的索引。因為使用者一屏能看到的資料很少,所以檢查邏輯帶來的效能消耗也可以忽略不計。而且這個檢查操作實際上也不算是額外加的邏輯,為了搜尋結果展示內容的靈活性,我們也要在展示搜尋結果時讀出業務資料,這樣也就順帶做了資料有效性的檢查。
8.3 建索引速度優化
索引只有在搜尋的時候才會用到,它的更新優先順序並沒有業務資料那麼高,可以儘量攢更多的業務資料才去批量建索引。
批量建索引有以下三個好處:
1)減少磁碟的寫入次數,提高平均建索引速度;
2)在一個事務中,建索引SQL語句的解析結果可以反覆使用,可以減少SQL語句的解析次數,進而提高平均建索引速度;
3)減少生成Segment的數量,從而減少Merge Segment帶來的讀寫消耗。
當然:也不能保留太多業務資料不建索引,這樣使用者要搜尋時會來不及建索引,從而導致搜尋結果不完整。
有了前面的Segment自動Merge機制,索引的寫入速度非常可控,只要控制好量,就不用擔心批量建索引帶來的高耗時問題。
我們綜合考慮了低端機器的建索引速度和搜尋頁面的拉起時間,確定了最大批量建索引資料條數為100條。
同時:我們會在記憶體中cache本次微信執行期間產生的未建索引業務資料,在極端情況下給沒有來得及建索引的業務資料提供相對記憶體搜尋,保證搜尋結果的完整性。因為cache上一次微信執行期間產生的未建索引資料需要引入額外的磁碟IO,所以微信啟動後會觸發一次建索引邏輯,對現有的未建索引業務資料建一次索引。
總結一下觸發建索引的時機有三個:
1)未建索引業務資料達到100條;
2)進入搜尋介面;
3)微信啟動。
8.4 刪除索引速度優化
索引的刪除速度經常是設計索引更新機制時比較容易忽視的因素,因為被刪除的業務資料量容易被低估,會被誤以為是低概率場景。
但實際被使用者刪除的業務資料可能會達到50%,是個不可忽視的主場景。而且SQLite是不支援並行寫入的,刪除索引的效能也會間接影響到索引的寫入速度,會為索引更新引入不可控因素。
因為刪除索引的時候是拿著業務資料的id去刪除的。
所以提高刪除索引速度的方式有兩種:
1)建一個業務資料id到FTS索引的rowid的普通索引;
2)在FTS索引表中去掉業務資料Id那一列的UNINDEXED約束,給業務資料Id新增倒排索引。
這裡倒排索引其實沒有普通索引那麼高效,有兩個原因:
1)倒排索引相比普通索引還帶了很多額外資訊,搜尋效率低一些;
2)如果需要多個業務欄位才能確定一條倒排索引時,倒排索引是建不了聯合索引的,只能匹配其中一個業務欄位,其他欄位就是遍歷匹配,這種情況搜尋效率會很低。
8.5 優化前後的效果對比
聊天記錄的優化前後索引效能資料如下:
收藏的優化前後索引效能資料如下:
9、應用層優化3:搜尋邏輯優化
9.1 問題
使用者在iOS微信的首頁搜尋內容時,互動邏輯如下:
如上圖所示:當使用者變更搜尋框的內容之後,會並行發起所有業務的搜尋任務,各個搜尋任務執行完之後才再將搜尋結果返回到主執行緒給頁面展示。這個邏輯會隨著使用者變更搜尋內容而繼續重複。
9.2 單個搜尋任務應支援並行執行
雖然現在不同搜尋任務已經支援並行執行,但是不同業務的資料量和搜尋邏輯差別很大,資料量大或者搜尋邏輯複雜的任務耗時會很久,這樣還不能充分發揮手機的並行處理能力。
我們還可以將並行處理能力引入單個搜尋任務內,這裡有兩種處理方式:
1)對於搜尋資料量大的業務(比如聊天記錄搜尋):可以將索引資料均分儲存到多個FTS索引表(注意這裡不均分的話還是會存在短板效應),這樣搜尋時可以並行搜尋各個索引表,然後彙總各個表的搜尋結果,再進行統一排序。這裡拆分的索引表數量既不能太多也不能太少,太多會超出手機實際的並行處理能力,也會影響其他搜尋任務的效能,太少又不能充分利用並行處理能力。以前微信用了十個FTS表儲存聊天記錄索引,現在改為使用四個FTS表。
2)對於搜尋邏輯複雜的業務(比如聯絡人搜尋):可以將可獨立執行的搜尋邏輯並行執行(比如:在聯絡人搜尋任務中,我們將聯絡人的普通文字搜尋、拼音搜尋、標籤和地區的搜尋、多群成員的搜尋並行執行,搜完之後再合併結果進行排序)。這裡為什麼不也用拆表的方式呢?因為這種搜尋結果數量少的場景,搜尋的耗時主要是集中在搜尋索引的環節,索引可以看做一顆B樹,將一顆B樹拆分成多個,搜尋耗時並不會成比例下降。
9.3 搜尋任務應支援中斷
使用者在搜尋框持續輸入內容的過程中可能會自動多次發起搜尋任務,如果在前一次發起的搜尋任務還沒執行完時,就再次發起搜尋任務,那前後兩次搜尋任務就會互相影響對方效能。
這種情況在使用者輸入內容從短到長的過程中還挺容易出現的,因為搜尋文字短的時候命中結果就很多,搜尋任務也就更加耗時,從而更有機會撞上後面的搜尋任務。太多工同時執行還會容易引起手機發燙、爆記憶體的問題。
所以我們需要讓搜尋任務支援隨時中斷,這樣就可以在後一次搜尋任務發起的時候,能夠中斷前一次的搜尋任務,避免任務量過多的問題。
搜尋任務支援中斷的實現方式是給每個搜尋任務設定一個CancelFlag,在搜尋邏輯執行時每搜到一個結果就判斷一下CancelFlag是否置位,如果置位了就立即退出任務。外部邏輯可以通過置位CancelFlag來中斷搜尋任務。
邏輯流程如下圖所示:
為了讓搜尋任務能夠及時中斷,我們需要讓檢查CancelFlag的時間間隔儘量相等,要實現這個目標就要在搜尋時避免使用OrderBy子句對結果進行排序。
因為FTS5不支援建立聯合索引,所以在使用OrderBy子句時,SQLite在輸出第一個結果前會遍歷所有匹配結果進行排序,這就讓輸出第一個結果的耗時幾乎等於輸出全部結果的耗時,中斷邏輯就失去了意義。
不使用OrderBy子句就對搜尋邏輯新增了兩個限制:
1)從資料庫讀取所有結果之後再排序:我們可以在讀取結果時將用於排序的欄位一併讀出,然後在讀完所有結果之後再對所有結果執行排序。因為排序的耗時佔總搜尋耗時的比例很低,加上排序演算法的效能大同小異,這種做法對搜尋速度的影響可以忽略。
2)不能使用分段查詢:在全文搜尋這個場景中,分段查詢其實是沒有什麼作用的。因為分段查詢就要對結果排序,對結果排序就要遍歷所有結果,所以分段查詢並不能降低搜尋耗時(除非按照FTS索引的Rowid分段查詢,但是Rowid不包含實際的業務資訊)。
9.4 搜尋讀取內容應最少化
搜尋時讀取內容的量也是決定搜尋耗時的一個關鍵因素。
FTS索引表實際是有多個SQLite普通表組成的,這其中一些表格儲存實際的倒排索引內容,還有一個表格儲存使用者儲存到FTS索引表的全部原文。當搜尋時讀取Rowid以外的內容時,就需要用Rowid到儲存原文的表的讀取內容。
索引表輸出結果的內部執行過程如下:
所以讀取內容越少輸出結果的速度越快,而且讀取內容過多也會有消耗記憶體的隱患。
我們採用的方式是:搜尋時只讀取業務資料id和用於排序的業務屬性,排好序之後,在需要給使用者展示結果時,才用業務資料id按需讀取業務資料具體內容出來展示。這樣做的擴充套件性也會很好,可以在不更改儲存內容的情況下,根據各個業務的需求不斷調整搜尋結果展示的內容。
還有個地方要特別提一下:就是搜尋時儘量不要讀取高亮資訊(SQLite的highlight函式有這個能力)。因為要獲取高亮欄位不僅要將文字的原文讀取出來,還要對文字原文再次分詞,才能定位命中位置的原文內容,搜尋結果多的情況下分詞帶來的消耗非常明顯。
那展示搜尋結果時如何獲取高亮匹配內容呢?我們採用的方式是將使用者的搜尋文字進行分詞,然後在展示結果時查詢每個Token在展示文字中的位置,然後將那個位置高亮顯示(同樣因為使用者一屏看到的結果數量是很少的,這裡的高亮邏輯帶來的效能消耗可以忽略)。
當然在搜尋規則很複雜的情況下,直接讀取高亮資訊是比較方便(比如:聯絡人搜尋就使用前面提到的SubstringMatchInfo函式來讀取高亮內容)。這裡主要還是因為要讀取匹配內容所在的層級和位置用於排序,所以逐個結果重新分詞的操作在所難免。
9.5 優化前後的效果對比
下面是微信各搜尋業務優化前後的搜尋耗時對比:
10、本文小結
目前iOS微信已經將這套新全文搜尋技術方案全量應用到聊天記錄、聯絡人和收藏的搜尋業務中。
使用新方案之後:全文搜尋的索引檔案佔用空間更小、索引更新耗時更少、搜尋速度也更快了,可以說全文搜尋的效能得到了全方位提升。
附錄:QQ、微信團隊技術文章彙總
《微信朋友圈千億訪問量背後的技術挑戰和實踐總結》
《騰訊技術分享:Android版手機QQ的快取監控與優化實踐》
《微信團隊分享:iOS版微信的高效能通用key-value元件技術實踐》
《微信團隊分享:iOS版微信是如何防止特殊字元導致的炸群、APP崩潰的?》
《騰訊技術分享:Android手Q的執行緒死鎖監控系統技術實踐》
《iOS後臺喚醒實戰:微信收款到賬語音提醒技術總結》
《微信團隊分享:微信每日億次實時音視訊聊天背後的技術解密》
《騰訊團隊分享 :一次手Q聊天介面中圖片顯示bug的追蹤過程分享》
《微信團隊分享:微信Android版小視訊編碼填過的那些坑》
《企業微信客戶端中組織架構資料的同步更新方案優化實戰》
《微信團隊披露:微信介面卡死超級bug“15。。。。”的來龍去脈》
《QQ 18年:解密8億月活的QQ後臺服務介面隔離技術》
《月活8.89億的超級IM微信是如何進行Android端相容測試的》
《微信後臺基於時間序的海量資料冷熱分級架構設計實踐》
《微信團隊原創分享:Android版微信的臃腫之困與模組化實踐之路》
《微信後臺團隊:微信後臺非同步訊息佇列的優化升級實踐分享》
《微信團隊原創分享:微信客戶端SQLite資料庫損壞修復實踐》
《騰訊原創分享(一):如何大幅提升行動網路下手機QQ的圖片傳輸速度和成功率》
《微信新一代通訊安全解決方案:基於TLS1.3的MMTLS詳解》
《微信團隊原創分享:Android版微信後臺保活實戰分享(網路保活篇)》
《微信技術總監談架構:微信之道——大道至簡(演講全文)》
《微信海量使用者背後的後臺系統儲存架構(視訊+PPT) [附件下載]》
《微信非同步化改造實踐:8億月活、單機千萬連線背後的後臺解決方案》
《微信朋友圈海量技術之道PPT [附件下載]》
《微信對網路影響的技術試驗及分析(論文全文)》
《架構之道:3個程式設計師成就微信朋友圈日均10億釋出量[有視訊]》
《快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)》
《微信團隊原創分享:Android記憶體洩漏監控和優化技巧總結》
《Android版微信安裝包“減肥”實戰記錄》
《iOS版微信安裝包“減肥”實戰記錄》
《移動端IM實踐:iOS版微信介面卡頓監測方案》
《微信“紅包照片”背後的技術難題》
《移動端IM實踐:iOS版微信小視訊功能技術方案實錄》
《移動端IM實踐:Android版微信如何大幅提升互動效能(一)》
《移動端IM實踐:實現Android版微信的智慧心跳機制》
《移動端IM實踐:谷歌訊息推送服務(GCM)研究(來自微信)》
《移動端IM實踐:iOS版微信的多裝置字型適配方案探討》
《IPv6技術詳解:基本概念、應用現狀、技術實踐(上篇)》
《手把手教你讀取Android版微信和手Q的聊天記錄(僅作技術研究學習)》
《微信技術分享:微信的海量IM聊天訊息序列號生成實踐(演算法原理篇)》
《社交軟體紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等》
《社交軟體紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進》
《社交軟體紅包技術解密(三):微信搖一搖紅包雨背後的技術細節》
《社交軟體紅包技術解密(四):微信紅包系統是如何應對高併發的》
《社交軟體紅包技術解密(五):微信紅包系統是如何實現高可用性的》
《社交軟體紅包技術解密(六):微信紅包系統的儲存層架構演進實踐》
《社交軟體紅包技術解密(十一):解密微信紅包隨機演算法(含程式碼實現)》
《IM開發寶典:史上最全,微信各種功能引數和邏輯規則資料彙總》
《微信團隊分享:微信直播聊天室單房間1500萬線上的訊息架構演進之路》
《企業微信的IM架構設計揭祕:訊息模型、萬人群、已讀回執、訊息撤回等》
(本文同步釋出於:http://www.52im.net/thread-38...)