IM全文檢索技術(shù)專題(四):微信iOS端的最新全文檢索技術(shù)優(yōu)化實(shí)踐

本文由微信開發(fā)團(tuán)隊(duì)工程師“ qiuwenchen”分享蚤蔓,發(fā)布于WeMobileDev公眾號续膳,有修訂。

1兰粉、引言

全文搜索是使用倒排索引進(jìn)行搜索的一種搜索方式。倒排索引也稱為反向索引顶瞳,是指對輸入的內(nèi)容中的每個(gè)Token建立一個(gè)索引玖姑,索引中保存了這個(gè)Token在內(nèi)容中的具體位置。全文搜索技術(shù)主要應(yīng)用在對大量文本內(nèi)容進(jìn)行搜索的場景慨菱。

微信終端涉及到大量文本搜索的業(yè)務(wù)場景主要包括:im聯(lián)系人焰络、im聊天記錄、收藏的搜索符喝。

這些功能從2014年上線至今闪彼,底層技術(shù)已多年沒有更新:

1)聊天記錄使用的全文搜索引擎還是SQLite FTS3,而現(xiàn)在已經(jīng)有SQLite FTS5协饲;

2)收藏首頁的搜索還是使用簡單的Like語句去匹配文本畏腕;

3)聯(lián)系人搜索甚至用的是內(nèi)存搜索(在內(nèi)存中遍歷所有聯(lián)系人的所有屬性進(jìn)行匹配)。

隨著用戶在微信上積累的im聊天數(shù)據(jù)越來越多茉稠,提升微信底層搜索技術(shù)的需求也越來越迫切描馅。于是,在2021年我們對微信iOS端的全文搜索技術(shù)進(jìn)行了一次全面升級而线,本文主要記錄了本次技術(shù)升級過程中的技術(shù)實(shí)踐铭污。

(本文同步發(fā)布于:http://www.52im.net/thread-3839-1-1.html

2、系列文章

本文是專題系列文章中的第4篇:

IM全文檢索技術(shù)專題(一):微信移動(dòng)端的全文檢索優(yōu)化之路

IM全文檢索技術(shù)專題(二):微信移動(dòng)端的全文檢索多音字問題解決方案

IM全文檢索技術(shù)專題(三):網(wǎng)易云信Web端IM的聊天消息全文檢索技術(shù)實(shí)踐

IM全文檢索技術(shù)專題(四):微信iOS端的最新全文檢索技術(shù)優(yōu)化實(shí)踐》(* 本文

3吞获、全文檢索引擎的選型

iOS客戶端可以使用的全文搜索引擎并不多况凉,主要有:

1)SQLite三個(gè)版本的FTS組件(FTS3和4FTS5)各拷;

2)Lucene的C++實(shí)現(xiàn)版本CLucene刁绒;

3)?Lucene的C語言橋接版本Lucy

這里給出了這些引擎在事務(wù)能力烤黍、技術(shù)風(fēng)險(xiǎn)知市、搜索能力傻盟、讀寫性能等方面的比較(見下圖)。

1)在事務(wù)能力方面:

Lucene沒有提供完整的事務(wù)能力嫂丙,因?yàn)長ucene使用了多文件的存儲結(jié)構(gòu)娘赴,它沒有保證事務(wù)的原子性。

而SQLite的FTS組件因?yàn)榈讓舆€是使用普通的表來實(shí)現(xiàn)的跟啤,可以完美繼承SQLite的事務(wù)能力诽表。

2)在技術(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)是最低的种柑。

3)在搜索能力方面::

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)矾兜。

4)在讀寫性能方面:

下面3個(gè)圖是用不同引擎對100萬條長度為10的隨機(jī)生成中文語句生成Optimize狀態(tài)的索引的性能數(shù)據(jù),其中每個(gè)語句的漢字出現(xiàn)頻率按照實(shí)際的漢字使用頻率患久。

從上面的3張圖可以看到: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微信全文搜索的搜索引擎。

