跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

使用指南与混合检索

索引管理、Top-K 查询、预过滤、与 vchord 的混合检索以及 Block-WeakAnd 内部原理。

了解如何在生产环境中构建、调优和查询 vchord_bm25 索引:

创建 bm25 索引、调优段大小与 BM25 参数(k1、b)、监控并行构建进度并检查索引页。

通过后过滤、预过滤和部分索引,将 WHERE 过滤条件与 BM25 排序无缝结合。

1 - 索引构建与维护

在 PostgreSQL 中创建、配置、维护和检查 bm25 索引。

vchord_bm25 提供了 bm25 索引访问方法(CREATE ACCESS METHOD bm25 TYPE INDEX HANDLER _bm25_amhandler)以及 bm25_ops 操作符类。

创建 bm25 索引

稳定发布版(v0.1 – v0.3,bm25vector)

CREATE INDEX documents_embedding_bm25
ON documents
USING bm25 (embedding bm25_ops);

新一代 main 分支(tsvector)

在 main 分支中,bm25_ops 直接作用于 tsvector,并支持索引级存储参数(options、limit 和 prefilter):

CREATE INDEX documents_vector_bm25
ON documents
USING bm25 (vector bm25_ops)
WITH (
    options = $$
    k1 = 1.2
    b = 0.75
    $$,
    limit = 100,
    prefilter = false
);
存储参数 (WITH (...)) 类型 默认值 取值范围 说明
options (k1) float(TOML 内) 1.2 1.2 – 2.0 词频饱和度参数 k1k_1。数值越大,文档内重复出现的词项权重越高。
options (b) float(TOML 内) 0.75 0.0 – 1.0 文档长度归一化参数 bb。0.0 禁用长度归一化;1.0 完全按相对于 avgdl\text{avgdl} 的文档长度进行归一化。
limit int 0 0 – 65535 存储在索引上的默认 Top-K 检索上限(当会话 GUC bm25.limit = 0 时使用)。
prefilter bool false true / false 存储在索引上的默认预过滤行为(可在会话级通过 bm25.prefilter 覆盖)。

你可以使用 ALTER INDEX 动态更新现有索引上的 limit 或 prefilter 参数:

ALTER INDEX documents_vector_bm25 SET (limit = 200, prefilter = true);
ALTER INDEX documents_vector_bm25 RESET (limit, prefilter);

自动分词触发器(v0.1 – v0.3)

使用 pg_tokenizer 时,可以通过触发器在执行 INSERT 和 UPDATE 时自动同步更新 bm25vector 列:

使用自定义模型触发器(一步到位辅助函数)

create_custom_model_tokenizer_and_trigger 会一步完成自定义模型创建、分词器创建、存量数据回填,并自动挂载 BEFORE INSERT OR UPDATE 触发器:

SELECT create_custom_model_tokenizer_and_trigger(
    tokenizer_name => 'my_tokenizer',
    model_name     => 'my_model',
    text_analyzer_name => 'my_analyzer',
    table_name     => 'documents',
    source_column  => 'passage',
    target_column  => 'embedding'
);

将标准 PL/pgSQL 触发器用于预训练模型

如果你使用的是预训练模型(如 llmlingua2 或 bert_base_uncased),可以创建一个标准的触发器函数:

CREATE OR REPLACE FUNCTION documents_tokenize_trigger()
RETURNS trigger AS $$
BEGIN
    NEW.embedding := tokenize(NEW.passage, 'my_tokenizer');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_documents_tokenize
BEFORE INSERT OR UPDATE OF passage ON documents
FOR EACH ROW
EXECUTE FUNCTION documents_tokenize_trigger();

段架构:增长段(Growing)与封闭段(Sealed)(v0.1 – v0.3)

在内部实现中,vchord_bm25 将索引元组组织为两种段状态:

  1. 增长段(Growing Segment):新写入的元组(INSERT / UPDATE)会追加到增长段中,从而无需重新压缩整条倒排链表即可获得极高的写入吞吐量。
  2. 封闭段(Sealed Segment):当增长段超过 bm25_catalog.segment_growing_max_page_size 个页面(默认 4096 页,约 32 MB)时,它会被封存为不可变的只读段,采用差分位打包(Delta-Bitpacked)的 128 文档块结构,并预计算块级最大得分上界与词项文档频率(DF)统计信息。
