TIN发布:PostgreSQL全文搜索扩展,性能远超ParadeDB

TIN发布PostgreSQL全文搜索扩展。该工具支持布尔、短语及正则查询,兼顾功能完整性与性能,适用于所有Postgres实例。基准测试显示其性能远超ParadeDB,能满足复杂场景下的快速文本索引需求。

2026 年 9 月 16 日,爱里克·里奇(Erik Rijkers)与帕特里克·雷诺兹(Patrick Reynolds)宣布推出名为 TIN 的 PostgreSQL 全文搜索扩展。该工具旨在提供快速、功能完整且可靠的文本索引能力,目前已作为 GA(正式可用)版本发布,适用于所有 Postgres 及 Neki 数据库实例。

TIN QPS达ParadeDB的25倍

TIN 核心特性与设计目标

TIN 的设计初衷是解决现有 Postgres 文本搜索方案在功能完整性与性能之间的权衡问题。其基本使用方式如下:

CREATE INDEX an_index_name ON table_name USING tin(text_column_name);
SELECT * FROM table_name
  WHERE text_column_name ==> 'some words';

开发团队指出,一个理想的 Postgres 文本索引应同时具备以下能力:

  • 支持布尔表达式、短语查询和跨度查询;
  • 支持术语级别的模糊匹配、代码匹配及正则表达式匹配;
  • 自动处理大小写折叠与重音符号折叠;
  • 支持 COUNT(*) 查询及基于 BM25 评分的 Top-K 排序查询。

此外,TIN 强调其在复杂场景下的兼容性,包括多表连接、混合列类型的 WHERE 子句、并发更新、数据复制、备份机制以及正确的事务可见性支持。尽管 Postgres 生态中已存在至少三种文本搜索索引方案,但据称尚无一款能完全满足上述所有要求,而 TIN 在此方面进行了针对性优化。

应用场景示例

应用程序开发者可利用 TIN 构建多种搜索功能。例如,电商平台需检索包含特定关键词的前十个产品:

SELECT * FROM products
  WHERE description ==> 'stretch denim jeans'
  ORDER BY tin.score(ctid) DESC
  LIMIT 10

法律发现平台可能需要返回包含特定词组的文件,且不关注排名结果:

SELECT * FROM emails
  WHERE body ==> '[insider trading conspiracy]'

照片标签平台则可能仅需统计特定标签的照片数量:

SELECT COUNT(*) FROM photos
  WHERE tags ==> '"san francisco"';

在实际生产环境中,应用通常需要在持续写入、更新或删除文档的同时进行索引查询。TIN 承诺,只要事务提交,搜索查询即可立即反映新增或修改的数据行。

基准测试环境与配置

为评估性能,团队针对多种工作负载运行了基准测试,涵盖合取(AND)、析取(OR)及短语查询,并测试了 BM25 评分排序及并发写入场景下的表现。

数据集选择

测试使用了多个大型文本语料库,包括整个维基百科、总计 2.3 TB 的 Reddit 评论集合,以及一个名为“pile”的混合工作负载(含 797 GB 开放获取论文、法律文件、公版书籍及 Enron 邮件)。主要基准测试结果基于 Stack Exchange 问答数据导出集:该语料库包含 1.5 亿份文档,总大小 85 GB。由于缺乏标准查询轨迹,团队通过采样 2 至 15 个词的子串生成了合成查询,分别解释为合取、析取和短语查询,共计生成 1,719 条查询。

硬件与软件环境

基准测试在 AWS i7i.8xlarge EC2 实例上进行。每个文本搜索扩展均在隔离容器中运行 Postgres 18.6。为确保公平性,测试顺序执行以避免资源争用,并使用相同实例类型以最小化网络延迟影响。

配置参数方面,除 ParadeDB 基准工具默认值外,团队调整了三项关键参数以匹配容器资源:max_parallel_workers 设为 8(原默认 40),shared_buffers 设为 24 GB(原默认 128 MB),maintenance_work_mem 设为 24 GB(原默认 64 MB)。测试对比对象包括 TIN v1.0.2、ParadeDB v0.25.2、pg_textsearch v1.4.0 以及 Postgres v18.6 内置 GIN 索引。结果显示,除 TIN 外,仅 ParadeDB 能完成全部基准测试。

性能测试结果

