天天看點

Simple: SQLite3 中文結巴分詞插件

SQLite3 中使用結巴分詞實作更精準中文搜尋

一年前開發 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 表及關聯關系的表就行,表結構見下圖:

Simple: SQLite3 中文結巴分詞插件

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

總結

上面主要介紹了 simple 分詞器最新的功能,基于結巴分詞實作基于詞組的搜尋功能,實作更精準的比對。另外也介紹了在實際項目中使用 FTS 表的方案,希望對大家有所助益。

Reference

  • Simple 分詞器: https://github.com/wangfenjin/simple
  • sqlite 官方文檔:https://www.sqlite.org/fts5.html
  • 微信全文搜尋優化之路:https://cloud.tencent.com/developer/article/1006159
  • 微信移動端的全文檢索多音字問題解決方案:https://cloud.tencent.com/developer/article/1198371
  • Simple: 一個支援中文和拼音搜尋的 sqlite fts5插件:https://www.wangfenjin.com/posts/simple-tokenizer/
  • Full Text Search With Sqlite SQLite:https://kimsereylam.com/sqlite/2020/03/06/full-text-search-with-sqlite.html