2026年8月3日 1 分钟阅读

用 ParadeDB 在 PostgreSQL 里做 BM25 检索、过滤与排序

tinyash 0 条评论

很多站内搜索的“第一版”并不难:业务数据在 PostgreSQL,搜索索引却在 Elasticsearch 或另一台搜索服务里;写入时再异步同步一份文档。真正麻烦的通常发生在后面:商品改名、库存变化、权限变更和索引回填各自有延迟;排障时还要判断是业务库数据不对,还是同步链路漏了事件。

ParadeDB 提供了另一条路线。它的 pg_search 扩展把全文搜索、BM25 相关性排序、过滤和聚合放到 PostgreSQL 内部。这里的重点不是“PostgreSQL 也能搜文本”,而是把搜索索引和应用数据放在同一数据库里,从而删除一条数据同步链路。

它并不是所有搜索系统的替代品。若团队已经依赖独立搜索集群的跨地域容量、复杂运维体系或特定插件,迁移成本未必划算。但对于已有 Postgres 商品目录、文档库或后台检索,并且首先想解决“数据为什么不同步”的团队,这种同库索引模式值得先在本地验证。

先明确:ParadeDB 解决的是哪一层问题

以商品搜索为例,一次用户查询经常不只是在 description 里找关键词。它通常还要满足:

  • 只展示有库存的商品;
  • 只看某个类目;
  • 允许按评分、价格或新旧程度再排序;
  • 把关键词匹配度作为主要排序信号。

如果全文索引在独立引擎中,业务库里的 in_stockcategoryrating 等字段要么也同步过去,要么先从搜索引擎查 ID 再回 Postgres 补过滤。前者增加同步一致性负担,后者容易让分页、排序和权限逻辑变复杂。

ParadeDB 的做法是创建一个覆盖多个列的 ParadeDB index。官方文档特别建议:全文查询中会用于过滤、GROUP BYORDER BY 或聚合的列,也应纳入索引。这样,相关性计算和这些条件就能在同一条 SQL 中表达。

用官方本地环境跑通最小闭环

官方 README 给出的快捷安装方式会启动新的 Docker 容器,并直接进入 psql 会话:

curl -fsSL https://paradedb.com/install.sh | sh

这是适合试验的入口,不等于生产部署方案。生产环境还需要按团队的 PostgreSQL 版本、持久化、备份、升级窗口和 AGPL-3.0 许可要求评估。ParadeDB 社区版采用 AGPL-3.0,仓库也提供另行授权的 Enterprise 选项;不要把它误写成宽松许可证项目。

进入 psql 后,官方环境文档可以创建一张带模拟商品数据的表:

CALL paradedb.create_bm25_test_table(
  schema_name => 'public',
  table_name => 'mock_items'
);

SELECT description, rating, category
FROM mock_items
LIMIT 3;

接着建立索引。下面的字段列表来自官方示例:id 是索引的 key field,文本、向量和元数据字段可共同出现在同一个索引定义中。

CREATE INDEX search_idx ON mock_items
USING paradedb (
  id,
  description,
  embedding vector_cosine_ops,
  category,
  rating,
  in_stock,
  created_at,
  metadata,
  weight_range
)
WITH (key_field = 'id');

这里有两个容易忽略的设计点。

第一,key_field 是必需项,它应当是能稳定标识记录的字段。业务表若没有稳定主键,先修数据模型比先调相关性更重要。第二,不要把“覆盖索引”理解为“把表中所有列都塞进去”。应该从实际检索接口反推:用户会按哪些字段过滤、分组、排序或聚合,就把哪些字段放进索引;未参与检索路径的大字段不应仅因“可能有用”而盲目加入。

一条 SQL 同时完成匹配、库存过滤和 BM25 排序

ParadeDB 的 ||| 操作符用于匹配查询;pdb.score() 产生 BM25 分数,分数越高代表相关性越高。将两者与普通关系条件组合,可以写出如下查询:

SELECT
  id,
  description,
  category,
  rating,
  pdb.score(id) AS score
FROM mock_items
WHERE description ||| 'keyboard'
  AND in_stock = true
  AND category = 'Electronics'
ORDER BY pdb.score(id) DESC, id ASC
LIMIT 5;

末尾的 id ASC 不是装饰。官方分数文档说明:仅按 pdb.score 排序时,相同分数的文档没有稳定顺序;把已索引的主键作为 tie-breaker,才能让分页、缓存和回归测试看到可重复的结果。若接口使用 offset 分页,尤其应固定这一规则,否则同一查询可能在相邻页出现重复或漏项。