索引构建效率

在索引构建阶段,各引擎的表现差异显著。值得注意的是,除 TIN 外的其他三个引擎在初始配置的 32GB RAM 限制下均出现失败,因此仅在该环节增加了可用内存进行测试。最终查询阶段统一恢复至 32 GB RAM 配置。

引擎 总耗时 索引大小 所需 RAM
TIN 8分10秒 50.7 GB 32 GB
ParadeDB 19分20秒 52.1 GB 64 GB
pg_textsearch 26分49秒 41.5 GB 128 GB
Postgres GIN 2小时09分04秒 28.0 GB 64 GB

混合查询与 Top-10 排序

在不涉及并发写入的情况下,对合取、析取和短语混合查询并按 BM25 分数返回前 10 名结果的测试显示,TIN 的每秒查询数(QPS)是 ParadeDB 的 25 倍,p99 延迟低 26 倍。GIN 因在执行析取查询时耗尽内存而无法完成测试,pg_textsearch 则因仅支持合取查询而未参与此项对比。

合取与短语查询

在仅包含合取和短语查询的 Top-10 场景中,TIN 的 QPS 达到 ParadeDB 的 10 倍、GIN 的 541 倍;p99 延迟分别低 6 倍和 1,356 倍。此测试中,TIN 与 ParadeDB 使用 BM25 算法,而 GIN 使用 ts_rank_cd 进行排名。

并发写入下的析取查询

当引入每秒 1,000 次 UPDATE 操作的并发客户端时,TIN 依然保持高性能。在十分钟的运行中,TIN 完成了 270,279 次更新,而 ParadeDB 完成 185,584 次,pg_textsearch 仅完成 735 次。TIN 的读取吞吐量是 pg_textsearch 的 36 倍、ParadeDB 的 57 倍,p99 延迟分别低 24 倍和 36 倍。相比之下,ParadeDB 的写入接受策略牺牲了读取性能,而 pg_textsearch 因锁竞争导致写入几乎停滞。

内存充足场景

当索引完全适配共享缓冲区时,针对维基百科语料库(8.0 GB 文档)的析取查询计数测试显示,TIN 的性能优势更为明显。pg_textsearch 因不支持非 Top-K 计数查询而未参与此项对比。

详细数据汇总

下表展示了不同场景下的具体指标,其中“MB/查询”反映了从磁盘或块缓存读取的数据量,较低的值有助于减轻 I/O 压力并保持服务器整体响应速度。

Conjunction, disjunction, and phrase queries; top-10
┌────────────────────────────────────────────────────────────────────┐
│                            QPS        p99    MB/query     Updates  │
├─────────────────────────┬───────┬──────────┬───────────┬───────────┤
│ TIN - read-only         │  199  │   256ms  │       65  │           │
│     - with updates      │  172  │   284ms  │       88  │  271,398  │
├─────────────────────────┼───────┼──────────┼───────────┼───────────┤
│ ParadeDB - read-only    │  7.9  │ 6,765ms  │      582  │           │
│          - with updates │  6.0  │ 7,990ms  │      591  │  193,487  │
└─────────────────────────┴───────┴──────────┴───────────┴───────────┘
Conjunction and phrase queries; top-10 (read-only)
┌───────────────────────────────────────────────┐
│                  QPS         p99    MB/query  │
├───────────────┬───────┬───────────┬───────────┤
│ TIN           │  242  │     212ms │        73 │
├───────────────┼───────┼───────────┼───────────┤
│ ParadeDB      │   24  │   1,279ms │       668 │
├───────────────┼───────┼───────────┼───────────┤
│ Postgres GIN  │  0.4  │ 288,066ms │       595 │
└───────────────┴───────┴───────────┴───────────┘
Disjunction queries; top-10
┌────────────────────────────────────────────────────────────────────────┐
│                                 QPS         p99    MB/query   Updates  │
├──────────────────────────────┬───────┬───────────┬─────────┬───────────┤
│ TIN - read-only              │  148  │    324ms  │     48  │           │
│     - with updates           │  125  │    354ms  │     77  │  270,279  │
├──────────────────────────────┼───────┼───────────┼─────────┼───────────┤
│ ParadeDB - read-only         │   17  │  2,385ms  │    303  │           │
│          - with updates      │  2.2  │ 12,634ms  │    394  │  185,584  │
├──────────────────────────────┼───────┼───────────┼─────────┼───────────┤
│ pg_textsearch - read-only    │  3.5  │  8,646ms  │ 11,639  │           │
│               - with updates │  3.5  │  8,409ms  │ 11,656  │      735  │
└──────────────────────────────┴───────┴───────────┴─────────┴───────────┘
Conjunction, disjunction, and phrase queries; COUNT(*) (read-only)
┌─────────────────────────────────────────┐
│              QPS      p99     MB/query  │
├───────────┬───────┬──────────┬──────────┤
│ TIN       │  179  │   438ms  │      97  │
├───────────┼───────┼──────────┼──────────┤
│ ParadeDB  │   10  │ 2,704ms  │     544  │
└───────────┴───────┴──────────┴──────────┘
Disjunction queries; COUNT(*); Wikipedia corpus (read-only)
┌───────────────────────────────────────────────────┐
│                     QPS         p99     MB/query  │
├───────────────┬──────────┬─────────────┬──────────┤
│ TIN           │  10,260  │        2ms  │     1.7  │
├───────────────┼──────────┼─────────────┼──────────┤
│ ParadeDB      │     291  │       95ms  │      22  │
├───────────────┼──────────┼─────────────┼──────────┤
│ Postgres GIN  │     1.4  │   30,292ms  │     2.5  │
└───────────────┴──────────┴─────────────┴──────────┘