-- 调整增长段封存阈值(单位为 8 KB 页面,取值范围 1..1000000)
SET bm25_catalog.segment_growing_max_page_size = 4096;

并行索引构建与进度监控

索引构建支持 PostgreSQL 并行维护工作进程(Parallel Maintenance Workers),并可通过 pg_stat_progress_create_index 跨三个阶段实时汇报构建进度:

  1. initializing(初始化)
  2. scanning table(扫描表)
  3. writing index structures(写入索引结构)
-- 使用并行工作进程加速大规模索引构建
SET max_parallel_maintenance_workers = 8;
SET maintenance_work_mem = '2GB';

-- 在另一个会话中监控构建进度
SELECT
    pid,
    phase,
    tuples_total,
    tuples_done,
    round(100.0 * tuples_done / nullif(tuples_total, 0), 2) AS pct_done
FROM pg_stat_progress_create_index;

检查索引页(v0.3.0)

0.3.0 版本提供了 bm25_page_inspect(index regclass, blkno int) 函数,用于检查 bm25 索引的内部元数据与页头信息:

-- 检查 bm25 索引的元信息页(第 0 块)
SELECT bm25_page_inspect('documents_embedding_bm25', 0);

索引约束与限制

  • 仅支持单列索引:bm25 索引只能创建在单个 bm25vector(或 tsvector)列上,不支持多列联合 bm25 索引。
  • 不支持 Unlogged 表:bm25 访问方法不支持 UNLOGGED 表(调用 ambuildempty 会抛出 Unlogged indexes are not supported 错误)。
  • 仅支持正向扫描:bm25 索引扫描在 <&> 操作符上仅支持正向(ASC)扫描方向。

2 - 查询与相关性评分

执行 Top-K BM25 查询、理解负值 BM25 得分并调优检索上限参数。

基础 Top-K 查询语法

要运行经索引加速的 Top-K BM25 检索,请在 ORDER BY 子句中将 <&> 操作符与 to_bm25query 结合使用:

-- v0.1 – v0.3 (bm25vector + pg_tokenizer)
SELECT
    id,
    passage,
    embedding <&> to_bm25query('documents_embedding_bm25', tokenize('PostgreSQL vector search', 'my_tokenizer')) AS raw_score,
    -(embedding <&> to_bm25query('documents_embedding_bm25', tokenize('PostgreSQL vector search', 'my_tokenizer'))) AS bm25_score
FROM documents
ORDER BY raw_score ASC
LIMIT 10;
-- main 分支(原生 tsvector)
SELECT
    id,
    passage,
    -(vector <&> to_bm25query(to_tsvector('english', 'PostgreSQL vector search'), 'documents_bm25_idx')) AS bm25_score
FROM documents
ORDER BY vector <&> to_bm25query(to_tsvector('english', 'PostgreSQL vector search'), 'documents_bm25_idx') ASC
LIMIT 10;

为什么 to_bm25query 需要传入索引名称

与向量距离操作符不同(两个向量之间的距离仅取决于这两个向量本身),一篇文档的 BM25 得分依赖于存储在 BM25 索引中的全语料库统计信息:

  • 语料库中的文档总数(NN)
  • 全语料库的平均文档长度(avgdl\text{avgdl})
  • 每个查询词元在语料库中的文档频率(n(qi)n(q_i))

将索引('documents_embedding_bm25'::regclass)传入 to_bm25query(...),使得独立评分函数(search_bm25query / _bm25_evaluate)即使在 PostgreSQL 于 SELECT 投影列表中求值 <&> 或执行暴力顺序扫描(Sequential Scan)时,也能从索引中读取语料库统计信息。

理解负值 BM25 得分

根据设计,<&> 返回的是负值 BM25 得分:

scoreoperator(D,Q)=−BM25(D,Q)≤0\text{score}_{\text{operator}}(D, Q) = -\text{BM25}(D, Q) \le 0
  • 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 最小堆,以激进地剪枝缺乏竞争力的倒排块,因此索引扫描器在扫描开始时就需要知道 KK(即最多需要收集多少个候选文档)。

在 v0.1.0 – v0.3.0 中:bm25_catalog.bm25_limit

GUC 参数 bm25_catalog.bm25_limit(默认值为 100,取值范围 -1 至 65535)控制单次 bm25 索引扫描最多返回的 Top-K 文档数量:

-- 默认值为 100。如果你的 SQL 查询请求 LIMIT 500,请先调大该参数:
SET bm25_catalog.bm25_limit = 500;

SELECT id, passage
FROM documents
ORDER BY embedding <&> to_bm25query('documents_embedding_bm25', tokenize('database', 'my_tokenizer'))
LIMIT 500;

bm25_catalog.bm25_limit 的特殊取值说明:

取值 行为说明
1 – 65535 使用 Block-WeakAnd 计算并返回最多 bm25_limit 条高分文档(默认 100)。
0 立即返回 0 行。
-1 禁用 Block-WeakAnd Top-K 剪枝,对索引中所有匹配的倒排项执行全量暴力扫描。

在 main 分支中:bm25.limit 与索引 limit 存储参数

在 main 分支中,实际生效的检索上限按以下优先级顺序决定:

  1. 会话 GUC 参数 bm25.limit(若 > 0)
  2. 索引存储参数 WITH (limit = ...)(若 > 0)
SET bm25.limit = 100;
警告

请始终确保 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:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM documents
ORDER BY embedding <&> to_bm25query('documents_embedding_bm25', tokenize('PostgreSQL', 'my_tokenizer'))
LIMIT 10;

如需强制执行顺序扫描 + 精确暴力求值(常用于验证召回率或调试),可以临时禁用索引扫描:

-- v0.1 – v0.3
SET bm25_catalog.enable_index = false;

-- main 分支
SET bm25.enable_scan = false;

3 - 过滤检索与预过滤

通过后过滤、预过滤和部分索引,将 SQL WHERE 子句与 BM25 Top-K 相关性排序相结合。

在实际业务场景中,全文 BM25 排序通常需要与结构化关系过滤条件(例如 category_id = 5、tenant_id = 'acme' 或时间范围)结合使用。vchord_bm25 支持三种带过滤条件的 BM25 检索策略。

1. 后过滤(Post-Filtering)与预过滤(Pre-Filtering)

当你执行一条同时包含 WHERE 子句和 ORDER BY embedding <&> to_bm25query(...) LIMIT K 的查询时:

SELECT id, passage
FROM documents
WHERE category = 'database'
ORDER BY embedding <&> to_bm25query('documents_embedding_bm25', tokenize('PostgreSQL BM25', 'my_tokenizer'))
LIMIT 10;

vchord_bm25 可以通过以下两种方式评估 WHERE 谓词:

后过滤(enable_prefilter = false)

  1. bm25 索引在整个语料库上运行 Block-WeakAnd,返回全局前 bm25_limit 名候选文档(例如全局 Top 100)。
  2. PostgreSQL 回表取出这 100 条堆表元组,并应用 WHERE category = 'database' 过滤条件。
  3. 注意事项:如果全局 Top-100 文档中只有 3 篇满足 category = 'database',那么即使表里还有数百篇 BM25 得分略低的 'database' 类别文档,查询也只会返回 3 行!

预过滤(enable_prefilter = true)

启用预过滤后,vchord_bm25 会在将候选文档推入 Block-WeakAnd Top-K 堆之前,先行检查堆表可见性并评估过滤条件是否满足:

  1. 不满足过滤条件的文档绝不会进入 Top-K 堆,也不会抬高 Block-WeakAnd 的剪枝阈值 θ\theta。
  2. 索引扫描会持续推进,直到找齐 KK 篇既满足 WHERE 过滤条件、又在所有匹配行中拥有最高 BM25 得分的文档。

启用预过滤

在 v0.2.2 – v0.3.0 中:

-- 在 v0.3.0 中默认启用;可在会话级动态切换:
SET bm25_catalog.enable_prefilter = true;

