一、iOS微信全文搜索技術(shù)的現(xiàn)狀
全文搜索是使用倒排索引進(jìn)行搜索的一種搜索方式。倒排索引也稱為反向索引,是指對輸入的內(nèi)容中的每個(gè)Token
建立一個(gè)索引,索引中保存了這個(gè)Token
在內(nèi)容中的具體位置。全文搜索技術(shù)主要應(yīng)用在對大量文本內(nèi)容進(jìn)行搜索的場景。
微信終端涉及到大量文本搜索的業(yè)務(wù)場景主要包括聯(lián)系人、聊天記錄、收藏的搜索。這些搜索功能從2014年上線至今,已經(jīng)多年沒有更新底層搜索技術(shù),聊天記錄使用的全文搜索引擎還是SQLite FTS3,而現(xiàn)在已經(jīng)有SQLite FTS5,收藏首頁的搜索還是使用簡單的Like
語句去匹配文本,聯(lián)系人搜索甚至用的是內(nèi)存搜索(在內(nèi)存中遍歷所有聯(lián)系人的所有屬性進(jìn)行匹配)。隨著用戶在微信上積累的數(shù)據(jù)越來越多,提升微信底層搜索技術(shù)的需求也越來越迫切。在2021年,我們對iOS微信的全文搜索技術(shù)進(jìn)行了一次全面升級,本文主要介紹本次技術(shù)升級的工作經(jīng)驗(yàn)。
二、全文搜索引擎的選型與優(yōu)化
1、搜索引擎選型
iOS客戶端可以使用的全文搜索引擎并不多,主要有SQLite三個(gè)版本的FTS組件、Lucene的C++實(shí)現(xiàn)版本CLucene和C語言橋接版本Lucy。這里給出了這些引擎在事務(wù)能力、技術(shù)風(fēng)險(xiǎn)、搜索能力、讀寫性能等方面的比較。
在事務(wù)能力方面,Lucene沒有提供完整的事務(wù)能力,因?yàn)長ucene使用了多文件的存儲(chǔ)結(jié)構(gòu),它沒有保證事務(wù)的原子性。SQLite的FTS組件因?yàn)榈讓舆€是使用普通的表來實(shí)現(xiàn)的,可以完美繼承SQLite的事務(wù)能力。
在技術(shù)風(fēng)險(xiǎn)方面,Lucene主要應(yīng)用于服務(wù)端,在客戶端沒有大規(guī)模應(yīng)用的案例,而且CLucene和Lucy自2013年后官方都停止維護(hù)了,技術(shù)風(fēng)險(xiǎn)較高。SQLite的FTS3和FTS4組件則是屬于SQLite的舊版本引擎,官方維護(hù)不多了,而且這兩個(gè)版本都是將一個(gè)詞的索引存到一條記錄中,極端情況下有超出SQLite單條記錄最大長度限制的風(fēng)險(xiǎn)。SQLite的FTS5組件作為最新版本引擎也已經(jīng)推出超過六年了,在安卓微信上也已經(jīng)全量應(yīng)用,所以技術(shù)風(fēng)險(xiǎn)是最低的。
在搜索能力方面,Lucene的發(fā)展歷史比SQLite的FTS組件長很多,搜索能力相比也是最豐富的。特別是Lucene有豐富的搜索結(jié)果評分排序機(jī)制,但這個(gè)在微信客戶端沒有應(yīng)用場景。因?yàn)槲覀兊乃阉鹘Y(jié)果要么是按照時(shí)間排序,要么是按照一些簡單的自定義規(guī)則排序。在SQLite幾個(gè)版本的引擎中,F(xiàn)TS5的搜索語法更加完備嚴(yán)謹(jǐn),提供了很多接口給用戶自定義搜索函數(shù),所以搜索能力也相對強(qiáng)一點(diǎn)。
在讀寫性能方面,下面是用不同引擎對100萬條長度為10的隨機(jī)生成中文語句生成Optimize狀態(tài)的索引的性能數(shù)據(jù),其中每個(gè)語句的漢字出現(xiàn)頻率按照實(shí)際的漢字使用頻率:
可以看到,Lucene讀取命中數(shù)量的性能比SQLite好很多,說明Lucene索引的文件格式很有優(yōu)勢,但是微信沒有只讀取命中數(shù)量的應(yīng)用場景,Lucene的其他性能數(shù)據(jù)跟SQLite的差距不明顯。SQLite FTS3和FTS5的大部分性能很接近,F(xiàn)TS5索引的生成耗時(shí)比FTS3高一截,這個(gè)有優(yōu)化方法。
綜合考慮這些因素,我們選擇SQLite FTS5作為iOS微信全文搜索的搜索引擎。
資料領(lǐng)取直通車:大廠面試題錦集+視頻教程https://docs.qq.com/doc/DTlhVekRrZUdDUEpy
Linux服務(wù)器學(xué)習(xí)網(wǎng)站:C/C++Linux服務(wù)器開發(fā)/后臺(tái)架構(gòu)師https://ke.qq.com/course/417774?flowToken=1028592
?
2、實(shí)現(xiàn)FTS5的Segment自動(dòng)Merge機(jī)制
SQLite FTS5會(huì)把每個(gè)事務(wù)寫入的內(nèi)容保存成一個(gè)獨(dú)立的b樹,稱為一個(gè)segment
,segment
中保存了本次寫入內(nèi)容中的每個(gè)詞在本次內(nèi)容中行號(rowid
)、列號和字段中的每次出現(xiàn)的位置偏移,所以這個(gè)segment
就是該內(nèi)容的倒排索引。多次寫入就會(huì)形成多個(gè)segment
,查詢時(shí)就需要分別查詢這些segment
再匯總結(jié)果,從而segment
數(shù)量越多,查詢速度越慢。
為了減少segment
的數(shù)量,SQLite FTS5引入了merge
機(jī)制。新寫入的segment
的level
為0,merge
操作可以把level
為i
的現(xiàn)有segment
合并成一個(gè)level
為i+1
的新的segment
。merge
的示例如下:
FTS5默認(rèn)的merge
操作有兩種:
- 某一個(gè)
level
的segment
達(dá)到4
時(shí)就開始在寫入內(nèi)容時(shí)自動(dòng)執(zhí)行一部分merge
操作,稱為一次automerge
。每次automerge
的寫入量跟本次更新的寫入量成正比,需要多次automerge
才能完整合并成一個(gè)新segment
。Automerge
在完整生成一個(gè)新的segment
前,需要多次裁剪舊的segment
的已合并內(nèi)容,引入多余的寫入量。 - 本次寫入后某一個(gè)
level
的segment
數(shù)量達(dá)到16時(shí),一次性合并這個(gè)level
的segment
,稱為crisismerge
。
FTS5的默認(rèn)merge
操作都是在寫入時(shí)同步執(zhí)行的,會(huì)對業(yè)務(wù)邏輯造成性能影響,特別是crisismerge
會(huì)偶然導(dǎo)致某一次寫入操作特別久,這會(huì)讓業(yè)務(wù)性能不可控。之前的測試中FTS5的建索引耗時(shí)較久,也主要因?yàn)镕TS5的merge
操作比其他兩種引擎更加耗時(shí)。
我們在WCDB中實(shí)現(xiàn)FTS5的segment
自動(dòng)merge
機(jī)制,將這些merge
操作集中到一個(gè)單獨(dú)子線程執(zhí)行,并且優(yōu)化執(zhí)行參數(shù),具體如下:
- 監(jiān)聽有FTS5索引的數(shù)據(jù)庫每個(gè)事務(wù)變更到的FTS5索引表,拋通知到子線程觸發(fā)WCDB的自動(dòng)
merge
操作。 - Merge線程檢查所有FTS5索引表中
segment
數(shù)超過?1
?的level執(zhí)行一次merge
。 -
Merge
時(shí)每寫入16
頁數(shù)據(jù)檢查一次有沒有其他線程的寫入操作因?yàn)?code>merge操作阻塞,如果有就立即commit
,盡量減小merge
對業(yè)務(wù)性能的影響。
自動(dòng)merge
邏輯執(zhí)行的流程圖如下:
限制每個(gè)level
的segment
數(shù)量為1
,可以讓FTS5的查詢性能最接近optimize
(所有segment
合并成一個(gè))之后的性能,而且引入的寫入量是可接受的。假設(shè)業(yè)務(wù)每次寫入量為M
,寫入了N
次,那么在merge執(zhí)行完整之后,數(shù)據(jù)庫實(shí)際寫入量為**MN(log2(N)+1)**。業(yè)務(wù)批量寫入,提高M
也可以減小總寫入量。
性能方面,對一個(gè)包含100w條中文內(nèi)容,每條長度100漢字的fts5的表查詢?nèi)齻€(gè)詞,optimize
狀態(tài)下耗時(shí)2.9ms
,分別限制每個(gè)level
的segment
數(shù)量為2
、3
、4
時(shí)的查詢耗時(shí)分別為4.7ms
、8.9ms
、15ms
。100w條內(nèi)容每次寫入100條的情況下,按照WCDB的方案執(zhí)行merge
的耗時(shí)在10s
內(nèi)。
使用自動(dòng)Merge
機(jī)制,可以在不影響索引更新性能的情況下,將FTS5索引保持在最接近Optimize
的狀態(tài),提高了搜索速度。
3、分詞器優(yōu)化
分詞器性能優(yōu)化
分詞器是全文搜索的關(guān)鍵模塊,它實(shí)現(xiàn)將輸入內(nèi)容拆分成多個(gè)Token
并提供這些Token
的位置,搜索引擎再對這些Token
建立索引。SQLite的FTS組件支持自定義分詞器,可以按照業(yè)務(wù)需求實(shí)現(xiàn)自己的分詞器。
分詞器的分詞方法可以分為按字分詞和按詞分詞。前者只是簡單對輸入內(nèi)容逐字建立索引,后者則需要理解輸入內(nèi)容的語義,對有具體含義的詞組建立索引。相比于按字分詞,按詞分詞的優(yōu)勢是既可以減少建索引的Token
數(shù)量,也可以減少搜索時(shí)匹配的Token
數(shù)量,劣勢是需要理解語義,而且用戶輸入的詞不完整時(shí)也會(huì)有搜不到的問題。
為了簡化客戶端邏輯和避免用戶漏輸內(nèi)容時(shí)搜不到的問題,iOS微信之前的FTS3分詞器OneOrBinaryTokenizer
是采用了一種巧妙的按字分詞算法,除了對輸入內(nèi)容逐字建索引,還會(huì)對內(nèi)容中每兩個(gè)連續(xù)的字建索引,對于搜索內(nèi)容則是按照每兩個(gè)字進(jìn)行分詞。下面是一個(gè)用“北京歡迎你”去搜索相同內(nèi)容的分詞例子:
相比于簡單的按字分詞,這種分詞方式的優(yōu)勢是可以將搜索時(shí)匹配的Token
數(shù)量接近降低一半,提高搜索速度,而且在一定程度上可以提升搜索精度,比如搜索“歡迎你北京”就匹配不到“北京歡迎你”;這種分詞方式的劣勢就是保存的索引內(nèi)容很多,基本輸入內(nèi)容的每個(gè)字都在索引中保存了三次,是一種用空間換時(shí)間的做法。
因?yàn)?code>OneOrBinaryTokenizer用接近三倍的索引內(nèi)容增長才換取不到兩倍的搜索性能提升,不是很劃算,所以我們在FTS5上重新開發(fā)了一種新的分詞器VerbatimTokenizer
,這個(gè)分詞器只采用基本的按字分詞,不保存冗余索引內(nèi)容。同時(shí)在搜索時(shí),每兩個(gè)字用引號引起來組成一個(gè)Phrase
,按照FTS5的搜索語法,搜索時(shí)Phrase
中的字要按順序相鄰出現(xiàn)的內(nèi)容才會(huì)命中,實(shí)現(xiàn)了跟OneOrBinaryTokenizer
一樣的搜索精度。?VerbatimTokenizer
的分詞規(guī)則示意圖如下:
分詞器能力擴(kuò)展
VerbatimTokenizer還根據(jù)微信實(shí)際的業(yè)務(wù)需求實(shí)現(xiàn)了五種擴(kuò)展能力來提高搜索的容錯(cuò)能力:
- 支持在分詞時(shí)將繁體字轉(zhuǎn)換成簡體字。這樣用戶可以用繁體字搜到簡體字內(nèi)容,用簡體字也能搜到繁體字內(nèi)容,避免了因?yàn)闈h字的簡體和繁體字形相近導(dǎo)致用戶輸錯(cuò)的問題。
- 支持Unicode歸一化。Unicode支持相同字形的字符用不同的編碼來表示,比如編碼為
\ue9
的é
和編碼為\u65\u301
的e?
有相同的字形,這會(huì)導(dǎo)致用戶用看上去一樣的內(nèi)容去搜索結(jié)果搜不到的問題。Unicode歸一化就是把字形相同的字符用同一個(gè)編碼表示。 - 支持過濾符號。大部分情況下,我們不需要支持對符號建索引,符號的重復(fù)量大而且用戶一般也不會(huì)用符號去搜索內(nèi)容,但是聯(lián)系人搜索這個(gè)業(yè)務(wù)場景需要支持符號搜索,因?yàn)橛脩舻年欠Q里面經(jīng)常出現(xiàn)顏文字,符號的使用量不低。
- 支持用
Porter Stemming
算法對英文單詞取詞干。取詞干的好處是允許用戶搜索內(nèi)容的單復(fù)數(shù)和時(shí)態(tài)跟命中內(nèi)容不一致,讓用戶更容易搜到內(nèi)容。但是取詞干也有弊端,比如用戶要搜索的內(nèi)容是“happyday”,輸入“happy”作為前綴去搜索卻會(huì)搜不到,因?yàn)椤癶appyday”取詞干變成“happydai”,“happy”取詞干變成“happi”,后者就不能成為前者的前綴。這種badcase在內(nèi)容為多個(gè)英文單詞拼接一起時(shí)容易出現(xiàn),聯(lián)系人昵稱的拼接英文很常見,所以在聯(lián)系人的索引中沒有取詞干,在其他業(yè)務(wù)場景中都用上了。 - 支持將字母全部轉(zhuǎn)成小寫。這樣用戶可以用小寫搜到大寫,反之亦然。
這些擴(kuò)展能力都是對建索引內(nèi)容和搜索內(nèi)容中的每個(gè)字做變換,這個(gè)變換其實(shí)也可以在業(yè)務(wù)層做,其中的Unicode歸一化和簡繁轉(zhuǎn)換以前就是在業(yè)務(wù)層實(shí)現(xiàn)的。但是這樣做有兩個(gè)弊端,一個(gè)是業(yè)務(wù)層每做一個(gè)轉(zhuǎn)換都需要對內(nèi)容做一次遍歷,引入冗余計(jì)算量,另一個(gè)是寫入到索引中的內(nèi)容是轉(zhuǎn)變后的內(nèi)容,那么搜索出來的結(jié)果也是轉(zhuǎn)變后的,會(huì)和原文不一致,業(yè)務(wù)層做內(nèi)容判斷的時(shí)候容易出錯(cuò)。鑒于這兩個(gè)原因,VerbatimTokenizer
將這些轉(zhuǎn)變能力都集中到了分詞器中實(shí)現(xiàn)。
4、索引內(nèi)容支持多級分隔符
SQLite的FTS索引表不支持在建表后再添加新列,但是隨著業(yè)務(wù)的發(fā)展,業(yè)務(wù)數(shù)據(jù)支持搜索的屬性會(huì)變多,如何解決新屬性的搜索問題呢?特別是在聯(lián)系人搜索這個(gè)業(yè)務(wù)場景,一個(gè)聯(lián)系人支持搜索的字段非常多。一個(gè)直接的想法是將新屬性和舊屬性用分隔符拼接到一起建索引。但這樣會(huì)引入新的問題,F(xiàn)TS5是以整個(gè)字段的內(nèi)容作為整體去匹配的,如果用戶搜索匹配的Token
在不同的屬性,那這條數(shù)據(jù)也會(huì)命中,這個(gè)結(jié)果顯然不是用戶想要的,搜索結(jié)果的精確度就降低了。
我們需要搜索匹配的Token
中間不存在分隔符,那這樣可以確保匹配的Token
都在一個(gè)屬性內(nèi)。同時(shí),為了支持業(yè)務(wù)靈活擴(kuò)展,還需要支持多級分隔符,而且搜索結(jié)果中還要支持獲取匹配結(jié)果的層級、位置以及該段內(nèi)容的原文和匹配詞。這個(gè)能力FTS5還不沒有,而FTS5的自定義輔助函數(shù)支持在搜索時(shí)獲取到所有命中結(jié)果中每個(gè)命中Token
的位置,利用這個(gè)信息可以推斷出這些Token
中間有沒有分隔符,以及這些Token
所在的層級,所以我們開發(fā)了SubstringMatchInfo
這個(gè)新的FTS5搜索輔助函數(shù)來實(shí)現(xiàn)這個(gè)能力。這個(gè)函數(shù)的大致執(zhí)行流程如下:
三、全文搜索應(yīng)用邏輯優(yōu)化
1、數(shù)據(jù)庫表格式優(yōu)化
1.1 非文本搜索內(nèi)容的保存方式
在實(shí)際應(yīng)用中,我們除了要在數(shù)據(jù)庫中保存需要搜索的文本的FTS索引,還需要額外保存這個(gè)文本對應(yīng)的業(yè)務(wù)數(shù)據(jù)的id
、用于結(jié)果排序的的屬性(常見的是業(yè)務(wù)數(shù)據(jù)的創(chuàng)建時(shí)間)以及其他需要直接跟隨搜索結(jié)果讀出的內(nèi)容,這些都是不參與文本搜索的內(nèi)容。根據(jù)非文本搜索內(nèi)容的不同存儲(chǔ)位置,我們可以將FTS索引表的表格式分成兩種:
- 第一種方式是將非文本搜索內(nèi)容存儲(chǔ)在額外的普通表中,這個(gè)表保存FTS索引的
Rowid
和非文本搜索內(nèi)容的映射關(guān)系,而FTS索引表的每一行只保存可搜索的文本內(nèi)容,這個(gè)表格式類似于這樣:
這種表格式的優(yōu)勢是FTS索引表的內(nèi)容很簡單,不熟悉FTS索引表配置的同學(xué)不容易出錯(cuò),而且普通表的可擴(kuò)展性好,支持添加新列;劣勢則是搜索時(shí)需要先用FTS索引的Rowid
讀取到普通表的Rowid
,這樣才能讀取到普通表的其他內(nèi)容,搜索速度慢一點(diǎn),而且搜索時(shí)需要聯(lián)表查詢,搜索SQL語句稍微復(fù)雜一點(diǎn)。
- 第二種方式是將非文本搜索內(nèi)容直接和可搜索文本內(nèi)容一起存儲(chǔ)在FTS索引表中,表格式類似于這樣:
這種方式的優(yōu)劣勢跟前一種方式恰好相反,優(yōu)勢是搜索速度快而且搜索方式簡單,劣勢是擴(kuò)展性差且需要更細(xì)致的配置。
因?yàn)閕OS微信以前是使用第二種表格式,而且微信的搜索業(yè)務(wù)已經(jīng)穩(wěn)定不會(huì)有大變化,我們現(xiàn)在更加追求搜索速度,所以我們還是繼續(xù)使用第二種表格式來存儲(chǔ)全文搜索的數(shù)據(jù)。
1.2 避免冗余索引內(nèi)容
FTS索引表默認(rèn)對表中的每一列的內(nèi)容都建倒排索引,即便是數(shù)字內(nèi)容也會(huì)按照文本來處理,這樣會(huì)導(dǎo)致我們保存在FTS索引表中的非文本搜索內(nèi)容也建了索引,進(jìn)而增大索引文件的大小、索引更新的耗時(shí)和搜索的耗時(shí),這顯然不是我們想要的。
FTS5支持給索引表中的列添加UNINDEXED
約束,這樣FTS5就不會(huì)對這個(gè)列建索引了,所以給可搜索文本內(nèi)容之外的所有列添加這個(gè)約束就可以避免冗余索引。
1.3 降低索引內(nèi)容的大小
前面提到,倒排索引主要保存文本中每個(gè)Token
對應(yīng)的行號(rowid
)、列號和字段中的每次出現(xiàn)的位置偏移,其中的行號是SQLite自動(dòng)分配的,位置偏移是根據(jù)業(yè)務(wù)的實(shí)際內(nèi)容,這兩個(gè)我們都決定不了,但是列號是可以調(diào)整的。
在FTS5索引中,一個(gè)Token
在一行中的索引內(nèi)容的格式是這樣的:
從中可以看出,如果我們把可搜索文本內(nèi)容設(shè)置在第一列的話(多個(gè)可搜索文本列的話,把內(nèi)容多的列放到第一列),就可以少保存列分割符0x01
和列號,這樣可以明顯降低索引文件大小。
所以我們最終的表格式是這樣:
1.4 索引文件大小優(yōu)化數(shù)據(jù)
下面是iOS微信優(yōu)化前后的平均每個(gè)用戶的索引文件大小對比:
2、索引更新邏輯優(yōu)化
為了將全文搜索邏輯和業(yè)務(wù)邏輯解耦,iOS微信的FTS索引是不保存在各個(gè)業(yè)務(wù)的數(shù)據(jù)庫中的,而是集中保存到一個(gè)專用的全文搜索數(shù)據(jù)庫,各個(gè)業(yè)務(wù)的數(shù)據(jù)有更新之后再異步通知全文搜索模塊更新索引。整體流程如下:
這樣做既可以避免索引更新拖慢業(yè)務(wù)數(shù)據(jù)更新的速度,也能避免索引數(shù)據(jù)更新出錯(cuò)甚至索引數(shù)據(jù)損壞對業(yè)務(wù)造成影響,讓全文搜索功能模塊能夠充分獨(dú)立。
2.1 保證索引和數(shù)據(jù)的一致
業(yè)務(wù)數(shù)據(jù)和索引數(shù)據(jù)分離且異步同步的好處很多,但實(shí)現(xiàn)起來也很難,最難的問題是如何保證業(yè)務(wù)數(shù)據(jù)和索引數(shù)據(jù)的一致,也即要保證業(yè)務(wù)數(shù)據(jù)和索引數(shù)據(jù)要逐條對應(yīng),不多不少。曾經(jīng)iOS微信在這里踩了很多坑,打了很多補(bǔ)丁都不能完整解決這個(gè)問題,我們需要一個(gè)更加體系化的方法來解決這個(gè)問題。
為了簡化問題,我們可以把一致性問題可以拆成兩個(gè)方面分別處理,一個(gè)是保證所有業(yè)務(wù)數(shù)據(jù)都有索引,這個(gè)用戶的搜索結(jié)果就不會(huì)有缺漏;第二個(gè)是保證所有索引都對應(yīng)一個(gè)有效的業(yè)務(wù)數(shù)據(jù),這樣用戶就不會(huì)搜到無效的結(jié)果。
要保證所有業(yè)務(wù)數(shù)據(jù)都有索引,首先要找到或者構(gòu)造一種一直增長的數(shù)據(jù)來描述業(yè)務(wù)數(shù)據(jù)更新的進(jìn)度,這個(gè)進(jìn)度數(shù)據(jù)的更新和業(yè)務(wù)數(shù)據(jù)的更新能保證原子性,而且根據(jù)這個(gè)進(jìn)度的區(qū)間能拿出業(yè)務(wù)數(shù)據(jù)更新的內(nèi)容,這樣我們就可以依賴這個(gè)進(jìn)度來更新索引。在微信的業(yè)務(wù)中,不同業(yè)務(wù)的進(jìn)度數(shù)據(jù)不同,聊天記錄是使用消息的rowid
,收藏是使用收藏跟后臺(tái)同步的updateSequence
,而聯(lián)系人找不到這種一直增長的進(jìn)度數(shù)據(jù),我們是通過在聯(lián)系人數(shù)據(jù)庫中標(biāo)記有新增或有更新的聯(lián)系人的微信號來作為索引更新進(jìn)度。進(jìn)度數(shù)據(jù)的使用方法如下:
無論業(yè)務(wù)數(shù)據(jù)是否保存成功、更新通知是否到達(dá)全文搜索模塊、索引數(shù)據(jù)是否保存成功,這套索引更新邏輯都能保證保存成功的業(yè)務(wù)數(shù)據(jù)都能成功建到索引。這其中的一個(gè)關(guān)鍵點(diǎn)是數(shù)據(jù)和進(jìn)度要在同個(gè)事務(wù)中一起更新,而且要保存在同個(gè)數(shù)據(jù)庫中,這樣才能保證數(shù)據(jù)和進(jìn)度的更新的原子性(WCDB創(chuàng)建的數(shù)據(jù)庫因?yàn)槭褂?code>WAL模式而無法保證不同數(shù)據(jù)庫的事務(wù)的原子性)。還有一個(gè)操作圖中沒有畫出,具體是微信啟動(dòng)時(shí)如果檢查到業(yè)務(wù)進(jìn)度小于索引進(jìn)度,這種一般意味著業(yè)務(wù)數(shù)據(jù)損壞后被重置了,這種情況下要?jiǎng)h掉索引并重置索引進(jìn)度。
對于每個(gè)索引都對應(yīng)有效的業(yè)務(wù)數(shù)據(jù),這就要求業(yè)務(wù)數(shù)據(jù)刪除之后索引也要必須刪掉。現(xiàn)在業(yè)務(wù)數(shù)據(jù)的刪除和索引的刪除是異步的,會(huì)出現(xiàn)業(yè)務(wù)數(shù)據(jù)刪掉之后索引沒刪除的情況。這種情況會(huì)導(dǎo)致兩個(gè)問題,一個(gè)是冗余索引會(huì)導(dǎo)致搜索速度變慢,但這個(gè)問題出現(xiàn)概率很小,這個(gè)影響可以忽略不計(jì);第二個(gè)問題是會(huì)導(dǎo)致用戶搜到無效數(shù)據(jù),這個(gè)是要避免的。因?yàn)橐耆珓h掉所有無效索引成本比較高,所以我們采用了惰性檢查的方法來解決這個(gè)問題,具體做法是搜索結(jié)果要顯示給用戶時(shí),才檢查這個(gè)數(shù)據(jù)是否有效,無效的話不顯示這個(gè)搜索結(jié)果并異步刪除對應(yīng)的索引。因?yàn)橛脩粢黄聊芸吹降臄?shù)據(jù)很少,所以檢查邏輯帶來的性能消耗也可以忽略不計(jì)。而且這個(gè)檢查操作實(shí)際上也不算是額外加的邏輯,為了搜索結(jié)果展示內(nèi)容的靈活性,我們也要在展示搜索結(jié)果時(shí)讀出業(yè)務(wù)數(shù)據(jù),這樣也就順帶做了數(shù)據(jù)有效性的檢查。
2.2 建索引速度優(yōu)化
索引只有在搜索的時(shí)候才會(huì)用到,它的更新優(yōu)先級并沒有業(yè)務(wù)數(shù)據(jù)那么高,可以盡量攢更多的業(yè)務(wù)數(shù)據(jù)才去批量建索引。批量建索引有以下三個(gè)好處:
- 減少磁盤的寫入次數(shù),提高平均建索引速度。
- 在一個(gè)事務(wù)中,建索引SQL語句的解析結(jié)果可以反復(fù)使用,可以減少SQL語句的解析次數(shù),進(jìn)而提高平均建索引速度。
- 減少生成Segment的數(shù)量,從而減少
Merge Segment
帶來的讀寫消耗。
當(dāng)然,也不能保留太多業(yè)務(wù)數(shù)據(jù)不建索引,這樣用戶要搜索時(shí)會(huì)來不及建索引,從而導(dǎo)致搜索結(jié)果不完整。有了前面的Segment
自動(dòng)Merge
機(jī)制,索引的寫入速度非??煽?,只要控制好量,就不用擔(dān)心批量建索引帶來的高耗時(shí)問題。我們綜合考慮了低端機(jī)器的建索引速度和搜索頁面的拉起時(shí)間,確定了最大批量建索引數(shù)據(jù)條數(shù)為100條。同時(shí),我們會(huì)在內(nèi)存中cache本次微信運(yùn)行期間產(chǎn)生的未建索引業(yè)務(wù)數(shù)據(jù),在極端情況下給沒有來得及建索引的業(yè)務(wù)數(shù)據(jù)提供相對內(nèi)存搜索,保證搜索結(jié)果的完整性。因?yàn)閏ache上一次微信運(yùn)行期間產(chǎn)生的未建索引數(shù)據(jù)需要引入額外的磁盤IO,所以微信啟動(dòng)后會(huì)觸發(fā)一次建索引邏輯,對現(xiàn)有的未建索引業(yè)務(wù)數(shù)據(jù)建一次索引。總結(jié)一下觸發(fā)建索引的時(shí)機(jī)有三個(gè):
- 未建索引業(yè)務(wù)數(shù)據(jù)達(dá)到100條。
- 進(jìn)入搜索界面。
- 微信啟動(dòng)。
2.3 刪除索引速度優(yōu)化
索引的刪除速度經(jīng)常是設(shè)計(jì)索引更新機(jī)制時(shí)比較容易忽視的因素,因?yàn)楸粍h除的業(yè)務(wù)數(shù)據(jù)量容易被低估,會(huì)被誤以為是低概率場景,但實(shí)際被用戶刪除的業(yè)務(wù)數(shù)據(jù)可能會(huì)達(dá)到50%,是個(gè)不可忽視的主場景。而且SQLite是不支持并行寫入的,刪除索引的性能也會(huì)間接影響到索引的寫入速度,會(huì)為索引更新引入不可控因素。
因?yàn)閯h除索引的時(shí)候是拿著業(yè)務(wù)數(shù)據(jù)的id去刪除的,所以提高刪除索引速度的方式有兩種:
- 建一個(gè)業(yè)務(wù)數(shù)據(jù)
id
到FTS索引的rowid
的普通索引。 - 在FTS索引表中去掉業(yè)務(wù)數(shù)據(jù)
Id
那一列的UNINDEXED
約束,給業(yè)務(wù)數(shù)據(jù)Id
添加倒排索引。
這里倒排索引其實(shí)沒有普通索引那么高效,有兩個(gè)原因:
- 倒排索引相比普通索引還帶了很多額外信息,搜索效率低一些。
- 如果需要多個(gè)業(yè)務(wù)字段才能確定一條倒排索引時(shí),倒排索引是建不了聯(lián)合索引的,只能匹配其中一個(gè)業(yè)務(wù)字段,其他字段就是遍歷匹配,這種情況搜索效率會(huì)很低。
2.4 索引更新性能優(yōu)化數(shù)據(jù)
聊天記錄的優(yōu)化前后索引性能數(shù)據(jù)如下:
收藏的優(yōu)化前后索引性能數(shù)據(jù)如下:
3、搜索邏輯優(yōu)化
用戶在iOS微信的首頁輸入內(nèi)容搜索時(shí),搜索的整體流程如下:
用戶變更搜索框的內(nèi)容之后,會(huì)并行發(fā)起所有業(yè)務(wù)的搜索任務(wù),各個(gè)搜索任務(wù)執(zhí)行完之后才再將搜索結(jié)果返回到主線程給頁面展示。這個(gè)邏輯會(huì)隨著用戶變更搜索內(nèi)容而繼續(xù)重復(fù)。
3.1 單個(gè)搜索任務(wù)支持并行執(zhí)行
雖然現(xiàn)在不同搜索任務(wù)已經(jīng)支持并行執(zhí)行,但是不同業(yè)務(wù)的數(shù)據(jù)量和搜索邏輯差別很大,數(shù)據(jù)量大或者搜索邏輯復(fù)雜的任務(wù)耗時(shí)會(huì)很久,這樣還不能充分發(fā)揮手機(jī)的并行處理能力。我們還可以將并行處理能力引入單個(gè)搜索任務(wù)內(nèi),這里有兩種處理方式:
- 對于搜索數(shù)據(jù)量大的業(yè)務(wù)(比如聊天記錄搜索),可以將索引數(shù)據(jù)均分存儲(chǔ)到多個(gè)FTS索引表(注意這里不均分的話還是會(huì)存在短板效應(yīng)),這樣搜索時(shí)可以并行搜索各個(gè)索引表,然后匯總各個(gè)表的搜索結(jié)果,再進(jìn)行統(tǒng)一排序。這里拆分的索引表數(shù)量既不能太多也不能太少,太多會(huì)超出手機(jī)實(shí)際的并行處理能力,也會(huì)影響其他搜索任務(wù)的性能,太少又不能充分利用并行處理能力。以前微信用了十個(gè)FTS表存儲(chǔ)聊天記錄索引,現(xiàn)在改為使用四個(gè)FTS表。
- 對于搜索邏輯復(fù)雜的業(yè)務(wù)(比如聯(lián)系人搜索),可以將可獨(dú)立執(zhí)行的搜索邏輯并行執(zhí)行。比如在聯(lián)系人搜索任務(wù)中,我們將聯(lián)系人的普通文本搜索、拼音搜索、標(biāo)簽和地區(qū)的搜索、多群成員的搜索并行執(zhí)行,搜完之后再合并結(jié)果進(jìn)行排序。這里為什么不也用拆表的方式呢?因?yàn)檫@種搜索結(jié)果數(shù)量少的場景,搜索的耗時(shí)主要是集中在搜索索引的環(huán)節(jié),索引可以看做一顆B樹,將一顆B樹拆分成多個(gè),搜索耗時(shí)并不會(huì)成比例下降。
3.2 搜索任務(wù)支持中斷
用戶在搜索框持續(xù)輸入內(nèi)容的過程中可能會(huì)自動(dòng)多次發(fā)起搜索任務(wù),如果在前一次發(fā)起的搜索任務(wù)還沒執(zhí)行完時(shí),就再次發(fā)起搜索任務(wù),那前后兩次搜索任務(wù)就會(huì)互相影響對方性能。這種情況在用戶輸入內(nèi)容從短到長的過程中還挺容易出現(xiàn)的,因?yàn)樗阉魑谋径痰臅r(shí)候命中結(jié)果就很多,搜索任務(wù)也就更加耗時(shí),從而更有機(jī)會(huì)撞上后面的搜索任務(wù)。太多任務(wù)同時(shí)執(zhí)行還會(huì)容易引起手機(jī)發(fā)燙、爆內(nèi)存的問題。所以我們需要讓搜索任務(wù)支持隨時(shí)中斷,這樣就可以在后一次搜索任務(wù)發(fā)起的時(shí)候,能夠中斷前一次的搜索任務(wù),避免任務(wù)量過多的問題。
搜索任務(wù)支持中斷的實(shí)現(xiàn)方式是給每個(gè)搜索任務(wù)設(shè)置一個(gè)CancelFlag
,在搜索邏輯執(zhí)行時(shí)每搜到一個(gè)結(jié)果就判斷一下CancelFlag
是否置位,如果置位了就立即退出任務(wù)。外部邏輯可以通過置位CancelFlag
來中斷搜索任務(wù)。邏輯流程如下圖所示:
為了讓搜索任務(wù)能夠及時(shí)中斷,我們需要讓檢查CancelFlag
的時(shí)間間隔盡量相等,要實(shí)現(xiàn)這個(gè)目標(biāo)就要在搜索時(shí)避免使用OrderBy子句對結(jié)果進(jìn)行排序。因?yàn)镕TS5不支持建立聯(lián)合索引,所以在使用OrderBy
子句時(shí),SQLite在輸出第一個(gè)結(jié)果前會(huì)遍歷所有匹配結(jié)果進(jìn)行排序,這就讓輸出第一個(gè)結(jié)果的耗時(shí)幾乎等于輸出全部結(jié)果的耗時(shí),中斷邏輯就失去了意義。不使用OrderBy
子句就對搜索邏輯添加了兩個(gè)限制:
- 從數(shù)據(jù)庫讀取所有結(jié)果之后再排序。我們可以在讀取結(jié)果時(shí)將用于排序的字段一并讀出,然后在讀完所有結(jié)果之后再對所有結(jié)果執(zhí)行排序。因?yàn)榕判虻暮臅r(shí)占總搜索耗時(shí)的比例很低,加上排序算法的性能大同小異,這種做法對搜索速度的影響可以忽略。
- 不能使用分段查詢。在全文搜索這個(gè)場景中,分段查詢其實(shí)是沒有什么作用的。因?yàn)榉侄尾樵兙鸵獙Y(jié)果排序,對結(jié)果排序就要遍歷所有結(jié)果,所以分段查詢并不能降低搜索耗時(shí)(除非按照FTS索引的
Rowid
分段查詢,但是Rowid
不包含實(shí)際的業(yè)務(wù)信息)。
3.3 搜索讀取內(nèi)容最少化
搜索時(shí)讀取內(nèi)容的量也是決定搜索耗時(shí)的一個(gè)關(guān)鍵因素。FTS索引表實(shí)際是有多個(gè)SQLite普通表組成的,這其中一些表格存儲(chǔ)實(shí)際的倒排索引內(nèi)容,還有一個(gè)表格存儲(chǔ)用戶保存到FTS索引表的全部原文。當(dāng)搜索時(shí)讀取Rowid
以外的內(nèi)容時(shí),就需要用Rowid
到保存原文的表的讀取內(nèi)容,索引表輸出結(jié)果的內(nèi)部執(zhí)行過程如下:
所以讀取內(nèi)容越少輸出結(jié)果的速度越快,而且讀取內(nèi)容過多也會(huì)有消耗內(nèi)存的隱患。我們采用的方式是搜索時(shí)只讀取業(yè)務(wù)數(shù)據(jù)id和用于排序的業(yè)務(wù)屬性,排好序之后,在需要給用戶展示結(jié)果時(shí),才用業(yè)務(wù)數(shù)據(jù)id按需讀取業(yè)務(wù)數(shù)據(jù)具體內(nèi)容出來展示。這樣做的擴(kuò)展性也會(huì)很好,可以在不更改存儲(chǔ)內(nèi)容的情況下,根據(jù)各個(gè)業(yè)務(wù)的需求不斷調(diào)整搜索結(jié)果展示的內(nèi)容。
這里還有一個(gè)要特別提一下的地方,就是搜索時(shí)盡量不要讀取高亮信息(SQLite的highlight
函數(shù)有這個(gè)能力)。因?yàn)橐@取高亮字段不僅要將文本的原文讀取出來,還要對文本原文再次分詞,才能定位命中位置的原文內(nèi)容,搜索結(jié)果多的情況下分詞帶來的消耗非常明顯。那展示搜索結(jié)果時(shí)如何獲取高亮匹配內(nèi)容呢?我們采用的方式是將用戶的搜索文本進(jìn)行分詞,然后在展示結(jié)果時(shí)查找每個(gè)Token
在展示文本中的位置,然后將那個(gè)位置高亮顯示。同樣因?yàn)橛脩粢黄量吹降慕Y(jié)果數(shù)量是很少的,這里的高亮邏輯帶來的性能消耗可以忽略。
當(dāng)然在搜索規(guī)則很復(fù)雜的情況下,直接讀取高亮信息是比較方便,比如聯(lián)系人搜索就使用前面提到的SubstringMatchInfo
函數(shù)來讀取高亮內(nèi)容。這里主要還是因?yàn)橐x取匹配內(nèi)容所在的層級和位置用于排序,所以逐個(gè)結(jié)果重新分詞的操作在所難免。
3.4 搜索性能優(yōu)化數(shù)據(jù)
下面是微信各搜索業(yè)務(wù)優(yōu)化前后的搜索耗時(shí)對比:
文章來源:http://www.zghlxwxcb.cn/news/detail-410314.html
四、總結(jié)
目前iOS微信已經(jīng)將這套新全文搜索技術(shù)方案全量應(yīng)用到聊天記錄、聯(lián)系人和收藏的搜索業(yè)務(wù)中。使用新方案之后,全文搜索的索引文件占用空間更小,索引更新耗時(shí)更少,搜索速度也更快了,可以說全文搜索的性能得到了全方位提升。文章來源地址http://www.zghlxwxcb.cn/news/detail-410314.html
到了這里,關(guān)于程序員業(yè)務(wù),微信全文搜索技術(shù)優(yōu)化的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!