4、引擎層優(yōu)化1:實(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操作有兩種:

1)automerge:某一個(gè)level的segment達(dá)到4時(shí)就開始在寫入內(nèi)容時(shí)自動(dòng)執(zhí)行一部分merge操作淀歇,稱為一次automerge双揪。每次automerge的寫入量跟本次更新的寫入量成正比英染,需要多次automerge才能完整合并成一個(gè)新segment蜂大。Automerge在完整生成一個(gè)新的segment前喂柒,需要多次裁剪舊的segment的已合并內(nèi)容志鞍,引入多余的寫入量恨搓;

2)crisismerge:本次寫入后某一個(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ù)昼激。

具體做法如下:

1)監(jiān)聽有FTS5索引的數(shù)據(jù)庫每個(gè)事務(wù)變更到的FTS5索引表庇绽,拋通知到子線程觸發(fā)WCDB的自動(dòng)merge操作;

2)Merge線程檢查所有FTS5索引表中segment數(shù)超過 1 的level執(zhí)行一次merge橙困;

3)Merge時(shí)每寫入16頁數(shù)據(jù)檢查一次有沒有其他線程的寫入操作因?yàn)閙erge操作阻塞瞧掺,如果有就立即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(jìn)也可以減小總寫入量。

性能方面庶溶,對一個(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)喳张,提高了搜索速度。

5美澳、引擎層優(yōu)化2:分詞器優(yōu)化

5.1 分詞器性能優(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)行分詞附鸽。

下面是用“北京歡迎你”去搜索相同內(nèi)容的分詞例子:

相比于簡單的按字分詞脱拼,這種分詞方式的優(yōu)勢是可以將搜索時(shí)匹配的Token數(shù)量接近降低一半,提高搜索速度坷备,而且在一定程度上可以提升搜索精度(比如搜索“歡迎你北京”就匹配不到“北京歡迎你”)熄浓。這種分詞方式的劣勢就是保存的索引內(nèi)容很多,基本輸入內(nèi)容的每個(gè)字都在索引中保存了三次省撑,是一種用空間換時(shí)間的做法赌蔑。

因?yàn)?i>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ī)則示意圖如下:

5.2 分詞器能力擴(kuò)展

VerbatimTokenizer還根據(jù)微信實(shí)際的業(yè)務(wù)需求實(shí)現(xiàn)了五種擴(kuò)展能力來提高搜索的容錯(cuò)能力:

1)支持在分詞時(shí)將繁體字轉(zhuǎn)換成簡體字:這樣用戶可以用繁體字搜到簡體字內(nèi)容筷黔,用簡體字也能搜到繁體字內(nèi)容,避免了因?yàn)闈h字的簡體和繁體字形相近導(dǎo)致用戶輸錯(cuò)的問題仗颈。

2)支持Unicode歸一化:Unicode支持相同字形的字符用不同的編碼來表示佛舱,比如編碼為\ue9的é和編碼為\u65\u301的é有相同的字形,這會(huì)導(dǎo)致用戶用看上去一樣的內(nèi)容去搜索結(jié)果搜不到的問題挨决。Unicode歸一化就是把字形相同的字符用同一個(gè)編碼表示请祖。

3)支持過濾符號:大部分情況下,我們不需要支持對符號建索引脖祈,符號的重復(fù)量大而且用戶一般也不會(huì)用符號去搜索內(nèi)容肆捕,但是聯(lián)系人搜索這個(gè)業(yè)務(wù)場景需要支持符號搜索,因?yàn)橛脩舻年欠Q里面經(jīng)常出現(xiàn)顏文字盖高,符號的使用量不低慎陵。

4)支持用Porter Stemming算法對英文單詞取詞干:取詞干的好處是允許用戶搜索內(nèi)容的單復(fù)數(shù)和時(shí)態(tài)跟命中內(nèi)容不一致,讓用戶更容易搜到內(nèi)容喻奥。但是取詞干也有弊端席纽,比如用戶要搜索的內(nèi)容是“happyday”,輸入“happy”作為前綴去搜索卻會(huì)搜不到撞蚕,因?yàn)椤?i>happyday”取詞干變成“happydai”润梯,“happy”取詞干變成“happi”,后者就不能成為前者的前綴。這種badcase在內(nèi)容為多個(gè)英文單詞拼接一起時(shí)容易出現(xiàn)纺铭,聯(lián)系人昵稱的拼接英文很常見寇钉,所以在聯(lián)系人的索引中沒有取詞干,在其他業(yè)務(wù)場景中都用上了舶赔。

