在IM客戶端的使用場景中,基於本地數據的全文檢索功能扮演着重要的角色,最經常使用的好比:查找聊天記錄、聯繫人,就像下圖這樣。html
▲ 微信的聊天記錄查找功能前端
相似於IM中的聊天記錄查找、聯繫人搜索這類功能,有了全文檢索能力後,確實能大大提升內容查找的效率,否則,讓用戶手動翻找,確實下降了用戶體驗。node
本文將具體來聊聊網易雲信是如何實現IM客戶端全文檢索能力的,但願能帶給你啓發。git
李寧: 網易雲信高級前端開發工程師,負責音視頻 IM SDK 的應用開發、組件化開發及解決方案開發,對 React、PaaS 組件化設計、多平臺的開發與編譯有豐富的實戰經驗。github
IM客戶端全文檢索相關文章:算法
網易技術團隊分享的其它文章:數據庫
所謂全文檢索,就是要在大量內容中找到包含某個單詞出現位置的技術。數組
在傳統的關係型數據庫中,只能經過 LIKE 條件查詢來實現,這樣有幾個弊端:瀏覽器
咱們在 IM 的 iOS、安卓以及桌面端中都實現了基於 SQLite 等庫的本地數據全文檢索功能,可是在 Web 端和 Electron 上缺乏了這部分功能。微信
由於在 Web 端,因爲瀏覽器環境限制,能使用的本地存儲數據庫只有 IndexDB,暫不在討論的範圍內。但在 Electron 上,雖然也是內置了 Chromium 的內核,可是由於可使用 Node.js 的能力,因而乎選擇的範圍就多了一些。本文內容咱們具體以基於Electron的IM客戶端爲例,來討論全文檢索技術實現(技術思路是相通的,並不侷限於具體什麼端)。
PS: 若是你不瞭解什麼是Electron技術,讀一下這篇《快速瞭解Electron:新一代基於Web的跨平臺桌面技術》。
咱們先來具體看下該如何實現全文檢索。
要實現全文檢索,離不開如下兩個知識點:
這兩個技術是實現全文檢索的技術以及難點,其實現的過程相對比較複雜,在聊全文索引的實現前,咱們具體學習一下這兩個技術的原理。
先簡單介紹下倒排索引,倒排索引的概念區別於正排索引:
以倒排索引庫 search-index 舉個實際的例子:
在咱們的 IM 中,每條消息對象都有 idClient 做爲惟一 ID,接下來咱們輸入「今每天氣真好」,將其每一箇中文單獨分詞(分詞的概念咱們在下文會詳細分享),因而輸入變成了「今」、「天」、「天」、「氣」、「真」、「好」。再經過 search-index 的 PUT 方法將其寫入庫中。
最後看下上述例子存儲內容的結構:
如是圖所示: 能夠看到倒排索引的結構,key 是分詞後的單箇中文、value 是包含該中文消息對象的 idClient 組成的數組。
固然: search-index 除了以上這些內容,還有一些其餘內容,例如 Weight、Count 以及正排的數據等,這些是爲了排序、分頁、按字段搜索等功能而存在的,本文就再也不細細展開了。
分詞就是將原先一條消息的內容,根據語義切分紅多個單字或詞句,考慮到中文分詞的效果以及須要在 Node 上運行,咱們選擇了 Nodejieba 做爲基礎分詞庫。
如下是 jieba 分詞的流程圖:
以「去北京大學玩」爲例,咱們選擇其中最爲重要的幾個模塊分析一下。
jieba 分詞會在初始化時先加載詞典,大體內容以下:
接下來會根據該詞典構建前綴詞典,結構以下:
其中: 「北京大」做爲「北京大學」的前綴,它的詞頻是0,這是爲了便於後續構建 DAG 圖。
DAG 圖是 Directed Acyclic Graph 的縮寫,即有向無環圖。
基於前綴詞典,對輸入的內容進行切分。
其中:
如此,能夠獲得每一個字做爲前綴詞的切分方式。
其 DAG 圖以下圖所示:
以上 DAG 圖的全部路徑以下:
去/北/京/大/學/玩
去/北京/大/學/玩
去/北京/大學/玩
去/北京大學/玩
由於每一個節點都是有權重(Weight)的,對於在前綴詞典裏的詞語,它的權重就是它的詞頻。所以咱們的問題就是想要求得一條最大路徑,使得整個句子的權重最高。
這是一個典型的動態規劃問題,首先咱們確認下動態規劃的兩個條件。
1)重複子問題:
對於節點 i 和其可能存在的多個後繼節點 j 和 k:
即對於擁有公共前驅節點 i 的 j 和 k,須要重複計算到達 i 路徑的權重。
2)最優子結構:
設整個句子的最優路徑爲 Rmax,末端節點爲 x,多個可能存在的前驅節點爲 i、j、k。
獲得公式以下:
Rmax = max(Rmaxi, Rmaxj, Rmaxk) + W(x)
因而問題變成了求解 Rmaxi、Rmaxj 以及 Rmaxk,子結構裏的最優解便是全局最優解的一部分。
如上,最後計算得出最優路徑爲「去/北京大學/玩」。
對於未登錄詞,jieba 分詞采用 HMM(Hidden Markov Model 的縮寫)模型進行分詞。
它將分詞問題視爲一個序列標註問題,句子爲觀測序列,分詞結果爲狀態序列。
jieba 分詞做者在 issue 中提到,HMM 模型的參數基於網上能下載到的 1998 人民日報的切分語料,一個 MSR 語料以及本身收集的 TXT 小說、用 ICTCLAS 切分,最後用 Python 腳本統計詞頻而成。
該模型由一個五元組組成,並有兩個基本假設。
五元組:
基本假設:
狀態值集合即_{ B: begin, E: end, M: middle, S: single }_,表示每一個字所處在句子中的位置,B 爲開始位置,E 爲結束位置,M 爲中間位置,S 是單字成詞。
觀察值集合就是咱們輸入句子中每一個字組成的集合。
狀態初始機率代表句子中的第一個字屬於 B、M、E、S 四種狀態的機率,其中 E 和 M 的機率都是0,由於第一個字只可能 B 或者 S,這與實際相符。
狀態轉移機率代表從狀態 1 轉移到狀態 2 的機率,知足齊次性假設,結構能夠用一個嵌套的對象表示:
P = {
B: {E: -0.510825623765990, M: -0.916290731874155},
E: {B: -0.5897149736854513, S: -0.8085250474669937},
M: {E: -0.33344856811948514, M: -1.2603623820268226},
S: {B: -0.7211965654669841, S: -0.6658631448798212},
}
P['B']['E'] 表示從狀態 B 轉移到狀態 E 的機率(結構中爲機率的對數,方便計算)爲 0.6,同理,P['B']['M'] 表示下一個狀態是 M 的機率爲 0.4,說明當一個字處於開頭時,下一個字處於結尾的機率高於下一個字處於中間的機率,符合直覺,由於二個字的詞比多個字的詞要更常見。
狀態發射機率代表當前狀態,知足觀察值獨立性假設,結構同上,也能夠用一個嵌套的對象表示:
P = {
B: {'突': -2.70366861046, '肅': -10.2782270947, '適': -5.57547658034},
M: {'要': -4.26625051239, '合': -2.1517176509, '成': -5.11354837278},
S: {……},
E: {……},
}
P['B']['突'] 的含義就是狀態處於 B,觀測的字是「突」的機率的對數值等於 -2.70366861046。
最後,經過 Viterbi 算法,輸入觀察值集合,將狀態初始機率、狀態轉移機率、狀態發射機率做爲參數,輸出狀態值集合(即最大機率的分詞結果)。關於 Viterbi 算法,本文再也不詳細展開,有興趣的讀者能夠自行查閱。
上節中介紹的全文檢索這兩塊技術,是咱們架構的技術核心。基於此,咱們對IM 的 Electron 端技術架構作了改進。如下將詳細介紹之。
考慮到全文檢索只是 IM 中的一個功能,爲了避免影響其餘 IM 的功能,而且能更快的迭代需求,因此採用了以下的架構方案。
架構圖以下:
如上圖所示,右邊是以前的技術架構,底層存儲庫使用了 indexDB,上層有讀寫兩個模塊。
讀寫模塊的具體做用是:
那麼,當數據量大的時候,查詢的速度是很是緩慢的。
左邊是加入了分詞以及倒排索引數據庫的新的架構方案,這個方案不會對以前的方案有任何影響,只是在以前的方案以前加了一層。
如今,讀寫模塊的工做邏輯:
該方案有如下4個優勢:
針對上述第「3)」點: 當 indexDB 寫入數據時,會自動通知到倒排索引庫的寫模塊,將消息內容分詞後,插入到存儲隊列當中,最後依次插入到倒排索引數據庫中。當須要全文檢索時,經過倒排索引庫的讀模塊,能快速找到對應關鍵字的消息對象的 idClient,根據 idClient 再去 indexDB 中找到消息對象並返回。
針對上述第「4)」點: 它暴露出一個高階函數,包裹 IM 並返回新的通過繼承擴展的 IM,由於 JS 面向原型的機制,在新的 IM 中不存在的方法,會自動去原型鏈(即老的 IM)當中查找,所以,使得插件能夠聚焦於自身方法的實現上,而且不須要關心 IM 的具體版本,而且插件支持自定義分詞函數,知足不一樣用戶不一樣分詞需求的場景
使用瞭如上架構後,通過咱們的測試,在數據量 20W 的級別上,搜索時間從最開始的十幾秒降到一秒內,搜索速度快了 20 倍左右。
本文中,咱們便基於 Nodejieba 和 search-index 在 Electron 上實現了IM聊天消息的全文檢索,加快了聊天記錄的搜索速度。
固然,後續咱們還會針對如下方面作更多的優化,好比如下兩點:
1)寫入性能 : 在實際的使用中,發現當數據量大了之後,search-index 依賴的底層數據庫 levelDB 會存在寫入性能瓶頸,而且 CPU 和內存的消耗較大。通過調研,SQLite 的寫入性能相對要好不少,從觀測來看,寫入速度只與數據量成正比,CPU 和內存也相對穩定,所以,後續可能會考慮用將 SQLite 編譯成 Node 原生模塊來替換 search-index。
2)可擴展性 : 目前對於業務邏輯的解耦還不夠完全。倒排索引庫當中存儲了某些業務字段。後續能夠考慮倒排索引庫只根據關鍵字查找消息對象的 idClient,將帶業務屬性的搜索放到 indexDB 中,將倒排索引庫與主業務庫完全解耦。
以上,就是本文的所有分享,但願個人分享能對你們有所幫助。(本文已同步發佈於:www.52im.net/thread-3651…)