FTS5 是 SQLite 内置的全文搜索扩展模块(第 5 代)。它解决的问题是:LIKE '%关键词%' 搜大表要逐行扫全表,越搜越慢;FTS5 预先建好倒排索引 (Inverted Index),搜索时直接查"哪些文档含这个词",复杂度从 O(n) 降到 O(1) 级别的查找。

核心机制:倒排索引

正排是"文档 → 词",倒排是"词 → 文档列表"——就像书的正文和书末索引页的关系:

正文(正排):  job_1 = "Python 后端 开发"
              job_2 = "Java 后端 开发"
索引(倒排):  "Python" → [job_1]
              "后端"   → [job_1, job_2]   ← 查这个词不用扫正文,直接命中

代价:索引通常比原文大数倍。zhitu 项目实测:1.08 万条职位,原文(payload 25MB + FTS 存储的内容列 15MB)共约 40MB,FTS5 建出的索引却占约 160MB——中文分词后词项多、文档-词项对(cardinal pairs)多是主因。

基本用法

-- 建虚拟表(不是普通表,是 FTS5 的"虚拟表")
CREATE VIRTUAL TABLE jobs_fts USING fts5(
    title, jd_text, content='jobs', content_rowid='id'  -- 外部内容表模式,原文不重复存
);

-- 插入后用 MATCH 查询
SELECT rowid FROM jobs_fts WHERE title MATCH '"后端" AND "Python"';

-- 中文需要外置分词器(默认 unicode61 对中文是按"连续汉字串"切,粒度太粗)
-- 常见方案:simple 分词器扩展,或 jieba 预分词后入库

要点:

  • MATCH 语法:支持 AND / OR / NOT、短语匹配 "exact phrase"、前缀 prefix*、列限定 column:term
  • 中文分词是 FTS5 在中文场景的头号坑:默认 tokenizer 把整句汉字当一个 token,MATCH '后端' 只能匹配完整连续串。生产可用 wangfenjin/simple 扩展或先用 jieba 切好词再存
  • content= 外部内容模式:原文留在普通表,FTS 只存索引,省一倍空间;代价是外部表变更时要手动 INSERT INTO fts(fts, rowid, ...) VALUES('delete', ...) 同步
  • 维护:FTS 索引不自动跟随外部表,删改必须同步重建对应行,否则出现"搜得到但正文已删"的幽灵结果

zhitu 实战:为什么用它、什么场景触发

zhitu(RoleFit)的 job_sqlite.py 里,管理端职位库(1.08 万条职位)建了 job_search_fts 虚拟表,分了 7 个内容列(all_text / jd_text / tag_text / tech_stack_text 等)。

选 FTS5 的理由(相对其他方案):

  1. 量级匹配:1 万条、单文件 200MB,SQLite 单机绰绰有余,不需要 Elasticsearch(要多跑一个 JVM 服务,3.6G 内存的小服务器养不起)
  2. 零部署:FTS5 内置于 SQLite,Python 标准库 sqlite3 直接可用,和"按需查 SQLite 替代内存常驻"的内存优化方向一致
  3. 查询场景:管理端的职位筛选/导出接口(search_jobs_data),用户输入"标题含 xx、公司含 xx"这类关键词过滤——是精确词过滤,不是语义搜索,所以用全文索引,不走向量

触发条件(代码逻辑 job_sqlite.py:707-716):

  • 某个文本过滤词 len >= 3 时启用 FTS 的 MATCH 子查询(短词走 LIKE,避免短词倒排命中过多反而慢)
  • FTS 表存在且至少有一个过滤词 → s.ordinal + 1 IN (SELECT rowid FROM job_search_fts WHERE {column} MATCH ?)
  • 否则回退 LIKE '%..%' 慢路径

和向量检索的分工(两套互不相干的检索):

FTS5(关键词)Qdrant(向量)
查什么职位文本,精确词过滤4867 个标签的 2048 维向量,语义相似召回
谁在用管理端筛选/导出学生简历 ↔ 职位匹配
相似性依据字面相同语义相近("前端开发" ≈ "web 工程师")

相关概念


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Algorithms