在 main 分支中:

-- 在当前会话中启用:
SET bm25.prefilter = true;

-- 或在索引级别默认启用:
ALTER INDEX documents_bm25_idx SET (prefilter = true);

2. 面向高选择性分区键的部分 BM25 索引(Partial Index)

如果你经常按固定的一组分区键(例如 status = 'published' 或特定 tenant_id)进行过滤,可以创建部分 bm25 索引(Partial Index):

CREATE INDEX documents_published_bm25
ON documents
USING bm25 (embedding bm25_ops)
WHERE status = 'published';

查询部分索引时,请在 to_bm25query 中指定该部分索引的名称,并在 SQL 中附上匹配的 WHERE 条件:

SELECT id, passage
FROM documents
WHERE status = 'published'
ORDER BY embedding <&> to_bm25query('documents_published_bm25', tokenize('PostgreSQL BM25', 'my_tokenizer'))
LIMIT 10;

选择合适的过滤策略

过滤条件选择率 推荐策略 原因说明
高匹配率(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)

结合稀疏词法 BM25 检索 (vchord_bm25) 与稠密向量检索 (vchord),通过倒数排序融合 (RRF) 与 ColBERT 重排序提升检索精度。

现代检索增强生成(RAG)与搜索流水线通常将稠密向量检索(vchord,擅长捕捉语义相似性)与稀疏词法 BM25 检索(vchord_bm25,擅长捕捉精确关键词、标识符与罕见词匹配)结合使用。由于这两个扩展均原生运行在 PostgreSQL 内部,你可以在单条 SQL 查询中端到端执行混合检索,无需任何外部编排服务。

CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
CREATE EXTENSION IF NOT EXISTS pg_tokenizer CASCADE;
CREATE EXTENSION IF NOT EXISTS vchord_bm25 CASCADE;

SET search_path TO "$user", public, bm25_catalog, tokenizer_catalog;

-- 1. 创建 BM25 分词器
SELECT create_tokenizer('bert', $$
model = "bert_base_uncased"
$$);

-- 2. 创建同时存储稠密向量与稀疏 bm25vector 的数据表
CREATE TABLE chunks (
    id SERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    dense_emb vector(768),
    bm25_emb bm25vector
);

-- 3. 创建 VectorChord 稠密向量索引 (vchordrq) 与 BM25 词法索引 (bm25)
CREATE INDEX chunks_dense_idx ON chunks USING vchordrq (dense_emb vector_l2_ops);
CREATE INDEX chunks_bm25_idx  ON chunks USING bm25 (bm25_emb bm25_ops);

方式一:纯 SQL 实现倒数排序融合(RRF)

倒数排序融合(Reciprocal Rank Fusion,RRF) 能够直接融合来自多个检索系统的排名结果,而无需在 L2/余弦距离与 BM25 得分之间做复杂的分数归一化校准:

RRF_Score(d)=wdensek+rankdense(d)+wbm25k+rankbm25(d)\text{RRF\_Score}(d) = \frac{w_{\text{dense}}}{k + \text{rank}_{\text{dense}}(d)} + \frac{w_{\text{bm25}}}{k + \text{rank}_{\text{bm25}}(d)}

其中 kk 为平滑常数(通常取 k=60k = 60)。

WITH dense_results AS (
    SELECT
        id,
        RANK() OVER (ORDER BY dense_emb <-> '[0.012, -0.034, ...]'::vector) AS dense_rank
    FROM chunks
    ORDER BY dense_emb <-> '[0.012, -0.034, ...]'::vector
    LIMIT 100
),
bm25_results AS (
    SELECT
        id,
        RANK() OVER (
            ORDER BY bm25_emb <&> to_bm25query('chunks_bm25_idx', tokenize('PostgreSQL BM25 hybrid search', 'bert'))
        ) AS bm25_rank
    FROM chunks
    ORDER BY bm25_emb <&> to_bm25query('chunks_bm25_idx', tokenize('PostgreSQL BM25 hybrid search', 'bert'))
    LIMIT 100
),
fused AS (
    SELECT
        COALESCE(d.id, b.id) AS id,
        d.dense_rank,
        b.bm25_rank,
        COALESCE(1.0 / (60 + d.dense_rank), 0.0) +
        COALESCE(1.0 / (60 + b.bm25_rank), 0.0) AS rrf_score
    FROM dense_results d
    FULL OUTER JOIN bm25_results b ON d.id = b.id
    ORDER BY rrf_score DESC
    LIMIT 10
)
SELECT
    c.id,
    c.content,
    f.dense_rank,
    f.bm25_rank,
    round(f.rrf_score::numeric, 6) AS rrf_score
