SQLite FTS3/FTS4與一些使用心得

此文已由做者王攀受權網易雲社區發佈。html

歡迎訪問網易雲社區,瞭解更多網易技術產品運營經驗。算法


簡介

對於今天的移動、桌面客戶端應用而言,離線全文檢索的需求已經十分強烈,咱們平常使用的郵件客戶端、雲音樂、雲筆記、易信等就是離線全文檢索的潛在用戶。sql

做爲目前使用最爲普遍的嵌入式數據庫,SQLite3其實內置了全文檢索的擴展模塊——FTS。FTS分爲FTS一、FTS二、FTS三、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等。bash

編譯

想讓SQLite支持FTS3/FTS4,在編譯SQLite的時候須要打開如下編譯開關函數

-DSQLITE_ENABLE_FTS3性能

注:Chromium、CEF和iOS7及之後的版本內建的SQLite都默認打開了此選項。 優化

若是想要讓FTS3/FTS4支持帶括號優先級的高級查詢(見下文),那麼須要同時打開如下開關:ui

-DSQLITE_ENABLE_FTS3_PARENTHESISspa

注: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個詞。

邏輯操做

FTS三、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是比較類似的,它們共享了不少底層技術,也共享了相同的接口。它們的不一樣點在於:

  1. FTS4包含了查詢優化,能夠顯著提高高頻詞的檢索性能

  2. FTS4下matchinfo()內建函數獲得更多的可選信息

  3. FTS4爲了實現1中提到的優勢,須要額外的存儲空間,不過通常狀況下這部分空間開銷比較小

  4. FTS4支持hooks來實現壓縮存儲以減少磁盤開銷

優化建議

控制範圍:咱們真的必要返回全部結果麼?是否能夠考慮按區間分批返回呢?

考慮matchinfo:有些時候咱們只須要返回部分查詢結果的偏移量或者片斷,這個時候咱們能夠考慮先用帶matchinfo的子查詢肯定咱們須要返回偏移量或片斷的rowid集,而後再對這個集合內的記錄進行深度的offsets或者snippet。由於offsets和snippet須要從磁盤調取整行數據,並做必定的字符串加工,效率較低。這方面你們能夠讀下SQLite源碼。

考慮外部正文:若是你須要索引的內容徹底能夠從一個必要的外部表中獲取,不妨考慮下外部正文。這樣就能夠有效減少存儲正文所須要的磁盤和時間開銷。遺憾的是,經過提取iOS版QQ郵箱某個版本的數據文件,咱們發現QQ郵箱這方面彷佛作得不太好。

咱們的困擾

FTS3/FTS4是好東西,但在實際項目中咱們發現它們沒法徹底知足咱們的需求:

查詢語法過於模糊,容易產生歧義,搜索結果不可控
內建函數可定製性不夠
offsets返回值爲字符串,屢次realloc和字符串轉換,效率過低
必定狀況下會更費內存
過期,採用FTS5後將來須要升級數據庫

因而咱們找到了替代它們的神器——FTS5!結合咱們自定義的分詞器(代號mmfts5),需求終於被徹底知足了。

參考

www.sqlite.org/fts3.html


網易雲免費體驗館,0成本體驗20+款雲產品!

更多網易技術、產品、運營經驗分享請點擊


相關文章:
【推薦】 Wireshark對HTTPS數據的解密
【推薦】 wireshark抓包分析——TCP/IP協議

相關文章
相關標籤/搜索