让符号、中文和版本号一起被搜到
PostgreSQL 用户搜索的东西很杂:参数名 work_mem,运算符 ->>,psql 命令 \dC,错误码 23505,中文词“复制延迟”,还常常带着版本号。通用的全文检索对这些都不友好。这篇说明 Crib 的做法。
先查名字,再查正文
排序分四层,前一层的结果总排在后一层前面:
- 当前范围内的精确名称和别名;
- 名称前缀;
- 标题和章节名;
- 正文。
精确名称存在一张普通的 B-tree 表里,键按类型规范化:配置参数、函数、运算符、psql 命令、SQLSTATE 各有自己的规则。\dC 和 \dc 是两个不同的命令,大小写保留;->> 和 @@ 按原样保存,不会被当成标点丢掉;SQLSTATE 代码按字符串处理,前导零不丢。同名的函数重载各自保留,不会因为名字相同只剩一个。
这张表由构建端从手册源码里提取,比如参数定义列表中的 max_connections 记为“定义”,正文里提到它的地方只算“提及”。所以搜 max_connections 时,第一条是参数定义本身,而不是某段恰好提到它的正文。
粘贴进来的对象名也能认出来:pg_catalog.max_connections; 会命中 max_connections 的定义,并在结果和正文里高亮这个名字。运算符、psql 命令和 SQL 表达式则保持原样,不做这种改写。
中文按双字切分
SQLite 自带的 unicode61 分词器会把一整段中文当成一个词;trigram 分词器不匹配少于三个字符的查询,“事务”“索引”这样两个字的词就搜不到。
Crib 用自己的切分规则:连续汉字按重叠双字切分,“事务隔离”切成“事务、务隔、隔离”;拉丁字母的词和标识符另外提取。切分结果编码成带类型前缀的 ASCII 词项再交给 FTS5,构建端(Go)和应用(Swift)用同一套规则,并用同一组测试样例核对。规则有自己的版本号,写在每个资料包的清单里,规则一改,旧包和新包不会混用。
双字切分只能证明两个字出现过,不能证明短语成立。所以加了引号的短语还会回到原文核对顺序和连续性。
单个汉字没有双字可切。“锁”这样的查询走标题和名称的字符索引,在有限的候选里查,并说明范围,不做全库扫描。
输入法全角模式打出的 vacuum 会在查询端折成半角再搜;全角逗号、括号是中文正文的一部分,保持原样。版本前缀和筛选条件里的全角冒号(pg18:)也能识别。
版本前缀和过滤器
查询先解析成结构化条件。PG96:、[email protected]:、pgdevel: 是版本前缀,lang:、kind:、branch:、collection:、domain:、project: 是过滤器,名单是固定的,其余都当作文本。pg_stat_activity: 或者粘贴进来的 ERROR: 不是版本前缀,照常当文本搜。
别名由已签名的版本目录解析:pg18 指向 18.6,pg96 指向 9.6.24。目标版本没有安装或被停用时,搜索直接说明,不会悄悄换成同一条线的其他小版本。显式写了 lang:en,就只返回英文正文。
多个版本折叠成一组
work_mem 在 31 条版本线里都有。如果每个版本各占一行,第一页就全是它。Crib 只在已经证明是同一个对象时才折叠:同一组里,当前选中的版本排在前面,其他版本展开后可见。出现在 31 个版本里不会让它的相关性变高;没有证明是同一对象的同名条目,各自独立。
用什么来检验
我们维护了一组 369 条标注过的查询,覆盖精确名称、符号、psql 命令、SQLSTATE、中文、中英混合和版本过滤。每条的期望结果由独立的审核者对照固定来源确认。2026-11-02 在当前源码和索引上的完整运行结果:
| 指标 | 结果 |
|---|---|
| 前 10 条内找到期望结果 | 369 / 369 |
| 要求排第一的查询 | 270 / 270 |
| 过滤、分组、摘要等约束违例 | 0 |
| 单版本搜索,热缓存 p95 | 31.3 ms |
| 全部版本搜索,热缓存 p95 | 21.8 ms |
延迟是在开发机(Apple M5 Max)上测的,不代表其他机器。这组查询会随着问题报告继续扩充;它证明的是这些查询的行为,不是所有查询都同样好。
搜索语法的完整写法见搜索。