px 断缘技术站 notes & experiments

没有框架的小站点,怎么把搜索做到 0.3 毫秒

技术笔记 · 数据与性能 · 约 600 字

小站做站内搜索不需要 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,能上索引就上索引,剩下的交给数据库自己。

← 返回技术笔记列表 · 返回首页