5)支持將字母全部轉(zhuǎn)成小寫:這樣用戶可以用小寫搜到大寫扫倡,反之亦然。

這些擴(kuò)展能力都是對建索引內(nèi)容和搜索內(nèi)容中的每個(gè)字做變換顿痪,這個(gè)變換其實(shí)也可以在業(yè)務(wù)層做镊辕,其中的Unicode歸一化和簡繁轉(zhuǎn)換以前就是在業(yè)務(wù)層實(shí)現(xiàn)的。

但是這樣做有兩個(gè)弊端:

1)一個(gè)是業(yè)務(wù)層每做一個(gè)轉(zhuǎn)換都需要對內(nèi)容做一次遍歷蚁袭,引入冗余計(jì)算量征懈;

2)一個(gè)是寫入到索引中的內(nèi)容是轉(zhuǎn)變后的內(nèi)容,那么搜索出來的結(jié)果也是轉(zhuǎn)變后的揩悄,會(huì)和原文不一致卖哎,業(yè)務(wù)層做內(nèi)容判斷的時(shí)候容易出錯(cuò)。

鑒于這兩個(gè)原因删性,VerbatimTokenizer將這些轉(zhuǎn)變能力都集中到了分詞器中實(shí)現(xiàn)亏娜。

6、引擎層優(yōu)化3:索引內(nèi)容支持多級分隔符

SQLite的FTS索引表不支持在建表后再添加新列蹬挺,但是隨著業(yè)務(wù)的發(fā)展维贺,業(yè)務(wù)數(shù)據(jù)支持搜索的屬性會(huì)變多,如何解決新屬性的搜索問題呢巴帮?

特別是在聯(lián)系人搜索這個(gè)業(yè)務(wù)場景溯泣,一個(gè)聯(lián)系人支持搜索的字段非常多。

一個(gè)直接的想法是:將新屬性和舊屬性用分隔符拼接到一起建索引榕茧。

但這樣會(huì)引入新的問題:FTS5是以整個(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í)行流程如下:

7、應(yīng)用層優(yōu)化1:數(shù)據(jù)庫表格式優(yōu)化

7.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)容的不同存儲位置,我們可以將FTS索引表的表格式分成兩種:

1)第一種方式:是將非文本搜索內(nèi)容存儲在額外的普通表中航唆,這個(gè)表保存FTS索引的Rowid和非文本搜索內(nèi)容的映射關(guān)系胀蛮,而FTS索引表的每一行只保存可搜索的文本內(nèi)容。

這個(gè)表格式類似于這樣:

這種表格式的優(yōu)勢和劣勢是很明顯糯钙,分別是:

a)優(yōu)勢是:FTS索引表的內(nèi)容很簡單粪狼,不熟悉FTS索引表配置的同學(xué)不容易出錯(cuò),而且普通表的可擴(kuò)展性好任岸,支持添加新列再榄;

b)劣勢是:搜索時(shí)需要先用FTS索引的Rowid讀取到普通表的Rowid,這樣才能讀取到普通表的其他內(nèi)容享潜,搜索速度慢一點(diǎn)困鸥,而且搜索時(shí)需要聯(lián)表查詢,搜索SQL語句稍微復(fù)雜一點(diǎn)剑按。

2)第二種方式:是將非文本搜索內(nèi)容直接和可搜索文本內(nèi)容一起存儲在FTS索引表中疾就。

表格式類似于這樣:

這種方式的優(yōu)劣勢跟前一種方式恰好相反:

a)優(yōu)勢是:搜索速度快而且搜索方式簡單;

b)劣勢是:擴(kuò)展性差且需要更細(xì)致的配置吕座。

