在 PostgreSQL 内部做混合搜索:用 BM25、稀疏向量与 RRF 兼顾语义和术语

2026-07-22 29 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

向量检索很擅长理解“用户换了一种说法”的问题,却未必能准确命中 wal_level = logicalLSN lagspock apply worker 这类精确术语。pgEdge Vectorizer 新增的混合搜索能力将 BM25 稀疏向量生成放进 PostgreSQL,与稠密 embedding 一起维护,再用 Reciprocal Rank Fusion(RRF)合并结果。这样,应用不必额外维护一套关键词搜索服务。

两种检索信号,各自解决不同问题

稠密向量检索将文本和查询编码为数值向量,按语义距离寻找相近内容。用户搜索“怎样重新进入账号”,即使文档标题是“账号恢复”,也可能被正确召回。它适合自然语言提问、同义改写和概念性问题。

它的弱点同样明显:对于高度具体的技术 token,模型可能把一篇泛泛而谈的复制文章排在精确解释 wal_level 参数的文章前面。两篇文章在语义上都与复制相关,但用户真正需要的往往是那个字面匹配的配置项。

BM25走的是另一条路线。它不理解语义,而是结合查询词在文档中的出现频率,以及这些词在整个语料中的稀有程度进行排序。the 这样的常见词几乎没有区分度;只在少数文档中出现的 wal_levelLSN 或函数签名,则会成为强信号。

因此,混合搜索不是让其中一种算法取代另一种,而是把它们放到同一个检索路径中:

  • 概念问题主要受益于稠密向量。
  • 参数名、错误码、函数名和产品术语主要受益于 BM25。
  • 大多数真实查询同时包含意图和精确名词,适合同时使用两种信号。

RRF 为什么比直接合并分数更实用

向量相似度和 BM25 分数不在同一个量纲上。直接把它们相加,往往会得到难以解释、难以稳定调参的结果。RRF 不关心原始分数,只关心某篇文档分别排在第几名。

一个常见的加权 RRF 形式如下:

final_score = alpha / (k + dense_rank)
            + (1 - alpha) / (k + sparse_rank)

其中 k 是平滑常数,alpha 控制稠密检索的权重。两种检索都排在前列的文档会累积更高分;只被一种检索发现的文档仍有机会进入结果集,但通常排得更靠后。

这正适合 PostgreSQL 文档库、内部知识库和运维手册:一方面用户会问“副本为什么落后”,另一方面又会直接输入 LSN lag。RRF 让两类查询都能从各自最擅长的召回机制中获益。

在数据库内维护的价值

启用混合模式后,Vectorizer 的后台 worker 会为每个文本 chunk 生成稠密 embedding 和 BM25 稀疏向量。chunk 表会出现类似 sparse_embedding 的额外列;系统还会维护每个 term 的 IDF 统计表,用于计算 BM25 的稀有词权重。

这带来三个直接收益:

  • 数据更新时,两类索引信号同步生成,无需由应用分别写入向量库和搜索引擎。
  • 搜索函数在 PostgreSQL 内完成候选结果融合,调用端只需要执行一次查询。
  • 在 pgEdge 多节点环境中,生成完成的 chunk 和 IDF 相关数据可经 Spock 复制到其他节点,让每个节点独立提供混合检索。

需要注意,IDF 依赖整个语料的词频。在只有几篇文档的小型测试集上,低分结果之间的排序可能看起来不稳定;当语料规模增长、词频分布趋于稳定后,BM25 信号通常更有参考价值。

用 SQL 直观看 RRF 如何合并排名

下面的 SQL 不依赖特定扩展 API,可直接在 psql 中运行,用于验证加权 RRF 的行为。实际项目中,将两个 CTE 换成稠密检索和 BM25 检索返回的候选列表即可。

WITH
params AS (
  SELECT 60.0::numeric AS k, 0.70::numeric AS p_alpha
),
dense_results(document_id, dense_rank) AS (
  VALUES
    ('replication-overview', 1),
    ('spock-worker-overload', 2),
    ('wal-level-logical', 3)
),
sparse_results(document_id, sparse_rank) AS (
  VALUES
    ('wal-level-logical', 1),
    ('spock-worker-overload', 2),
    ('replication-overview', 5)
),
all_candidates AS (
  SELECT document_id FROM dense_results
  UNION
  SELECT document_id FROM sparse_results
)
SELECT
  c.document_id,
  d.dense_rank,
  s.sparse_rank,
  round(
    coalesce(p.p_alpha / (p.k + d.dense_rank), 0) +
    coalesce((1 - p.p_alpha) / (p.k + s.sparse_rank), 0),
    6
  ) AS rrf_score
FROM all_candidates AS c
CROSS JOIN params AS p
LEFT JOIN dense_results AS d USING (document_id)
LEFT JOIN sparse_results AS s USING (document_id)
ORDER BY rrf_score DESC, c.document_id;

这里 spock-worker-overload 在两份列表中都排第 2,因此它会得到稳定的双重贡献。wal-level-logical 虽然稠密排名较低,但 BM25 排名第 1,仍能被显著抬升。将 p_alpha 改为 0.4,可以观察到精确关键词信号对最终排序的影响增大。

调参和集群运行时的边界

默认的 p_alpha = 0.7 更偏向语义检索,适合“为什么账号被锁定”这类帮助中心问题。对于经常出现配置参数、SQL 函数名或下划线标识符的 PostgreSQL 技术文档库,可以从 0.30.4 开始评估,让 BM25 获得更大的影响力。

不建议一开始就全局固定低权重。更实用的做法是保留默认值,并在应用发现查询像精确技术词时按请求调整。例如,包含下划线、等号、版本号、函数调用形式或已知参数名的查询,通常值得降低 p_alpha

在 pgEdge 集群中,还要确保每个节点都完成扩展注册和本地 worker、trigger 的部署。复制到本节点的行会绕过 trigger,以免重复生成 embedding;因此,如果节点上早已存在数据,再创建 trigger 后需要显式将旧行重新加入处理队列。新写入的数据在各节点完成配置后则可自动处理和复制。

落地检查清单

  • 用真实查询日志区分概念问题、精确术语问题和混合问题。
  • 同时记录稠密排名、稀疏排名与最终排名,定位错误结果是召回问题还是融合问题。
  • 先使用默认 p_alpha,再根据评测集调整,不要仅凭少量样本判断。
  • 在小语料阶段谨慎解读 IDF 和低分排序。
  • 对每个 pgEdge 节点完成 worker 与 trigger 配置,并处理配置前已存在的数据。

把 BM25 稀疏向量、稠密 embedding 和 RRF 放在 PostgreSQL 内部,核心价值不只是多了一种排序算法,而是将语义理解、精确术语匹配和数据同步收敛到同一套数据库工作流中。


相关推荐