LIKE 撑不住大数据量
如果你以前在 SQLite 里搜索过文本,大概用过 LIKE '%word%'。它在小表上能用,到了大表就不行了。没有任何索引能帮上忙:SQLite 必须扫描每一行,转成小写,再检查子字符串。单词边界、排序、多词查询和前缀匹配,全都得你自己实现。
FTS5 就是内置的解决方案。它是一种虚拟表类型,会为你的文本列维护倒排索引,支持一套小型查询语言,并用 BM25 给结果排序。它默认随 SQLite 一起提供,不需要安装扩展。
创建 FTS5 表
用 CREATE VIRTUAL TABLE ... USING fts5(...) 创建 FTS5 表,并列出要建立索引的文本列:
有三点值得注意。这些列没有类型:FTS5 把一切都当作文本。MATCH 运算符作用于表名(posts MATCH ...),而不是某一列。另外查询不区分大小写并且经过分词,所以 'sqlite' 能在任意一行中找到 SQLite。
MATCH 查询语言
MATCH 接受的不只是单个词。查询字符串有自己的一套小语法:
各自的作用:
'fts5 AND prefix':两个词都必须出现(顺序不限,位置不限)。'"keep fts"':精确短语,顺序也必须一致。'trig*':前缀搜索,匹配trigger、triggers、trigonometry等。'index NOT trigger':包含index,但不包含trigger。
你还可以用 column:term 只搜索某一列,比如 'title:sqlite'。完整的语法还支持用括号分组、用 OR 表示“或”,和你对搜索引擎的预期一样。
用 BM25 排序
默认情况下,FTS5 会给每一行附加一个隐藏的 rank 列。它就是 BM25 相关度分数:数字越小,匹配度越高。按它排序,最相关的结果就会排在最前面:
想让某些列的权重比其他列更高?调用 bm25() 并传入权重,按声明顺序每列一个:
第一篇文章排在最前,因为 sqlite 出现在 title(权重 10×)中,而不只是在 body(权重 1×)中。按照你的应用真正想要的排序方式来选择权重。
让索引保持同步
最简单的 FTS5 表会自己存一份文本。对于只插入不修改的日志类数据,这没问题;但大多数应用已经有一张真实的表,希望 FTS 跟着它变化。干净的做法是使用“外部内容”FTS 表,再加上三个触发器。
content='articles' 告诉 FTS5 不要自己存储文本,需要时从 articles 表中读取。触发器把写入操作同步到 FTS 索引中。这样 articles 是唯一的数据来源,而 articles_fts 只是旁边的搜索结构。
看起来有点奇怪的 INSERT INTO articles_fts(articles_fts, ...) VALUES ('delete', ...) 是 FTS5 的命令语法,用来通知索引删除某一行。
摘要与高亮
搜索结果通常需要一段预览,并突出显示匹配的词。FTS5 为此提供了两个函数:
highlight(table, column_index, open, close)返回该列的完整文本,匹配的词元会被包裹起来。snippet(table, column_index, open, close, ellipsis, token_count)返回以匹配处为中心的一小段摘录。
列索引按声明顺序从零开始。每个搜索界面都需要的“匹配词标黄”效果,就是用它们搭出来的。
值得了解的坑
有几件事经常让人中招:
MATCH只能用在 FTS 表上。 你不能对普通列使用MATCH。如果需要在已有的表上搜索,就用上面的外部内容模式。- 别忘了按
rank排序。 不排序的话,FTS5 会按存储顺序返回行,和相关度毫无关系。 - 分词器很重要。 默认分词器(
unicode61)按 Unicode 单词边界拆分并转为小写。如果需要词干提取(run匹配running),就用porter分词器:USING fts5(body, tokenize='porter')。 - FTS5 不能容忍拼写错误。 它做的是前缀匹配,不是模糊匹配。如果你需要“你是不是要找……”这样的功能,那属于 FTS5 之上的一层。
- 无内容表(
content='')更小,但会丢失信息。 你可以搜索它们,却取不回原始文本,只能拿到 rowid。适合文本存放在别处的情况。
下一篇:窗口函数
FTS5 解决的是文本搜索。下一页介绍另一种高级查询:窗口函数,它让你计算累计总和、排名和分组分析,而不必把行折叠成聚合结果。
常见问题
SQLite 中的 FTS5 是什么?
FTS5 是 SQLite 内置的全文搜索扩展。你用 CREATE VIRTUAL TABLE ... USING fts5(...) 创建一张特殊的虚拟表,再用 MATCH 运算符查询它。它在插入时对文本分词,保存倒排索引,默认用 BM25 给结果排序。
SQLite 中 MATCH 和 LIKE 有什么区别?
LIKE 会线性扫描子字符串,而且不管单词边界。MATCH 使用 FTS5 的倒排索引,所以在大表上也很快,并且理解词元、前缀查询(term*)、布尔运算符(AND、OR、NOT)和短语搜索("exact phrase")。MATCH 只能用在 FTS 虚拟表上。
如何让 FTS5 索引与普通表保持同步?
一种做法是使用指向真实表的无内容表或外部内容 FTS5 表;另一种是创建 AFTER INSERT、AFTER UPDATE 和 AFTER DELETE 触发器,把改动同步到 FTS 表中。外部内容模式(content='posts')可以避免把文本存两份。
SQLite 中如何给全文搜索结果排序?
FTS5 提供一个隐藏的 rank 列,返回 BM25 分数(越小越好)。直接按它排序即可:ORDER BY rank。你也可以调用 bm25(table) 显式获取分数,或者传入列权重,比如 bm25(posts, 10.0, 1.0),让标题的权重高于正文。