FTS5 是 SQLite 的全文搜索扩展模块(Full-Text Search version 5),是 SQLite 内置的一个虚拟表模块,用来对文本做高效的全文检索。下面从定位、原理、用法到实战细节完整讲一遍。
一、它是什么 / 解决什么问题
普通的 SQL LIKE '%关键词%' 做文本搜索有三个致命问题:
-
无法用索引——全表扫描,数据量大就慢。
-
无相关性排序——命中就命中,没有"哪条更相关"的概念。
-
无语言处理——不懂分词、大小写、词干、前缀。
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,但一般没必要。
六、限制与坑
-
MATCH 左值必须是表名,不是列名。
-
默认多词是 AND,不是 OR,很多人会踩。
-
unicode61 不切中文,一定要换 trigram 或自定义分词。
-
external content 表必须手动或触发器同步,否则索引和原文会漂移。
-
delete是特殊命令,删除时INSERT INTO fts(fts, rowid, ...) VALUES('delete', ...)。 -
不支持 JOIN 优化——FTS5 表和其他表 JOIN 时,通常先跑 FTS 再关联更高效。
-
索引体积——trigram 尤其明显,可能是原文的 3~5 倍。
-
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 或自定义分词。