因?yàn)閕OS微信以前是使用第二種表格式虐译,而且微信的搜索業(yè)務(wù)已經(jīng)穩(wěn)定不會(huì)有大變化瘪板,我們現(xiàn)在更加追求搜索速度吴趴,所以我們還是繼續(xù)使用第二種表格式來存儲全文搜索的數(shù)據(jù)

7.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è)約束就可以避免冗余索引楞捂。

7.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和列號绳矩,這樣可以明顯降低索引文件大小。

所以我們最終的表格式是這樣:

7.4 優(yōu)化前后的效果對比

下面是iOS微信優(yōu)化前后的平均每個(gè)用戶的索引文件大小對比:

8劫侧、應(yīng)用層優(yōu)化2:索引更新邏輯優(yōu)化

8.1 概述

為了將全文搜索邏輯和業(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ú)立珍特。

8.2 保證索引和數(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è)方面分別處理:

1)一是保證所有業(yè)務(wù)數(shù)據(jù)都有索引,這個(gè)用戶的搜索結(jié)果就不會(huì)有缺漏相满;

2)二是保證所有索引都對應(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ù)不同:

1)聊天記錄是使用消息的rowid;

2)收藏是使用收藏跟后臺同步的updateSequence痛单;

3)聯(lián)系人找不到這種一直增長的進(jìn)度數(shù)據(jù)(我們是通過在聯(lián)系人數(shù)據(jù)庫中標(biāo)記有新增或有更新的聯(lián)系人的微信號來作為索引更新進(jìn)度)击费。

針對上述第3)點(diǎ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)槭褂肳AL模式而無法保證不同數(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è)問題:

1)一是冗余索引會(huì)導(dǎo)致搜索速度變慢萎羔,但這個(gè)問題出現(xiàn)概率很小液走,這個(gè)影響可以忽略不計(jì);

2)二是會(huì)導(dǎo)致用戶搜到無效數(shù)據(jù)贾陷,這個(gè)是要避免的缘眶。

針對上述第2)點(diǎn):因?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ù)有效性的檢查犯助。

8.3 建索引速度優(yōu)化

索引只有在搜索的時(shí)候才會(huì)用到,它的更新優(yōu)先級并沒有業(yè)務(wù)數(shù)據(jù)那么高维咸,可以盡量攢更多的業(yè)務(wù)數(shù)據(jù)才去批量建索引剂买。

批量建索引有以下三個(gè)好處:

1)減少磁盤的寫入次數(shù)惠爽,提高平均建索引速度;

2)在一個(gè)事務(wù)中瞬哼,建索引SQL語句的解析結(jié)果可以反復(fù)使用婚肆,可以減少SQL語句的解析次數(shù),進(jìn)而提高平均建索引速度坐慰;

3)減少生成Segment的數(shù)量较性,從而減少M(fèi)erge 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è):

1)未建索引業(yè)務(wù)數(shù)據(jù)達(dá)到100條痒留;

2)進(jìn)入搜索界面;

3)微信啟動(dòng)蠢沿。

8.4 刪除索引速度優(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去刪除的匈子。

所以提高刪除索引速度的方式有兩種:

1)建一個(gè)業(yè)務(wù)數(shù)據(jù)id到FTS索引的rowid的普通索引河胎;

2)在FTS索引表中去掉業(yè)務(wù)數(shù)據(jù)Id那一列的UNINDEXED約束,給業(yè)務(wù)數(shù)據(jù)Id添加倒排索引虎敦。

這里倒排索引其實(shí)沒有普通索引那么高效游岳,有兩個(gè)原因:

1)倒排索引相比普通索引還帶了很多額外信息政敢,搜索效率低一些;

2)如果需要多個(gè)業(yè)務(wù)字段才能確定一條倒排索引時(shí)胚迫,倒排索引是建不了聯(lián)合索引的喷户,只能匹配其中一個(gè)業(yè)務(wù)字段,其他字段就是遍歷匹配访锻,這種情況搜索效率會(huì)很低褪尝。

8.5 優(yōu)化前后的效果對比

聊天記錄的優(yōu)化前后索引性能數(shù)據(jù)如下:


收藏的優(yōu)化前后索引性能數(shù)據(jù)如下:


9、應(yīng)用層優(yōu)化3:搜索邏輯優(yōu)化

9.1 問題

用戶在iOS微信的首頁搜索內(nèi)容時(shí)期犬,交互邏輯如下:

如上圖所示:當(dāng)用戶變更搜索框的內(nèi)容之后恼五,會(huì)并行發(fā)起所有業(yè)務(wù)的搜索任務(wù),各個(gè)搜索任務(wù)執(zhí)行完之后才再將搜索結(jié)果返回到主線程給頁面展示哭懈。這個(gè)邏輯會(huì)隨著用戶變更搜索內(nèi)容而繼續(xù)重復(fù)灾馒。

9.2 單個(gè)搜索任務(wù)應(yīng)支持并行執(zhí)行

雖然現(xiàn)在不同搜索任務(wù)已經(jīng)支持并行執(zhí)行,但是不同業(yè)務(wù)的數(shù)據(jù)量和搜索邏輯差別很大遣总,數(shù)據(jù)量大或者搜索邏輯復(fù)雜的任務(wù)耗時(shí)會(huì)很久睬罗,這樣還不能充分發(fā)揮手機(jī)的并行處理能力。

我們還可以將并行處理能力引入單個(gè)搜索任務(wù)內(nèi)旭斥,這里有兩種處理方式:

1)對于搜索數(shù)據(jù)量大的業(yè)務(wù)(比如聊天記錄搜索):可以將索引數(shù)據(jù)均分存儲到多個(gè)FTS索引表(注意這里不均分的話還是會(huì)存在短板效應(yīng))容达,這樣搜索時(shí)可以并行搜索各個(gè)索引表,然后匯總各個(gè)表的搜索結(jié)果垂券,再進(jìn)行統(tǒng)一排序花盐。這里拆分的索引表數(shù)量既不能太多也不能太少,太多會(huì)超出手機(jī)實(shí)際的并行處理能力菇爪,也會(huì)影響其他搜索任務(wù)的性能算芯,太少又不能充分利用并行處理能力。以前微信用了十個(gè)FTS表存儲聊天記錄索引凳宙,現(xiàn)在改為使用四個(gè)FTS表熙揍。

2)對于搜索邏輯復(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ì)成比例下降作郭。

9.3 搜索任務(wù)應(yīng)支持中斷

用戶在搜索框持續(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è)限制:

1)從數(shù)據(jù)庫讀取所有結(jié)果之后再排序:我們可以在讀取結(jié)果時(shí)將用于排序的字段一并讀出秕衙,然后在讀完所有結(jié)果之后再對所有結(jié)果執(zhí)行排序。因?yàn)榕判虻暮臅r(shí)占總搜索耗時(shí)的比例很低僵刮,加上排序算法的性能大同小異据忘,這種做法對搜索速度的影響可以忽略。

2)不能使用分段查詢:在全文搜索這個(gè)場景中搞糕,分段查詢其實(shí)是沒有什么作用的勇吊。因?yàn)榉侄尾樵兙鸵獙Y(jié)果排序,對結(jié)果排序就要遍歷所有結(jié)果窍仰,所以分段查詢并不能降低搜索耗時(shí)(除非按照FTS索引的Rowid分段查詢汉规,但是Rowid不包含實(shí)際的業(yè)務(wù)信息)。

9.4 搜索讀取內(nèi)容應(yīng)最少化

搜索時(shí)讀取內(nèi)容的量也是決定搜索耗時(shí)的一個(gè)關(guān)鍵因素。

FTS索引表實(shí)際是有多個(gè)SQLite普通表組成的针史,這其中一些表格存儲實(shí)際的倒排索引內(nèi)容晶伦,還有一個(gè)表格存儲用戶保存到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ì)很好,可以在不更改存儲內(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é)果重新分詞的操作在所難免辐怕。

9.5 優(yōu)化前后的效果對比

下面是微信各搜索業(yè)務(wù)優(yōu)化前后的搜索耗時(shí)對比:


10逼蒙、本文小結(jié)