这段查询的价值不在于 SQL 更短,而在于职责清晰:description ||| 'keyboard' 决定哪些记录匹配文本;in_stockcategory 保留业务约束;pdb.score(id) 决定匹配结果的相关性排序。读者可以先不引入 embedding,也能得到一个可解释、可复现的商品搜索路径。

实际接入时,建议把查询字符串和过滤条件参数化,而不是把用户输入拼进 SQL。例如应用层可固定 SQL 结构,让数据库驱动分别绑定关键词、类目和页大小。这样既避免注入风险,也方便在日志中分别观察“关键词为空”“类目过窄”和“库存条件过严”造成的零结果。

不要跳过基线:先和现有 PostgreSQL 检索比较

引入新扩展前,先写清楚现有方案是什么。若页面只需要少量字段的简单词匹配,PostgreSQL 自带的全文检索或现有 GIN 索引可能已经足够;这时增加扩展、镜像和升级流程反而扩大了运行面。ParadeDB 更适合需要把 BM25 相关性、多个过滤维度和后续聚合统一到同一个检索接口的情况。

比较也不要只看平均延迟。用同一批查询分别记录结果前几名、零结果比例、过滤后的候选数和写入后的可见性。搜索的“快”如果换来了排序明显变差,或库存更新不能按业务预期生效,就不是可接受的优化。把这份基线留在版本库中,未来升级 ParadeDB、调整 tokenizer 或增减索引字段时,才能判断变化究竟改善了什么。

从旧写法迁移时,先确认访问方法

如果你在旧文章或旧代码中看到 USING bm25,不要急着照抄。ParadeDB 当前文档说明,从 v0.25.0 起,原先称为 BM25 index 的索引重命名为 ParadeDB index,推荐写法是 USING paradedbUSING bm25 仍作为向后兼容别名存在。

这类命名变化看似不影响查询结果,却会影响团队的长期维护:新成员按当前文档排障时,若迁移脚本仍使用旧名,容易误以为项目依赖了另一套扩展。因此建议在升级或新建迁移时统一采用 USING paradedb,并在变更记录里注明兼容边界。

先别把“向量”和“混合检索”当成默认能力

ParadeDB 的项目定位提到 vector retrieval,但官方 README 的能力清单同时将 Native Vector SearchNative Hybrid Search 标注为 coming soon。也就是说,看到索引示例中出现 embedding vector_cosine_ops,不应据此推导“原生混合检索已经是稳定默认功能”。

如果当前目标只是关键词检索,先把 BM25、过滤和排序验证清楚。确有语义召回需求时,再依据当前版本文档确认所用向量扩展、融合策略与上线状态。不要为了追求“混合搜索”而在没有评估数据的情况下,把两种召回混成一个不可解释的黑盒。

另一个明确的性能边界是跨表打分。官方分数文档指出,直接对 join 后多个表的分数求和并排序,例如 ORDER BY pdb.score(t1) + pdb.score(t2),当前并不高效。订单、商品、用户等多表场景应先拆分检索目标,或者评估文档中提供的融合方案,而不是默认认为任意 join 都能沿用单表检索的性能特征。

上线前的验证清单

把 ParadeDB 接入真实业务前,建议用一份小而严格的验收集做验证:

  1. 选取包含同义词、拼写差异、短标题和长描述的真实样本,人工检查前 20 个结果是否符合预期。
  2. 对每种业务过滤条件分别做“命中”“零结果”“边界值”测试,确认库存或权限变化立即反映在搜索结果中。
  3. 分别测量冷启动、常见查询和高选择性过滤下的延迟;不要只看一条演示查询。
  4. 对索引创建、写入压力和升级安排压测,并将 PostgreSQL 备份恢复演练纳入方案。
  5. 记录索引字段清单及每个字段加入的理由。未来若搜索变慢,这份记录比“当时感觉需要”更容易帮助排障。

ParadeDB 的核心取舍很直接:用 Postgres 内部的搜索能力换取更少的数据复制和更统一的查询路径,同时把索引容量、性能调优与许可评估留在同一套数据库治理中。对于不想一开始就维护第二套搜索基础设施的产品,这是一条值得用真实数据先跑一轮的路线;对于超大规模、强隔离或高度专用化的搜索需求,则应把它当作候选架构,而不是自动替代品。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。