SQLite FTS3/FTS4與一些使用心得
此文已由作者王攀授權網易雲社區發布。
歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。
簡介
對於今天的移動、桌面客戶端應用而言,離線全文檢索的需求已經十分強烈,我們日常使用的郵件客戶端、雲音樂、雲筆記、易信等就是離線全文檢索的潛在用戶。
作為目前使用最為廣泛的嵌入式數據庫,SQLite3其實內置了全文檢索的擴展模塊——FTS。FTS分為FTS1、FTS2、FTS3、FTS4和FTS5幾個版本,其中FTS1和FTS2已經被廢棄,而FTS3在2007年9月4日發布的SQLite 3.5.0中被引入,其增強版FTS4則第一次出現在2010年12月8日的SQLite 3.7.4中。由於FTS3與FTS4有著千絲萬縷的聯系,所以本文將兩種FTS引擎放在一起來介紹。FTS5則它們不兼容,所以筆者將以另外一個文章來單獨作介紹。
相比於普通表,FTS3/FTS4其實是兩種虛表。當你創建一個名為t的FTS虛表的時候,你會發現數據庫中其實創建了若幹個普通表用於存儲物理數據,它們被稱為影子表(shadow tables),分別命名為t_content、t_messageize、t_segdir、t_segments、t_stat等。
編譯
想讓SQLite支持FTS3/FTS4,在編譯SQLite的時候需要打開以下編譯開關
-DSQLITE_ENABLE_FTS3
註:Chromium、CEF和iOS7及以後的版本內建的SQLite都默認打開了此選項。
如果想要讓FTS3/FTS4支持帶括號優先級的高級查詢(見下文),那麽需要同時打開以下開關:
-DSQLITE_ENABLE_FTS3_PARENTHESIS
註:Chromium、CEF內建SQLite沒有打開該開關。
如果想要讓FTS3/FTS4支持ICU分詞器,則需要再打開以下開關:
-DSQLITE_ENABLE_\ICU
註:Chromium、CEF內建SQLite打開並實現了該開關;iOS自帶的沒有。
表操作
最簡單地創建表的形式:
-- 創建一個fts3表message,包含title和body兩列CREATE VIRTUAL TABLE message USING fts3(title, body);-- 創建一個fts4表message,包含title和body兩列CREATE VIRTUAL TABLE message USING fts4(title, body);
需要註意的是如果在創建表的時候給某個列指定了類型,那麽這些類型將被完全忽略。 我們還可以在建表的時候給表指定分詞器。例如:
CREATE VIRTUAL TABLE message USING fts3(title, body, tokenize=porter);
以上創建了一個使用porter分詞器的表。此外FTS3/FTS4還支持simple、unicode61和外置的ICU等分詞器。對於中文,我們建議使用ICU分詞器。此外,FTS3/FTS4還支持自定義的分詞器,筆者將在之後介紹FTS5的文章中以FTS5為例介紹自定義分詞器。
創建FTS4表的時候我們還可以使用一些特殊選項:
compress=、uncompress= 用於支持壓縮和解壓縮
content= 用於創建無正文表(只有索引)和外部正文表(正文來自其他表而非虛表本身)等
matchinfo= 用於以FTS3方式存儲FTS4,忽略FTS4額外所需的信息,但是功能也會因此受限
notindexed= 指定某個列為非索引列
prefix= 額外為指定自己的前綴創建索引,這可以加快前綴查詢(見後文)
刪除FTS表非常簡單,實用DROP語句即可。
增刪改
要向FTS表中插入數據類似普通表:
INSERT INTO message(title, body) VALUES(‘警告‘, ‘10086提醒您:您移動卡上余額不足10元‘); INSERT INTO message(docid, title, body) VALUES(2, ‘警告‘, ‘10086提醒您:您移動卡上余額不足5元‘);
註意到第二句中我們指定了一個叫docid的列,這是隱藏列rowid的一個別名,類似於普通表。
更新和刪除和普通表無異:
UPDATE message SET title = ‘提示‘ WHERE rowid = 1;DELETE FROM message WHERE rowid = 1;
查詢
查詢操作是FTS表存在的最大意義。兩類查詢在FTS表上是比較高效的,它們是:
僅包含rowid的普通查詢
全文檢索
SELECT * FROM message WHERE rowid = 1 SELECT * FROM message WHERE body MATCH ‘10086‘
下面以ICU為分詞器針對全文檢索進行進一步介紹。
詞查詢
查詢可以針對整個文檔或者文檔的某些列來進行精確的詞查詢:
-- 查詢包含“移動”關鍵字的文檔SELECT * FROM message WHERE message MATCH ‘移動‘-- 查詢消息體包含“移動”關鍵字的文檔SELECT * FROM message WHERE body MATCH ‘移動‘SELECT * FROM message WHERE message MATCH ‘body:移動‘-- 查詢消息體包含“移動”且文檔中包含“您”關鍵字的文檔SELECT * FROM message WHERE message MATCH ‘body:移動 您‘
註意到,用“列名:詞”的方式可以指定在某個列上查詢,而用空格隔開可以以“且”的方式連接多個條件。
在FTS4下,在詞前面加入^,表示該詞必須是某個列的第一個詞:
SELECT * FROM message WHERE message MATCH ‘body:^移動‘
特別需要註意的是:英文詞必須使用小寫。因為後文中很多關鍵字需要用它們的大寫身份來被識別。
前綴查詢
我們在詞後面加入一個星號(*)即構成以該詞為前綴的查詢:
-- 下面的查詢包含“移動”的文檔會被命中SELECT * FROM message WHERE message MATCH ‘移*‘
在FTS4下,^同樣適用於前綴查詢。
短語查詢
如果我們給定一個由詞和前綴組成的有序序列,去數據庫中匹配一個連續的有序詞序列,使得兩個序列中詞/前綴逐個依序匹配,就構成了短語查詢。
-- 下面的查詢將匹配以上兩條記錄SELECT * FROM message WHERE message MATCH ‘"移 動"‘;-- 下面的查詢將無法匹配,因為原文中“移”出現在“動”之前而查詢中則相反SELECT * FROM message WHERE message MATCH ‘"動 移"‘;-- 下面的查詢將無法匹配,因為“移”、“卡”之間隔了一個“動”SELECT * FROM message WHERE message MATCH ‘"移 卡"‘;
註意短語查詢必須將有序詞/前綴集用雙引號引起來,並且將有序集內每個詞用空格隔開。
NEAR查詢
短語查詢要求詞之間必須連續重現,但是有時候我們允許他們就近出現,這個時候就需要使用NEAR查詢。
SELECT * FROM message WHERE message MATCH ‘"移 NEAR 動"‘;
默認情況下兩個詞允許最大間隔10個詞,但是你也可以自定義:
SELECT * FROM message WHERE message MATCH ‘"移 NEAR/6 動"‘;
上例最多允許“移”、“動”之間出現6個詞。
邏輯操作
FTS3、FTS4支持邏輯條件關鍵字(必須大寫):
AND:邏輯與,取交集;默認不加條件關鍵字的情況下,就是這種關系。
OR:邏輯或,取並集
NOT:邏輯非,取補集。可以使用 - 代替
-- 以下兩個查詢是一致的SELECT * FROM message WHERE message MATCH ‘移 AND 動‘;SELECT * FROM message WHERE message MATCH ‘移 動‘;
優先級方面,NOT高於AND,高於OR。FTS3/FTS4支持使用括號來改變的優先級:
SELECT * FROM message WHERE message MATCH ‘(移 OR 動) AND 卡‘;
再次提醒:使用帶括號優先級的查詢支持,需要打開 -DSQLITE_ENABLE_FTS3_PARENTHESIS 開關編譯SQLite
內建函數
FTS3/FTS4支持三個非常有用的內建函數:offsets、snippet、matchinfo。
offsets
offsets函數返回所有匹配項的偏移信息。總體上來說,offsets針對每個匹配項將返回一個四元組,一句話概括就是:詞號為term的詞在表中第column列的offset字節處命中了連續的size字節的目標。offsets返回所有這樣的四元組的文本形式,例如若:
SELECT offsets(tb1) FROM tb1 WHERE tb1 MATCH ‘term1 term2‘;
返回
"0 1 3 4 1 0 0 6"
那麽就表示有兩處被命中:
第1列的3字節處被2號詞命中,命中長度為4
第2列的0字節處被1號詞命中,命中長度為6
註意:column、term、offset編號都從0開始的。
snippet
此函數用於返回最佳命中目標及其周圍的切片。例如,SQLite官網的搜索功能的高亮顯示就是用此函數實現的。
這個函數支持可變參數,我們可以給它傳1至6個參數。6個參數按照從0開始編號說明如下: 0:必須使用隱藏列,也就是要查詢的虛表名,比如上面的message。
1:返回值中被命中目標開始處的標記文本,默認為“”
2:返回值中被命中目標結束處的標記文本,默認為“”
3:被省略文本的標識,比如“...”
4:強制指定從哪個列提取切片文本,默認為-1,表示可從任意列提取
5:此值的絕對值表示返回值中大致包含多少個單詞,最大可取64,默認-15
SELECT snippet(message, ‘[ ‘]‘, ‘...‘) FROM message WHERE message MATCH ‘"移* 余*"‘
matchinfo
這是一個更加高效的函數,因為它本身的返回值不需要將整個行全部從磁盤調入內存而只需要查詢索引數據。此外,這個函數也提供了足夠的用於運行常用結果評價算法的信息。
限於篇幅,本函數不作展開詳述,大家可以參考最後給出的鏈接查閱。
常用特殊命令
FTS3/FTS4支持一些特殊命令來維護索引等。下面是我們會常用的兩條:
-- 優化表,本質是將所有獨立的小索引樹合並成一整棵B樹
INSERT INTO xyz(xyz) VALUES(‘optimize‘);
-- 重建索引
INSERT INTO xyz(xyz) VALUES(‘rebuild‘);
FTS3與FTS4的區別
FTS3和FTS4是比較相似的,它們共享了很多底層技術,也共享了相同的接口。它們的不同點在於:
FTS4包含了查詢優化,可以顯著提升高頻詞的檢索性能
FTS4下matchinfo()內建函數得到更多的可選信息
FTS4為了實現1中提到的優點,需要額外的存儲空間,不過一般情況下這部分空間開銷比較小
FTS4支持hooks來實現壓縮存儲以減小磁盤開銷
優化建議
控制範圍:我們真的必要返回所有結果麽?是否可以考慮按區間分批返回呢?
考慮matchinfo:有些時候我們只需要返回部分查詢結果的偏移量或者片段,這個時候我們可以考慮先用帶matchinfo的子查詢確定我們需要返回偏移量或片段的rowid集,然後再對這個集合內的記錄進行深度的offsets或者snippet。因為offsets和snippet需要從磁盤調取整行數據,並作一定的字符串加工,效率較低。這方面大家可以讀下SQLite源碼。
考慮外部正文:如果你需要索引的內容完全可以從一個必要的外部表中獲取,不妨考慮下外部正文。這樣就可以有效減小存儲正文所需要的磁盤和時間開銷。遺憾的是,通過提取iOS版QQ郵箱某個版本的數據文件,我們發現QQ郵箱這方面似乎做得不太好。
我們的困擾
FTS3/FTS4是好東西,但在實際項目中我們發現它們無法完全滿足我們的需求:
查詢語法過於模糊,容易產生歧義,搜索結果不可控
內建函數可定制性不夠
offsets返回值為字符串,多次realloc和字符串轉換,效率太低
一定情況下會更費內存
過時,采用FTS5後未來需要升級數據庫
於是我們找到了替代它們的神器——FTS5!結合我們自定義的分詞器(代號mmfts5),需求終於被完全滿足了。
參考
http://www.sqlite.org/fts3.html
網易雲免費體驗館,0成本體驗20+款雲產品!
更多網易技術、產品、運營經驗分享請點擊。
相關文章:
【推薦】 一個內部增長案例的分享
【推薦】 [5.19 線下活動]Docker Meetup杭州站—擁抱Kubernetes,容器深度實踐
【推薦】 6本互聯網技術暢銷書免費送(數據分析、深度學習、編程語言)!
SQLite FTS3/FTS4與一些使用心得