第 6 / 21 篇

第5章 用 SQLite FTS5 建立词面基线

一个可解释的词面基线有两个作用:它可以直接解决编号、产品名与制度关键词查询,也可以帮助区分模型收益来自哪里。假如系统连“退款政策”这样的标题词都找不到,应先检查解析和索引,再考虑换模型。

中文词项显式产生

SQLite FTS5 的默认 Unicode 分词器不会自动理解本书需要的中文词边界。随书代码在进入 FTS5 前先生成词项:ASCII 字母、数字与下划线组成连续词,并统一小写;连续中文生成相邻双字词项,只有一个汉字的连续段保留单字。比如“住宿发票”会生成“住宿、宿发、发票”。

这是一种便于观察的字符方案,没有领域词典,也不等同于中文词法分析。它可能召回共享局部字符的无关内容,对同义改写尤其脆弱。它的价值是规则透明、依赖少,可以作为后续表示学习的对照。

索引创建了真实虚拟表,而不是用 Python 列表模仿全文搜索:

CREATE VIRTUAL TABLE chunks_fts USING fts5(terms);

每个片段的预处理词项用空格连接写入 terms 列,FTS 行号与片段主键一致。写入、替换和删除时维护这份对应关系。文档表保存投影正文,片段表保存坐标与向量,FTS 表负责倒排召回;三者各有职责。

召回顺序与查询语法

搜索把去重后的前六十四个查询词项加引号,再用 OR 连接。整个 MATCH 表达式通过参数绑定传入 SQL;词项内部的引号也被转义。用户输入中的操作符不会直接成为任意全文查询语法,输入长度被限制为一千零二十四个码点。

参数绑定防止 SQL 结构被用户拼接,但不意味着任意 MATCH 表达式都安全。若直接把原始查询作为全文语法传入,仍可能得到不可控的运算或语法错误。本书明确选择有限的词项 OR 查询,测试包含引号和操作符输入,验证实际调用可执行且不会绕过过滤。

FTS5 使用 bm25(chunks_fts) 排序。按照 SQLite FTS5 文档,这个辅助函数把更好的匹配放在更小的数值方向,因此代码使用升序。不要把其他系统中“分数越大越好”的经验直接套进来。

权限进入候选查询

词面 SQL 同时关联文档,并要求租户相同、文档未删除、ACL 包含当前用户或租户内星号,然后才排序并限制前一百条。先取全局前一百、再过滤权限,可能让大量不可见结果挤掉可见文档,也更容易使日志和调试接口暴露内容。

不过,共享 FTS 表的文档频率等统计仍然是全局的。过滤返回行解决结果可见性,没有自动提供严格统计隔离。若租户之间连统计影响都不可接受,应使用独立索引或经过专门设计与审计的隔离方案。

实际评估中,词面召回在十条正例上的 Recall@3 与 MRR@3 都为零点九,漏掉“旅馆住一夜能花多少钱”。报告中的前三文档已经去重;这个结果只描述八篇作者文档,不能代表企业搜索平均水平。下一章用相同语料对比词项向量与实际模型表示。