SQLite 全文搜索 FTS5 核心原理与实战指南

FTS5 是 SQLite 的全文搜索扩展模块(Full-Text Search version 5),是 SQLite 内置的一个虚拟表模块,用来对文本做高效的全文检索。下面从定位、原理、用法到实战细节完整讲一遍。


一、它是什么 / 解决什么问题

普通的 SQL LIKE '%关键词%' 做文本搜索有三个致命问题:

  1. 无法用索引——全表扫描,数据量大就慢。

  2. 无相关性排序——命中就命中,没有"哪条更相关"的概念。

  3. 无语言处理——不懂分词、大小写、词干、前缀。

FTS5 就是来解决这些的。它把文本分词(tokenize)后建立倒排索引(inverted index),让你可以:

  • 毫秒级检索百万级文档

  • 按 BM25 相关性 排序

  • 支持前缀查询、短语查询、布尔组合、NEAR 邻近

  • 支持高亮(snippet / highlight)

它的前身是 FTS3/FTS4,FTS5 是 2015 年随 SQLite 3.9 引入的重写版,API 更干净、性能更好、扩展性更强。


二、核心原理:倒排索引

普通表是「行 → 内容」,倒排索引是「词 → 出现在哪些行」。

  
文档1: "sqlite full text search"
  
文档2: "full text index in sqlite"
  

  
倒排索引:
  
sqlite  -> [1, 2]
  
full    -> [1, 2]
  
text    -> [1, 2]
  
search  -> [1]
  
index   -> [2]
  
in      -> [2]
  

查询 full text 时,直接取两个词的交集 [1,2],不用扫描原文。这就是它快的根本原因。

FTS5 内部存储为几张影子表(shadow tables):

| 影子表 | 作用 |

|---|---|

| xxx_data | 实际倒排索引数据(B-tree) |

| xxx_idx | 索引段元数据 |

| xxx_content | 原始内容(若用 external content 则无) |

| xxx_docsize | 每文档大小 |

| xxx_config | 配置项 |

(xxx 是你建的表名)


三、基本用法

1. 建表

  
CREATE VIRTUAL TABLE docs USING fts5(title, body);
  

这就建了一个虚拟表,title、body 两列会被全文索引。它同时也有一个隐藏的 rowid。

2. 插入 / 更新 / 删除

  
INSERT INTO docs(title, body) VALUES ('Hello', 'SQLite full text search');
  
UPDATE docs SET body = 'new content' WHERE rowid = 1;
  
DELETE FROM docs WHERE rowid = 1;
  

用法跟普通表几乎一样。

3. 查询

  
-- 匹配单个词
  
SELECT * FROM docs WHERE docs MATCH 'sqlite';
  

  
-- 多词(默认 AND)
  
SELECT * FROM docs WHERE docs MATCH 'sqlite search';
  

  
-- 短语
  
SELECT * FROM docs WHERE docs MATCH '"full text search"';
  

  
-- 前缀
  
SELECT * FROM docs WHERE docs MATCH 'sql*';
  

  
-- 布尔
  
SELECT * FROM docs WHERE docs MATCH 'sqlite OR postgres';
  
SELECT * FROM docs WHERE docs MATCH 'sqlite NOT mysql';
  

  
-- 指定列
  
SELECT * FROM docs WHERE docs MATCH 'title: hello';
  
SELECT * FROM docs WHERE docs MATCH '{title body} : search';
  

  
-- NEAR 邻近(默认距离10)
  
SELECT * FROM docs WHERE docs MATCH 'NEAR(sqlite search, 5)';
  

注意:MATCH 的左边必须是表名,不能是列名。想按列搜要用 列名: 语法。

4. 排序与辅助函数

  
-- BM25 相关性排序(越小越相关,所以 ASC)
  
SELECT *, bm25(docs) AS rank
  
FROM docs
  
WHERE docs MATCH 'sqlite'
  
ORDER BY rank;
  

  
-- 加权:title 权重 10,body 权重 1
  
SELECT * FROM docs
  
WHERE docs MATCH 'sqlite'
  
ORDER BY bm25(docs, 10.0, 1.0);
  

  
-- 高亮
  
SELECT highlight(docs, 1, '<b>', '</b>') FROM docs WHERE docs MATCH 'sqlite';
  

  
-- 摘要片段
  
SELECT snippet(docs, 1, '<b>', '</b>', '...', 16) FROM docs WHERE docs MATCH 'sqlite';
  

bm25() 的额外参数对应各列权重,顺序与建表列顺序一致。


四、分词器(Tokenizer)

这是 FTS5 最关键的可配置项,决定"怎么切词"。

内置分词器

| 分词器 | 说明 |

|---|---|

| unicode61 | 默认。按 Unicode 规则切分,支持大小写折叠、去音标 |

| ascii | 只认 ASCII 字母数字 |

| porter | 在 unicode61 基础上加英文词干还原(running→run) |

| trigram | 三元组切分,支持任意子串匹配(LIKE 加速) |

用法:

  
CREATE VIRTUAL TABLE docs USING fts5(
  
  body,
  
  tokenize = 'porter unicode61 remove_diacritics 2'
  
);
  

