创建 bm25 索引、调优段大小与 BM25 参数(k1、b)、监控并行构建进度并检查索引页。
这是本节的多页打印视图。 .
使用指南与混合检索
- 1: 索引构建与维护
- 2: 查询与相关性评分
- 3: 过滤检索与预过滤
- 4: 混合检索 (BM25 + VectorChord)
- 5: BM25 与 Block-WeakAnd 内部原理
了解如何在生产环境中构建、调优和查询 vchord_bm25 索引:
运行 Top-K Block-WeakAnd 查询、理解负值 BM25 得分并调优 bm25_limit。
通过后过滤、预过滤和部分索引,将 WHERE 过滤条件与 BM25 排序无缝结合。
通过倒数排序融合(RRF)和 ColBERT 重排序,融合稀疏 BM25 词法检索与 VectorChord 稠密向量检索。
深入解析 Okapi BM25 公式、单字节 Fieldnorm 量化、差分位打包与 Block-Max WAND 算法。
1 - 索引构建与维护
vchord_bm25 提供了 bm25 索引访问方法(CREATE ACCESS METHOD bm25 TYPE INDEX HANDLER _bm25_amhandler)以及 bm25_ops 操作符类。
创建 bm25 索引
稳定发布版(v0.1 – v0.3,bm25vector)
新一代 main 分支(tsvector)
在 main 分支中,bm25_ops 直接作用于 tsvector,并支持索引级存储参数(options、limit 和 prefilter):
存储参数 (WITH (...)) |
类型 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|---|
options (k1) |
float(TOML 内) |
1.2 |
1.2 – 2.0 |
词频饱和度参数 。数值越大,文档内重复出现的词项权重越高。 |
options (b) |
float(TOML 内) |
0.75 |
0.0 – 1.0 |
文档长度归一化参数 。0.0 禁用长度归一化;1.0 完全按相对于 的文档长度进行归一化。 |
limit |
int |
0 |
0 – 65535 |
存储在索引上的默认 Top-K 检索上限(当会话 GUC bm25.limit = 0 时使用)。 |
prefilter |
bool |
false |
true / false |
存储在索引上的默认预过滤行为(可在会话级通过 bm25.prefilter 覆盖)。 |
你可以使用 ALTER INDEX 动态更新现有索引上的 limit 或 prefilter 参数:
自动分词触发器(v0.1 – v0.3)
使用 pg_tokenizer 时,可以通过触发器在执行 INSERT 和 UPDATE 时自动同步更新 bm25vector 列:
使用自定义模型触发器(一步到位辅助函数)
create_custom_model_tokenizer_and_trigger 会一步完成自定义模型创建、分词器创建、存量数据回填,并自动挂载 BEFORE INSERT OR UPDATE 触发器:
将标准 PL/pgSQL 触发器用于预训练模型
如果你使用的是预训练模型(如 llmlingua2 或 bert_base_uncased),可以创建一个标准的触发器函数:
段架构:增长段(Growing)与封闭段(Sealed)(v0.1 – v0.3)
在内部实现中,vchord_bm25 将索引元组组织为两种段状态:
- 增长段(Growing Segment):新写入的元组(
INSERT/UPDATE)会追加到增长段中,从而无需重新压缩整条倒排链表即可获得极高的写入吞吐量。 - 封闭段(Sealed Segment):当增长段超过
bm25_catalog.segment_growing_max_page_size个页面(默认4096页,约 32 MB)时,它会被封存为不可变的只读段,采用差分位打包(Delta-Bitpacked)的 128 文档块结构,并预计算块级最大得分上界与词项文档频率(DF)统计信息。
并行索引构建与进度监控
索引构建支持 PostgreSQL 并行维护工作进程(Parallel Maintenance Workers),并可通过 pg_stat_progress_create_index 跨三个阶段实时汇报构建进度:
initializing(初始化)scanning table(扫描表)writing index structures(写入索引结构)
检查索引页(v0.3.0)
0.3.0 版本提供了 bm25_page_inspect(index regclass, blkno int) 函数,用于检查 bm25 索引的内部元数据与页头信息:
索引约束与限制
- 仅支持单列索引:
bm25索引只能创建在单个bm25vector(或tsvector)列上,不支持多列联合bm25索引。 - 不支持 Unlogged 表:
bm25访问方法不支持UNLOGGED表(调用ambuildempty会抛出Unlogged indexes are not supported错误)。 - 仅支持正向扫描:
bm25索引扫描在<&>操作符上仅支持正向(ASC)扫描方向。
2 - 查询与相关性评分
基础 Top-K 查询语法
要运行经索引加速的 Top-K BM25 检索,请在 ORDER BY 子句中将 <&> 操作符与 to_bm25query 结合使用:
为什么 to_bm25query 需要传入索引名称
与向量距离操作符不同(两个向量之间的距离仅取决于这两个向量本身),一篇文档的 BM25 得分依赖于存储在 BM25 索引中的全语料库统计信息:
- 语料库中的文档总数()
- 全语料库的平均文档长度()
- 每个查询词元在语料库中的文档频率()
将索引('documents_embedding_bm25'::regclass)传入 to_bm25query(...),使得独立评分函数(search_bm25query / _bm25_evaluate)即使在 PostgreSQL 于 SELECT 投影列表中求值 <&> 或执行暴力顺序扫描(Sequential Scan)时,也能从索引中读取语料库统计信息。
理解负值 BM25 得分
根据设计,<&> 返回的是负值 BM25 得分:
- PostgreSQL 的索引访问方法 API(
amcanorderbyop)仅支持对ORDER BY <operator> ASC执行按索引排序的扫描。 - 由于 BM25 得分越高代表相关性越强,返回 $-\text{BM25}(D, Q)$ 可以在默认的升序(
ASC)排列下,将最相关的文档排在最前面。 - 切勿使用
ORDER BY embedding <&> to_bm25query(...) DESC——PostgreSQL 无法对DESC排序使用正向bm25索引扫描,会导致回退到全表扫描或抛出扫描方向错误。
调优 bm25_limit 与 SQL LIMIT
由于 Block-WeakAnd 在索引扫描期间会维护一个有界 Top-K 最小堆,以激进地剪枝缺乏竞争力的倒排块,因此索引扫描器在扫描开始时就需要知道 (即最多需要收集多少个候选文档)。
在 v0.1.0 – v0.3.0 中:bm25_catalog.bm25_limit
GUC 参数 bm25_catalog.bm25_limit(默认值为 100,取值范围 -1 至 65535)控制单次 bm25 索引扫描最多返回的 Top-K 文档数量:
bm25_catalog.bm25_limit 的特殊取值说明:
| 取值 | 行为说明 |
|---|---|
1 – 65535 |
使用 Block-WeakAnd 计算并返回最多 bm25_limit 条高分文档(默认 100)。 |
0 |
立即返回 0 行。 |
-1 |
禁用 Block-WeakAnd Top-K 剪枝,对索引中所有匹配的倒排项执行全量暴力扫描。 |
在 main 分支中:bm25.limit 与索引 limit 存储参数
在 main 分支中,实际生效的检索上限按以下优先级顺序决定:
- 会话 GUC 参数
bm25.limit(若> 0) - 索引存储参数
WITH (limit = ...)(若> 0)
请始终确保 bm25_catalog.bm25_limit(或 bm25.limit)大于或等于 SQL 查询中的 LIMIT 值。如果 SQL 查询指定了 LIMIT 200,而 bm25_limit 仍为 100,则索引扫描在产出 100 行后就会停止。
使用 EXPLAIN 验证查询计划
使用 EXPLAIN (ANALYZE, BUFFERS) 确认 PostgreSQL 正在使用 Index Scan using ... on documents:
如需强制执行顺序扫描 + 精确暴力求值(常用于验证召回率或调试),可以临时禁用索引扫描:
3 - 过滤检索与预过滤
在实际业务场景中,全文 BM25 排序通常需要与结构化关系过滤条件(例如 category_id = 5、tenant_id = 'acme' 或时间范围)结合使用。vchord_bm25 支持三种带过滤条件的 BM25 检索策略。
1. 后过滤(Post-Filtering)与预过滤(Pre-Filtering)
当你执行一条同时包含 WHERE 子句和 ORDER BY embedding <&> to_bm25query(...) LIMIT K 的查询时:
vchord_bm25 可以通过以下两种方式评估 WHERE 谓词:
后过滤(enable_prefilter = false)
bm25索引在整个语料库上运行 Block-WeakAnd,返回全局前bm25_limit名候选文档(例如全局 Top 100)。- PostgreSQL 回表取出这 100 条堆表元组,并应用
WHERE category = 'database'过滤条件。 - 注意事项:如果全局 Top-100 文档中只有 3 篇满足
category = 'database',那么即使表里还有数百篇 BM25 得分略低的'database'类别文档,查询也只会返回 3 行!
预过滤(enable_prefilter = true)
启用预过滤后,vchord_bm25 会在将候选文档推入 Block-WeakAnd Top-K 堆之前,先行检查堆表可见性并评估过滤条件是否满足:
- 不满足过滤条件的文档绝不会进入 Top-K 堆,也不会抬高 Block-WeakAnd 的剪枝阈值 。
- 索引扫描会持续推进,直到找齐 篇既满足
WHERE过滤条件、又在所有匹配行中拥有最高 BM25 得分的文档。
启用预过滤
在 v0.2.2 – v0.3.0 中:
在 main 分支中:
2. 面向高选择性分区键的部分 BM25 索引(Partial Index)
如果你经常按固定的一组分区键(例如 status = 'published' 或特定 tenant_id)进行过滤,可以创建部分 bm25 索引(Partial Index):
查询部分索引时,请在 to_bm25query 中指定该部分索引的名称,并在 SQL 中附上匹配的 WHERE 条件:
选择合适的过滤策略
| 过滤条件选择率 | 推荐策略 | 原因说明 |
|---|---|---|
| 高匹配率(50%–100% 的行匹配) | 后过滤(enable_prefilter = false)并适当调大 bm25_limit |
避免在 WAND 内层循环中频繁回表读取堆页;全局 Top-100 中即可轻松包含 10 条以上匹配行。 |
| 中等匹配率(5%–50% 的行匹配) | 预过滤(enable_prefilter = true / bm25.prefilter = true) |
确保在过滤后的子集上实现精确的 Top-K 召回,同时不扫描无关倒排项。 |
| 固定租户 / 状态子集 | 部分 bm25 索引(CREATE INDEX ... WHERE ...)或表分区 |
索引体积更小,拥有针对该子集独立的 IDF 统计信息,且对其他分区零无效检查开销。 |
4 - 混合检索 (BM25 + VectorChord)
现代检索增强生成(RAG)与搜索流水线通常将稠密向量检索(vchord,擅长捕捉语义相似性)与稀疏词法 BM25 检索(vchord_bm25,擅长捕捉精确关键词、标识符与罕见词匹配)结合使用。由于这两个扩展均原生运行在 PostgreSQL 内部,你可以在单条 SQL 查询中端到端执行混合检索,无需任何外部编排服务。
混合检索表结构准备
方式一:纯 SQL 实现倒数排序融合(RRF)
倒数排序融合(Reciprocal Rank Fusion,RRF) 能够直接融合来自多个检索系统的排名结果,而无需在 L2/余弦距离与 BM25 得分之间做复杂的分数归一化校准:
其中 为平滑常数(通常取 )。
方式二:混合召回 + ColBERT 晚期交互重排序
如果追求更高的检索精度,可以先使用 vchordrq(稠密检索)+ vchord_bm25(词法检索)召回候选文档并集,再在 PostgreSQL 内使用 VectorChord 的 @# (max_sim) 多向量 ColBERT 操作符进行精细重排序:
请参阅我们的工程深度解析博客 在 PostgreSQL 中使用 ColBERT 重排序为向量检索加速 以及 使用 Postgres 原生 BM25 与 VectorChord 实现混合检索,了解在 PostgreSQL 内部执行“稠密向量 + BM25 + ColBERT 重排序”的完整 BEIR 基准测试对比。
5 - BM25 与 Block-WeakAnd 内部原理
本页详细介绍 vchord_bm25(位于 crates/bm25/src/bm25.rs 与 src/algorithm/block_wand.rs)中实现的精确评分公式与底层数据结构。
1. Okapi BM25 评分公式
给定一篇文档 和一个查询 ,其中查询词频为 ,文档内词频为 ,vchord_bm25 按如下公式计算相关性得分:
逆文档频率()
其中:
- 为表中已索引文档的总数。
- 为包含词元 的文档数量(即文档频率 DF)。
饱和词频()
其中:
- 为词元 在文档 中出现的词频。
- 为从单字节
fieldnormID 解码得到的量化文档长度(文档 中的总词元数)。 - 为所有已索引文档的平均文档长度。
- (默认值为
1.2)用于控制词频饱和度。 - (默认值为
0.75)用于控制文档长度归一化强度。
2. 单字节 Fieldnorm 量化(FIELDNORM_TO_LENGTH)
如果为每篇文档都存储 32 位浮点数或整数长度,并在对每个倒排项评分时执行浮点除法,将消耗大量的内存带宽和 CPU 周期。为此,vchord_bm25 使用包含 256 个表项的 FIELDNORM_TO_LENGTH 查找表,将文档长度 量化为单个字节(u8,0..=255):
- 短文档精确表示(
0–40个词元):取值0..=40与真实词元数量呈 1:1 精确映射。 - 长文档对数步长增长(
41–255):步长随长度呈对数级递增(例如42, 44, 46, ...,最高可覆盖2,013,265,944个词元)。 - 256 项预计算得分缓存:由于
fieldnorm只有 256 种可能取值,vchord_bm25会在查询开始时针对每个查询词预先计算出全部 256 种可能文档长度的分母项:
如此一来,在索引扫描期间为候选倒排项打分就简化为一次查表与一次浮点除法:
3. 倒排链压缩与 Block-WeakAnd(Block-Max WAND)
基于差分位打包(Delta-Bitpacking)的 128 文档定长块
对于封闭段(Sealed Segment)中的每个词项,由 (doc_id, term_frequency) 组成的倒排链表会被划分为每块 128 篇文档的定长块,并采用**差分位打包(Delta-Bitpacking)**进行压缩:
- 块内文档 ID 按升序排列,存储相邻文档 ID 的差值(),并按块内最大差值所需的最小位宽进行位打包。
- 每个块头记录该块的最后一个文档 ID(用于快速浅层跳转查找,Shallow Seek)以及能够在当前 128 文档块内产生最高 BM25 分数贡献的块内最大词频 / 最短长度对 $(\text{tf}{\max}, \text{fn}{\min})$。
Block-WeakAnd Top-K 剪枝循环
在执行 ORDER BY vec <&> to_bm25query(...) LIMIT K 扫描期间:
flowchart TD
A["按当前 doc_id 对查询词游标排序"] --> B["跨游标累加全局最大得分上界 max_score\n直到累加和 > 当前 Top-K 阈值 θ(确定枢轴文档 Pivot)"]
B --> C{"枢轴处的块级最大得分之和\n(Block-Max) > θ ?"}
C -- "否(剪枝跳过整块!)" --> D["直接跳过整个 128 文档块\n无需解码倒排项"]
C -- "是" --> E{"所有前导游标是否均已\n对齐到 Pivot doc_id?"}
E -- "否" --> F["将前导游标推进至 Pivot doc_id"]
E -- "是" --> G["解码数据块并计算精确 BM25 得分\n更新 Top-K 最小堆并抬高阈值 θ"]
D --> A
F --> A
G --> A- 枢轴选择(Pivot Selection):所有查询词游标按其当前指向的
doc_id升序排列。从第一个游标开始,vchord_bm25依次累加各词项的全局得分上界max_score,直到累加和超过 Top-K 最小堆中当前的第 高分阈值 。使累加和跨过 的那个游标所指向的文档即被选为枢轴文档(Pivot Document)。 - 块级上界精化(Block-Max Refinement):在不解压任何倒排块的前提下,
vchord_bm25将前导游标浅层定位(Shallow Seek)到枢轴所在的 128 文档块,并累加更紧致的块级得分上界(block_max_score)。 - 快速跳块(Fast Block Skipping):如果各游标的
block_max_score之和 ,说明当前块交集中的任何文档都不可能进入 Top-K 结果——扫描器会直接跳过该块,完全不必解码文档 ID 或词频数据。 - 精确求值(Exact Evaluation):只有当块级上界之和超过 且所有前导游标均对齐到
pivot_doc时,vchord_bm25才会解码该数据块、检查 MVCC 可见性/删除位图(以及可选的预过滤条件)、计算精确的 BM25 得分,并更新 Top-K 最小堆。