用 ctid 做文档号的 Postgres 全文索引:85 GB 语料八分钟建完,实测数据与代价

一个叫 TIN 的 Postgres 扩展把全文搜索做成了原生索引访问方法,一条 CREATE EXTENSION 装好,检索、计数、BM25 排序都在同一个数据库事务里。85 GB、1.5 亿文档的语料上,索引构建 8 分 10 秒,混合查询 top-10 跑出 199 QPS,p99 256 ms,每查询只读 65 MB;同批查询 ParadeDB 是 7.9 QPS、p99 6765 ms、582 MB。数据出自发布方自己的测试,条件是 8 vCPU、32 GB、支持 AVX-512 的容器,数据库编码必须 UTF8 或 SQL_ASCII。适合重度使用 Postgres、不想再养一套搜索引擎的团队,读写混合的在线表优先。

内置 GIN 做全文检索卡在哪

Postgres 自带 tsvector 加 GIN。能过滤,不擅长算相关度。GIN 先返回全部命中的 tid,排前十条再靠 ts_rank_cd 逐行打分,语料一大这步就拖垮整个查询。发布方在同一台容器内实测,合取加短语的 top-10,GIN 只有 0.4 QPS,p99 接近 29 万毫秒。析取查询更糟,内存耗尽没跑完。

GIN 也没有每个词项的精确命中数,count(*) 要把位图全解出来再回表验可见性。TIN 在索引元数据里存了 posting 数,两词的页级位图无交集时,析取计数就是两个精确计数相加,一个 postings 都不用解。

为什么用 ctid 当文档号反而更快

TIN 不给文档编连续编号,直接用 Postgres 的行位置 ctid。这是 48 位数,高 32 位块号,低 16 位块内偏移,(190,17) 就是第 190 页第 17 个槽位,O(1) 定位。省掉的是文档号到 ctid 的映射。b-tree、GIN、GiST、hash 的 postings 存的本来就是 ctid,用连续文档号的外置索引得另存一份,匹配 1000 万行就查 1000 万次。

压缩跟着这个结构走。8 KB 页最多放 291 个元组,页级位图 256 bit,正好进一个向量寄存器,于是有了两级编码。高频词接近每 posting 1 bit,中频约 7 bit,低频可接近 25 bit,只出现一次的词不按位图存。合取析取变成 AND 与 OR 指令,计数用 POPCNT。the AND rareword 里页级位图交集中没有的页,页内偏移位图连解码都省了。

85 GB 语料的索引八分钟建得起来吗

建得起来,这是发布方实测最快的一项。语料是 Stack Exchange 问答导出,85 GB、1.5 亿文档,Postgres 18.6,容器限 8 vCPU 与 32 GB,刻意小到索引装不进缓冲。构建结果如下。

  • TIN 8 分 10 秒,索引 50.7 GB,需 32 GB 内存
  • ParadeDB 19 分 20 秒,52.1 GB,需 64 GB
  • 内置 GIN 2 小时 9 分 4 秒,28.0 GB,需 64 GB

Wikipedia 语料上做析取计数,索引装进 shared_buffers 时 TIN 达到 10,260 QPS、p99 2 ms。要复现这个速度,建索引前先把内存和并行给够,发布方建议 SET maintenance_work_mem = '8GB'SET max_parallel_maintenance_workers = 8;预算不足时索引更碎,会拖慢这个索引上的每个查询。段数在建索引时固定,换机型服役要按服役机型重设 initial_segment_count

既要每秒几百次更新又要相关度排序扛得住吗

扛得住,代价要看清。带并发写的析取 top-10,另一客户端目标每秒 1000 次 UPDATE,TIN 只读 148 QPS、p99 324 ms,带写 125 QPS、p99 354 ms,十分钟完成 270,279 次更新。

同步运维要跟上。TIN 的删除依赖 VACUUM,ctid 被判定死亡后才从索引侧清位,之前 count(*) 得回 heap 验可见性,BM25 统计也仍把已删行算进去。发布方建议按表把 autovacuum_vacuum_scale_factor 降到 0.01、autovacuum_vacuum_threshold 设为 1000,不搜索的表保持默认。索引体积只增不减,收缩只能靠 REINDEX,体积用 pg_relation_size 量。分区表上 BM25 按分区独立算,每个分区一份语料统计,跨分区比分数等于混合不同子集的分数。

中文检索怎么办

这部分我没核到,只能摆成待澄清。TIN 默认分词按 Unicode 词边界切分,折叠大小写与音标,emoji 可检索。官方已核页面都没有中文分词的说明,连续汉字怎么切、能不能外接中文分词器,一处都没提。做中文内容别照抄英文结论,先用 tin.tokenize 拿一段中文实测再决定。另外它不支持词干提取,内置 FTS 与 ParadeDB 都支持,这一项 TIN 明确不做。

谁适合用,怎么试

  • 已在 Postgres 里,检索量中等,不想再引入第二套集群和同步任务,值得试。
  • 写入密集且重排序,先按上面的 autovacuum 参数改造,再用真实数据量压一遍。
  • 本地与 CI 跑不了生产版,公开的只有 Lead,功能等价但逐行扫描,同样语料每查询要 4 分钟,别拿它推断性能。
  • 版本号两处不一致,一处 1.0.2,一处 0.9.0,落地前向提供方确认。