目前iOS微信已經(jīng)將這套新全文搜索技術(shù)方案全量應(yīng)用到聊天記錄、聯(lián)系人和收藏的搜索業(yè)務(wù)中寄疏。

使用新方案之后:全文搜索的索引文件占用空間更小是牢、索引更新耗時(shí)更少、搜索速度也更快了陕截,可以說全文搜索的性能得到了全方位提升驳棱。

附錄:QQ、微信團(tuán)隊(duì)技術(shù)文章匯總

微信朋友圈千億訪問量背后的技術(shù)挑戰(zhàn)和實(shí)踐總結(jié)

騰訊技術(shù)分享:Android版手機(jī)QQ的緩存監(jiān)控與優(yōu)化實(shí)踐

微信團(tuán)隊(duì)分享:iOS版微信的高性能通用key-value組件技術(shù)實(shí)踐

微信團(tuán)隊(duì)分享:iOS版微信是如何防止特殊字符導(dǎo)致的炸群、APP崩潰的?

騰訊技術(shù)分享:Android手Q的線程死鎖監(jiān)控系統(tǒng)技術(shù)實(shí)踐

iOS后臺喚醒實(shí)戰(zhàn):微信收款到賬語音提醒技術(shù)總結(jié)

微信團(tuán)隊(duì)分享:微信每日億次實(shí)時(shí)音視頻聊天背后的技術(shù)解密

騰訊團(tuán)隊(duì)分享 :一次手Q聊天界面中圖片顯示bug的追蹤過程分享

微信團(tuán)隊(duì)分享:微信Android版小視頻編碼填過的那些坑

企業(yè)微信客戶端中組織架構(gòu)數(shù)據(jù)的同步更新方案優(yōu)化實(shí)戰(zhàn)

微信團(tuán)隊(duì)披露:微信界面卡死超級bug“15。强衡。形葬。合呐。”的來龍去脈

QQ 18年:解密8億月活的QQ后臺服務(wù)接口隔離技術(shù)

月活8.89億的超級IM微信是如何進(jìn)行Android端兼容測試的

微信后臺基于時(shí)間序的海量數(shù)據(jù)冷熱分級架構(gòu)設(shè)計(jì)實(shí)踐

微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信的臃腫之困與模塊化實(shí)踐之路

微信后臺團(tuán)隊(duì):微信后臺異步消息隊(duì)列的優(yōu)化升級實(shí)踐分享

微信團(tuán)隊(duì)原創(chuàng)分享:微信客戶端SQLite數(shù)據(jù)庫損壞修復(fù)實(shí)踐

騰訊原創(chuàng)分享(一):如何大幅提升移動(dòng)網(wǎng)絡(luò)下手機(jī)QQ的圖片傳輸速度和成功率

微信新一代通信安全解決方案:基于TLS1.3的MMTLS詳解

微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺斌弦裕活實(shí)戰(zhàn)分享(網(wǎng)絡(luò)碧适担活篇)

微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡(演講全文)

微信海量用戶背后的后臺系統(tǒng)存儲架構(gòu)(視頻+PPT) [附件下載]

微信異步化改造實(shí)踐:8億月活、單機(jī)千萬連接背后的后臺解決方案

微信朋友圈海量技術(shù)之道PPT [附件下載]

微信對網(wǎng)絡(luò)影響的技術(shù)試驗(yàn)及分析(論文全文)

架構(gòu)之道:3個(gè)程序員成就微信朋友圈日均10億發(fā)布量[有視頻]

快速裂變:見證微信強(qiáng)大后臺架構(gòu)從0到1的演進(jìn)歷程(一)

微信團(tuán)隊(duì)原創(chuàng)分享:Android內(nèi)存泄漏監(jiān)控和優(yōu)化技巧總結(jié)

Android版微信安裝包“減肥”實(shí)戰(zhàn)記錄

iOS版微信安裝包“減肥”實(shí)戰(zhàn)記錄

移動(dòng)端IM實(shí)踐:iOS版微信界面卡頓監(jiān)測方案

微信“紅包照片”背后的技術(shù)難題