FROM fused f
JOIN chunks c ON c.id = f.id
ORDER BY f.rrf_score DESC;

方式二:混合召回 + ColBERT 晚期交互重排序

如果追求更高的检索精度,可以先使用 vchordrq(稠密检索)+ vchord_bm25(词法检索)召回候选文档并集,再在 PostgreSQL 内使用 VectorChord 的 @# (max_sim) 多向量 ColBERT 操作符进行精细重排序:

WITH dense_candidates AS (
    SELECT id FROM chunks
    ORDER BY dense_emb <-> $1::vector
    LIMIT 50
),
bm25_candidates AS (
    SELECT id FROM chunks
    ORDER BY bm25_emb <&> to_bm25query('chunks_bm25_idx', tokenize($2, 'bert'))
    LIMIT 50
),
candidate_ids AS (
    SELECT id FROM dense_candidates
    UNION
    SELECT id FROM bm25_candidates
)
SELECT
    c.id,
    c.content,
    -(c.colbert_emb @# $3::vector[]) AS colbert_maxsim_score
FROM candidate_ids u
JOIN chunks c ON c.id = u.id
ORDER BY c.colbert_emb @# $3::vector[] ASC
LIMIT 10;
提示

请参阅我们的工程深度解析博客 在 PostgreSQL 中使用 ColBERT 重排序为向量检索加速 以及 使用 Postgres 原生 BM25 与 VectorChord 实现混合检索,了解在 PostgreSQL 内部执行“稠密向量 + BM25 + ColBERT 重排序”的完整 BEIR 基准测试对比。

5 - BM25 与 Block-WeakAnd 内部原理

vchord_bm25 中 Okapi BM25、单字节 Fieldnorm 量化、差分位打包与 Block-WeakAnd 的数学公式与索引架构详解。

本页详细介绍 vchord_bm25(位于 crates/bm25/src/bm25.rs 与 src/algorithm/block_wand.rs)中实现的精确评分公式与底层数据结构。

1. Okapi BM25 评分公式

给定一篇文档 DD 和一个查询 Q={q1,q2,…,qm}Q = \{q_1, q_2, \dots, q_m\},其中查询词频为 c(qi,Q)c(q_i, Q),文档内词频为 f(qi,D)f(q_i, D),vchord_bm25 按如下公式计算相关性得分:

BM25(D,Q)=∑qi∈Q∩Dc(qi,Q)⋅IDF(qi)⋅TF(qi,D)\text{BM25}(D, Q) = \sum_{q_i \in Q \cap D} c(q_i, Q) \cdot \text{IDF}(q_i) \cdot \text{TF}(q_i, D)

逆文档频率(IDF\text{IDF})

IDF(qi)=ln⁡(N+1.0n(qi)+0.5)\text{IDF}(q_i) = \ln\left(\frac{N + 1.0}{n(q_i) + 0.5}\right)

其中:

  • NN 为表中已索引文档的总数。
  • n(qi)n(q_i) 为包含词元 qiq_i 的文档数量(即文档频率 DF)。

饱和词频(TF\text{TF})

TF(qi,D)=f(qi,D)⋅(k1+1)f(qi,D)+k1⋅(1−b+b⋅∣D∣^avgdl)\text{TF}(q_i, D) = \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{\hat{|D|}}{\text{avgdl}}\right)}

