Simple: SQLite3 中文結巴分詞外掛

茶樹發表於2021-02-21

一年前開發 simple 分詞器,實現了微信在兩篇文章中描述的,基於 SQLite 支援中文和拼音的搜尋方案。具體背景參見這篇文章。專案釋出後受到了一些朋友的關注,後續也釋出了一些改進,提升了專案易用性。

最近重新體驗微信客戶端搜尋功能,發現對於中文的搜尋已經不是基於單字命中,而是更精準的基於片語。比如搜尋“法國”,之前如果句子中有“法”和“國”兩個字時也會命中,所以如果一句話裡包含“國法”就會被命中,但是這跟“法國”沒有任何關係。

本文描述對 simple 分詞器新增的基於片語命中的實現,從而實現更好的查詢效果。另外本文也會基於之前在 issue 中大家提到的問題,提供一個怎麼使用 SQLite FTS 表的建議。

背景

先簡單回顧一下之前的實現,因為結巴分詞只跟中文有關,所以本文會略去拼音的部分。

搜尋主要分為兩部分,建立索引和命中索引。為了實現中文的搜尋,我們先把句子按照單字拆分,按照單字建立索引;然後對於使用者的輸入,也同樣按照單字拆分,這樣 query 就能命中索引了。為了支援片語搜尋,再按照單字拆分就很難滿足需求了,所以可以考慮的方案是要麼改索引,要麼改 query。如果改索引的話會有一些問題,比如如果使用者就輸入了一個字比如“國”,但是我們建索引的時候把“法國”放到了一起,那“國”字就命中不了了,所以最好是保持單字索引不變,通過改寫 query 來達到檢索片語的效果。

實現

simple 分詞器之前提供了一個 simple_query() 函式來幫助使用者生成 query,我們也可以加一個新的函式來實現片語的功能。經過簡單的調研,我們發現 cppjieba 用 C++ 實現了結巴分詞的功能,很適用於我們的需求。

所以我實現了一個新的函式叫做 jieba_query() ,它的使用方式跟 simple_query() 一樣,內部實現時,我們會先使用 cppjieba 對輸入進行分詞,再根據分詞的結果構建 SQLite3 能理解的 query ,從而實現了片語匹配的功能。具體的邏輯可以參考 這裡 。對於不需要結巴分詞功能的使用者,可以在編譯的時候使用 -DSIMPLE_WITH_JIEBA=OFF 關閉結巴分詞的功能,這樣能減少編譯檔案的大小,方便客戶端對檔案大小敏感的場景使用。

使用

本文想著重介紹一下 SQLite3 FTS5 功能使用的問題,這些問題都是有朋友在專案的 issue 中提到過的,都是非常好的問題,但是也說明有不少人對怎麼使用 FTS 表不太清楚,希望本文能解決一些疑惑。

首先第一點,FTS5 表雖然是一個虛擬表,提供了全文搜尋的功能,但是它整體還是跳不出 SQL 的範疇,所以其實很多用法和其他 SQL 表是一樣的,當然它也跳不出 SQL 的限制。比如有一個 issue 問如果表中有多列的時候,能不能檢索全表,但是隻返回命中的那些列?答案是不行的,因為按照 SQL 的語法規則,SELECT 語句後面必須顯示說明你想要 SELECT 哪些列,所以結果列是必須使用者指定的,如果我們像知道哪些列命中了,只能通過其他一些手段,感興趣的朋友可以看這個 issue36

另外 simple 分詞器提供了不少額外的功能,比如 simple_query() 和 simple_highlight() 等輔助函式,但是它並不影響我們使用原有 FTS5 的功能,比如如果想按照相關度排序,FTS5 自帶的 order by rank 功能還是可以繼續可以使用,也就是說 FTS5 頁面 提供的所有功能都是可以和 simple 分詞器一起使用的。

最後也是最重要的一個問題,FTS5 表到底該怎麼用?有一個 issue26 提到的問題非常好,我把它放到這裡:

《微信全文搜尋優化之路》一文中針對索引表的介紹,我對索引有幾個問題想請教一下:

  1. 業務表是正常的程式的資料表,還要再為了全文搜尋再多建立一份索引表,是嗎?我直接將我的業務資料表在建立的時候按虛表建立行嗎?(例如create virtual table tablename using fts5(列名1,列名2,tokenize = 'simple'))
  2. 如果再多建立一份索引表,那是不是每一個業務表和對應的索引表的表欄位是完全相同?
  3. 如果再多建立一份索引表,那資料庫的大小是不是加倍了,尤其是把檔案或圖片影片存入資料庫的情況(BLOB型別)?
  4. 原文中【為了解決業務變化而帶來的表結構修改問題,微信把業務屬性數字化】,這也是我想要的,能否幫助貼下原文中提到的【索引表-IndexTable】和【資料表-MetaTable】的建表語句?

核心的問題是:想讓表資料支援全文搜尋,需要把資料複製一份嗎?這樣會不會導致資料庫膨脹?在使用者的手機上我們可不想佔用太多無謂的空間。

externel content table

其實這個問題在 FTS5 的官方文件中已經給出瞭解決方案那就是 externel content table,大家也可以參考 這篇文章

它的意思是我們可以建一張普通表,然後再建一張 FTS5 表來支援全文索引,這張虛擬表本身不會儲存真實的資料,如果 SELECT 語句用到具體的內容,都會通過關聯關係去原表獲取,這樣就不存在資料重複的問題了。但是這裡就會涉及到資料一致性的問題,怎麼保證原表的資料和索引表是一致的呢?通過 trigger 也就是觸發器來實現:對於原表的增刪改,都會通過觸發器同步到 FTS 表。這樣基本上就完美解決了上面使用者的問題。

可能有人會問為什麼不直接用 FTS5 表呢?這樣普通表就不用了,也省了觸發器的邏輯。原因是 FTS 表提供了全文索引的能力,但是它也有限制,對於基於 ID 或者其他普通索引的請求它是不支援的,如果我們想有一個時間列並且基於時間列索引排序,FTS表就不行,還是需要普通表。通過普通表和 FTS 表結合的方案,我們就能同時使用兩者的能力。

微信的方案

需要注意的是,微信並沒有使用上面提到的方案,而是單獨建了一張打平的索引表,把所有需要全文索引的資料放到一張單獨的表裡面,再通過外來鍵關聯到具體的業務去。這樣的好處在微信的文章中有所提及,主要是其他關聯的表結構變更的時候,FTS 表不用動,這樣很容易新增想要搜尋的欄位,只需把該欄位寫入 FTS 表及關聯關係的表就行,表結構見下圖:

wechat-fts5

個人覺得對於微信這個複雜度的業務,可以考慮這個方案,畢竟需要搜尋的資訊非常多,這樣方便各個業務複用搜尋能力。但是對於大部分的業務,用 external content table 可能是更簡單的方案,畢竟在資料寫入和讀取的時候都更快更方便,微信的方案在資料操作流程上會複雜不少,需要邏輯上做更多的封裝。

總結

上面主要介紹了 simple 分詞器最新的功能,基於結巴分詞實現基於片語的搜尋功能,實現更精準的匹配。另外也介紹了在實際專案中使用 FTS 表的方案,希望對大家有所助益。

Reference

相關文章