小站做站内搜索不需要 Elasticsearch,也不需要任何搜索服务。一个文件、一张表、几万条文字,SQLite 的 FTS5 就已经够用,而且快到超出预期。
场景很朴素:站点要能同时搜标题和正文,数据量几万条,但要求「打字即出结果」,不能有肉眼可见的卡顿。
一、先从最土的办法说起
一开始我用的是 LIKE 模糊匹配,写法简单粗暴:
SELECT id, title FROM article WHERE body LIKE '%' || ? || '%';
数据量涨到几万行之后,这条查询稳定在 56ms 左右。单个数字看着不大,可它发生在每次按键之后,用户能明显感觉到慢半拍。而且前导通配符根本用不上索引,数据库只能老老实实全表扫描。
二、换成 FTS5 虚拟表
做法是单独建一张全文索引虚拟表,原始数据表照旧不动,两者用 rowid 对应:
CREATE VIRTUAL TABLE article_fts USING fts5( title, body, content='article', content_rowid='id' ); INSERT INTO article_fts(rowid, title, body) SELECT id, title, body FROM article;
查询改成匹配语法,再顺手取一段带高亮的摘要:
SELECT rowid,
snippet(article_fts, 1, '<em>', '</em>', '…', 12)
FROM article_fts
WHERE article_fts MATCH ?
ORDER BY rank
LIMIT 20;
同样一条「打字即出」的查询,从 56ms 掉到 0.29ms。要注意这是实测的方法而不是估的:每次测量前清掉页缓存避免假快,并且跑 100 次取中位数,而不是只跑一次挑最好看的成绩。
三、几个真会绊人的坑
- 中文分词:FTS5 默认按空格切词,一整句中文会被当成一个词,需要换用按字切分的分词器,否则搜「搜索」匹配不到「搜索引擎」。
- 前缀匹配:MATCH 的查询串要自己拼星号,写成「关键词*」才能命中词头,用户打到一半也能出结果。
- 主表同步:索引表和主表必须保持一致,增删改都要跟着写一次。最稳妥的方式是挂触发器,而不是在业务代码里到处补漏。
结论也是笨的:能不用 LIKE 就不用 LIKE,能上索引就上索引,剩下的交给数据库自己。