其中:

  • f(qi,D)f(q_i, D) 为词元 qiq_i 在文档 DD 中出现的词频。
  • ∣D∣^\hat{|D|} 为从单字节 fieldnorm ID 解码得到的量化文档长度(文档 DD 中的总词元数)。
  • avgdl\text{avgdl} 为所有已索引文档的平均文档长度。
  • k1∈[1.2,2.0]k_1 \in [1.2, 2.0](默认值为 1.2)用于控制词频饱和度。
  • b∈[0.0,1.0]b \in [0.0, 1.0](默认值为 0.75)用于控制文档长度归一化强度。

2. 单字节 Fieldnorm 量化(FIELDNORM_TO_LENGTH)

如果为每篇文档都存储 32 位浮点数或整数长度,并在对每个倒排项评分时执行浮点除法,将消耗大量的内存带宽和 CPU 周期。为此,vchord_bm25 使用包含 256 个表项的 FIELDNORM_TO_LENGTH 查找表,将文档长度 ∣D∣|D| 量化为单个字节(u8,0..=255):

  • 短文档精确表示(0 – 40 个词元):取值 0..=40 与真实词元数量呈 1:1 精确映射。
  • 长文档对数步长增长(41 – 255):步长随长度呈对数级递增(例如 42, 44, 46, ...,最高可覆盖 2,013,265,944 个词元)。
  • 256 项预计算得分缓存:由于 fieldnorm 只有 256 种可能取值,vchord_bm25 会在查询开始时针对每个查询词预先计算出全部 256 种可能文档长度的分母项:
s0(qi)=IDF(qi)⋅(k1+1),s1[fn]=k1⋅(1−b+b⋅FIELDNORM_TO_LENGTH[fn]avgdl)s_0(q_i) = \text{IDF}(q_i) \cdot (k_1 + 1), \qquad s_1[\text{fn}] = k_1 \cdot \left(1 - b + b \cdot \frac{\text{FIELDNORM\_TO\_LENGTH}[\text{fn}]}{\text{avgdl}}\right)

如此一来,在索引扫描期间为候选倒排项打分就简化为一次查表与一次浮点除法:

score(qi,D)=f(qi,D)⋅s0(qi)f(qi,D)+s1[fieldnorm(D)]\text{score}(q_i, D) = \frac{f(q_i, D) \cdot s_0(q_i)}{f(q_i, D) + s_1[\text{fieldnorm}(D)]}

3. 倒排链压缩与 Block-WeakAnd(Block-Max WAND)

基于差分位打包(Delta-Bitpacking)的 128 文档定长块

对于封闭段(Sealed Segment)中的每个词项,由 (doc_id, term_frequency) 组成的倒排链表会被划分为每块 128 篇文档的定长块,并采用**差分位打包(Delta-Bitpacking)**进行压缩:

  1. 块内文档 ID 按升序排列,存储相邻文档 ID 的差值(Δi=doci−doci−1\Delta_i = \text{doc}_i - \text{doc}_{i-1}),并按块内最大差值所需的最小位宽进行位打包。
  2. 每个块头记录该块的最后一个文档 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
  1. 枢轴选择(Pivot Selection):所有查询词游标按其当前指向的 doc_id 升序排列。从第一个游标开始,vchord_bm25 依次累加各词项的全局得分上界 max_score,直到累加和超过 Top-K 最小堆中当前的第 KK 高分阈值 θ\theta。使累加和跨过 θ\theta 的那个游标所指向的文档即被选为枢轴文档(Pivot Document)。
  2. 块级上界精化(Block-Max Refinement):在不解压任何倒排块的前提下,vchord_bm25 将前导游标浅层定位(Shallow Seek)到枢轴所在的 128 文档块,并累加更紧致的块级得分上界(block_max_score)。
  3. 快速跳块(Fast Block Skipping):如果各游标的 block_max_score 之和 ≤θ\le \theta,说明当前块交集中的任何文档都不可能进入 Top-K 结果——扫描器会直接跳过该块,完全不必解码文档 ID 或词频数据。
  4. 精确求值(Exact Evaluation):只有当块级上界之和超过 θ\theta 且所有前导游标均对齐到 pivot_doc 时,vchord_bm25 才会解码该数据块、检查 MVCC 可见性/删除位图(以及可选的预过滤条件)、计算精确的 BM25 得分,并更新 Top-K 最小堆。