中文怎么办

unicode61 不认中文分词——它会把一整段中文当成一个 token(因为中文没有空格)。三种方案:

方案 A:trigram(推荐,简单够用)

  
CREATE VIRTUAL TABLE docs USING fts5(body, tokenize='trigram');
  

trigram 把文本切成每 3 个字符一组,中文英文都能搜子串,代价是索引变大、无法做前缀之外的模糊。对中文搜索是性价比最高的方案。

方案 B:自定义分词器

用 C API 或 Python 的 sqlite3 注册 fts5_tokenizer,接入 jieba 等分词库。灵活但需要写代码。

方案 C:入库前自己分好词

把中文用空格隔开后存进去,查询时同样切分。简单粗暴,可控性强——这也是很多项目实际采用的做法。


五、进阶特性

1. External Content(外部内容表)

不想让 FTS5 再存一份原文(浪费空间),可以让它指向已有表:

  
CREATE TABLE posts(id INTEGER PRIMARY KEY, title TEXT, body TEXT);
  

  
CREATE VIRTUAL TABLE posts_fts USING fts5(
  
  title, body,
  
  content='posts',        -- 原文来自 posts
  
  content_rowid='id'      -- 主键列
  
);
  

然后用触发器保持同步:

  
CREATE TRIGGER posts_ai AFTER INSERT ON posts BEGIN
  
  INSERT INTO posts_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
  
END;
  
CREATE TRIGGER posts_ad AFTER DELETE ON posts BEGIN
  
  INSERT INTO posts_fts(posts_fts, rowid, title, body)
  
  VALUES ('delete', old.id, old.title, old.body);
  
END;
  
CREATE TRIGGER posts_au AFTER UPDATE ON posts BEGIN
  
  INSERT INTO posts_fts(posts_fts, rowid, title, body)
  
  VALUES ('delete', old.id, old.title, old.body);
  
  INSERT INTO posts_fts(rowid, title, body) VALUES (new.id, new.title, new.body);
  
END;
  

这样原文只存一份,索引和原文分离。注意删除/更新时必须用 'delete' 命令,直接 DELETE 会破坏索引一致性。

2. Contentless 表

只建索引不存原文:

  
CREATE VIRTUAL TABLE docs USING fts5(body, content='');
  

省空间,但无法返回原文,也无法 UPDATE/DELETE(除非用 contentless_delete=1)。

3. 列权重与 ranking

  
-- title 更重要
  
ORDER BY bm25(docs, 5.0, 1.0)
  

4. 前缀索引

  
CREATE VIRTUAL TABLE docs USING fts5(body, prefix='2 3');
  

为前 2、3 个字符建额外索引,加速 xx* 前缀查询,代价是索引变大。

5. 自定义 ranking 函数

可以注册自己的 rank 函数替代 bm25,但一般没必要。


六、限制与坑

  1. MATCH 左值必须是表名,不是列名。

  2. 默认多词是 AND,不是 OR,很多人会踩。

  3. unicode61 不切中文,一定要换 trigram 或自定义分词。

  4. external content 表必须手动或触发器同步,否则索引和原文会漂移。

  5. delete 是特殊命令,删除时 INSERT INTO fts(fts, rowid, ...) VALUES('delete', ...)。

  6. 不支持 JOIN 优化——FTS5 表和其他表 JOIN 时,通常先跑 FTS 再关联更高效。

  7. 索引体积——trigram 尤其明显,可能是原文的 3~5 倍。

  8. rowid 与业务主键——FTS5 只有 rowid,若业务主键非整数,需要映射。


七、完整可跑示例

  
-- 建表(中文用 trigram)
  
CREATE VIRTUAL TABLE notes USING fts5(
  
  title,
  
  body,
  
  tokenize = 'trigram'
  
);
  

  
INSERT INTO notes VALUES ('SQLite 入门', 'SQLite 是一个嵌入式数据库,支持全文搜索');
  
INSERT INTO notes VALUES ('FTS5 详解', 'FTS5 是 SQLite 的全文搜索模块,基于倒排索引');
  
INSERT INTO notes VALUES ('Postgres', 'Postgres 也有全文搜索,使用 tsvector');
  

  
-- 搜"全文搜索"
  
SELECT title, snippet(notes, 1, '[', ']', '...', 20)
  
FROM notes
  
WHERE notes MATCH '全文搜索'
  
ORDER BY bm25(notes, 5.0, 1.0);
  

八、它适合谁

  • 本地优先应用:笔记、文档、邮件客户端(不需要外部搜索引擎)

  • 中小型站点:几百万文档以内,不想引入 Elasticsearch

  • 嵌入式场景:桌面软件、移动 App、CLI 工具

  • 和 AI/RAG 结合:作为关键词检索层,与向量检索做混合(hybrid search)


一句话总结

FTS5 是 SQLite 内置的倒排索引全文搜索模块,用虚拟表封装分词、索引、BM25 排序和查询语法,让你在单文件数据库里获得接近专业搜索引擎的检索能力,代价是中文需换 trigram 或自定义分词。