综合来看,TIN 在各类场景中吞吐量至少比替代方案高 8 倍,磁盘读取数据量更少,且在高频更新下仍能保持稳定的性能表现。

技术架构解析:为何 TIN 速度快?

TIN 的高性能源于其独特的架构设计,核心在于直接使用 Postgres 的 ctid 作为文档标识符,并利用现代 CPU 的向量化指令进行高效交集与并集运算。

文档标识符的选择

传统文本索引系统通常将文档编号分配为连续整数(如段内 ID 1 到 n),以便利用 Delta 编码和位打包等技术高度压缩倒排列表。然而,这种独立分配的 ID 意味着不同段之间的 ID 无法直接比较,且最终查询时需将这些内部 ID 映射回 Postgres 可识别的物理位置。

TIN 摒弃了内部连续 ID 方案,直接采用 Postgres 的 ctid(Current Tuple Identifier)作为文档标识符。ctid 是一个 48 位数值,由页号(高 32 位)和页内偏移号(低 16 位)组成,唯一标识堆表中元组的物理位置。由于 Postgres 内部索引(B-tree, GIN, GiST, Hash)及缺失位图扫描均原生支持 ctid,TIN 无需维护额外的 ID 映射结构,从而避免了百万级匹配结果时的查找开销。

两级位图编码与向量化

虽然 48 位的 ctid 不连续使得传统压缩技术失效,但 TIN 利用了 Postgres 页面的特性实现了高效编码。单个 8KB 页面最多容纳约 291 个元组,实际场景中通常少于 32 个。这意味着页内偏移号分布密集且范围小,适合使用微型位图存储。

TIN 采用两级位图结构:

  • 页级位图:标记哪些页面包含特定术语。每个位图长 256 位,恰好适配 x86 CPU 的 AVX2 向量寄存器。
  • 偏移级位图:标记页面内的具体元组位置。每个位图可适配一个 AVX-512 寄存器或两个 AVX2 寄存器。

这种结构允许大量计算卸载至 CPU 向量指令:

  • 合取查询(AND):通过对页级位图执行向量 AND 操作,快速排除不包含共同术语的页面,避免解码无关的偏移级位图。
  • 析取查询(OR)与计数:对于 COUNT(*) 类查询,若两个术语的页级位图无交集,TIN 可直接相加各自的精确帖子计数,跳过读取完整的倒排列表。CPU 原生的 POPCNT 指令可用于快速统计位图中置位的数量。
  • 结果提取:查询返回的行数据可通过位位置直接重构出 ctid,无需磁盘查找。由于 ctid 本身按堆物理顺序排列,后续读取匹配元组时天然形成顺序访问模式,极大提升了 NVMe 等现代存储设备的 I/O 效率。

评论 0

0/500

评论需审核后展示,请文明发言

💬
还没有评论,来说两句

相关阅读