移動(dòng)端IM實(shí)踐:iOS版微信小視頻功能技術(shù)方案實(shí)錄

移動(dòng)端IM實(shí)踐:Android版微信如何大幅提升交互性能(一)

移動(dòng)端IM實(shí)踐:實(shí)現(xiàn)Android版微信的智能心跳機(jī)制

移動(dòng)端IM實(shí)踐:谷歌消息推送服務(wù)(GCM)研究(來自微信)

移動(dòng)端IM實(shí)踐:iOS版微信的多設(shè)備字體適配方案探討

IPv6技術(shù)詳解:基本概念源织、應(yīng)用現(xiàn)狀翩伪、技術(shù)實(shí)踐(上篇)

手把手教你讀取Android版微信和手Q的聊天記錄(僅作技術(shù)研究學(xué)習(xí))

微信技術(shù)分享:微信的海量IM聊天消息序列號生成實(shí)踐(算法原理篇)

社交軟件紅包技術(shù)解密(一):全面解密QQ紅包技術(shù)方案——架構(gòu)微猖、技術(shù)實(shí)現(xiàn)等

社交軟件紅包技術(shù)解密(二):解密微信搖一搖紅包從0到1的技術(shù)演進(jìn)

社交軟件紅包技術(shù)解密(三):微信搖一搖紅包雨背后的技術(shù)細(xì)節(jié)

社交軟件紅包技術(shù)解密(四):微信紅包系統(tǒng)是如何應(yīng)對高并發(fā)的

社交軟件紅包技術(shù)解密(五):微信紅包系統(tǒng)是如何實(shí)現(xiàn)高可用性的

社交軟件紅包技術(shù)解密(六):微信紅包系統(tǒng)的存儲層架構(gòu)演進(jìn)實(shí)踐

社交軟件紅包技術(shù)解密(十一):解密微信紅包隨機(jī)算法(含代碼實(shí)現(xiàn))

IM開發(fā)寶典:史上最全谈息,微信各種功能參數(shù)和邏輯規(guī)則資料匯總

微信團(tuán)隊(duì)分享:微信直播聊天室單房間1500萬在線的消息架構(gòu)演進(jìn)之路

企業(yè)微信的IM架構(gòu)設(shè)計(jì)揭秘:消息模型、萬人群凛剥、已讀回執(zhí)侠仇、消息撤回等

(本文同步發(fā)布于:http://www.52im.net/thread-3839-1-1.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市犁珠,隨后出現(xiàn)的幾起案子逻炊,更是在濱河造成了極大的恐慌,老刑警劉巖犁享,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件余素,死亡現(xiàn)場離奇詭異,居然都是意外死亡炊昆,警方通過查閱死者的電腦和手機(jī)桨吊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凤巨,“玉大人视乐,你說我怎么就攤上這事「易拢” “怎么了佑淀?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵,是天一觀的道長彰檬。 經(jīng)常有香客問我伸刃,道長,這世上最難降的妖魔是什么逢倍? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任奕枝,我火速辦了婚禮,結(jié)果婚禮上瓶堕,老公的妹妹穿的比我還像新娘隘道。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布谭梗。 她就那樣靜靜地躺著忘晤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪激捏。 梳的紋絲不亂的頭發(fā)上设塔,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天,我揣著相機(jī)與錄音远舅,去河邊找鬼闰蛔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛图柏,可吹牛的內(nèi)容都是我干的序六。 我是一名探鬼主播,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼蚤吹,長吁一口氣:“原來是場噩夢啊……” “哼例诀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起裁着,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤繁涂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后二驰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扔罪,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年桶雀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了矿酵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,973評論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡背犯,死狀恐怖坏瘩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漠魏,我是刑警寧澤倔矾,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站柱锹,受9級特大地震影響哪自,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜禁熏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一壤巷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞧毙,春花似錦胧华、人聲如沸寄症。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽有巧。三九已至,卻和暖如春悲没,著一層夾襖步出監(jiān)牢的瞬間篮迎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工示姿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留甜橱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓栈戳,卻偏偏與公主長得像岂傲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子荧琼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內(nèi)容