跳转到主要内容

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

返回本页常规视图.

vchord_bm25 与 pg_tokenizer

面向 PostgreSQL 的原生 BM25 排序索引与 Block-WeakAnd 全文检索扩展(vchord_bm25 与 pg_tokenizer)。

vchord_bm25(由 Pigsty 在 pgsty/vchord_bm25 维护)是一个使用 Rust(pgrx)构建的原生 PostgreSQL 扩展,在 PostgreSQL 内部实现了 Okapi BM25 相关性排序算法以及 Block-WeakAnd(Block-Max WAND) Top-K 索引扫描访问方法。

配合 pg_tokenizer(由 Pigsty 在 pgsty/pg_tokenizer 维护,提供可定制的多语言分词与词表模型)或 PostgreSQL 内置的 tsvector 分析器,vchord_bm25 将 Elasticsearch/Lucene 级别的词法全文检索排序能力直接带入关系型数据库,并能与 vchord 无缝结合,实现语义 + 词法混合检索。

了解为什么需要 BM25,安装 vchord_bm25 与 pg_tokenizer,并运行你的第一条全文排序查询。

构建 bm25 索引、执行 Top-K 查询、使用 WHERE 预过滤,并将 BM25 与 VectorChord 稠密向量检索融合。

配置文本分析器、词干提取器、停用词、同义词、HuggingFace 模型、中文结巴(Jieba)与日文 Lindera 分词。

完整的 SQL 类型、操作符、函数、GUC 参数、版本演进历史以及 BEIR 基准测试结果。

核心特性

  • 纯正的 Okapi BM25 评分:完整支持逆文档频率(IDF)、词频饱和度控制(k1k_1)以及单字节量化的文档长度归一化(bb 与平均文档长度 avgdl\text{avgdl}),从根本上解决了 PostgreSQL 原生 ts_rank 的相关性排序局限。
  • Block-WeakAnd 索引加速:使用差分位打包(Delta-Bitpacking)存储压缩倒排链表,并在每个块中记录最大得分上界,在 Top-K 查询期间直接跳过缺乏竞争力的文档块,无需扫描整条倒排链。
  • 多语言分词支持:在 v0.1–v0.3 中可配合 pg_tokenizer(支持 llmlingua2、bert_base_uncased、wiki_tocken、gemma2b、自定义语料训练模型、中文 jieba、日文 lindera)使用;在新一代 main 架构中更可直接支持 PostgreSQL 原生 tsvector。
  • 与 VectorChord 原生混合检索:在单条 SQL 查询中同时运行词法 BM25 检索(vchord_bm25)与稠密向量检索(vchordrq),支持倒数排序融合(RRF)或 ColBERT MaxSim 重排序。

1 - 快速上手

vchord_bm25 与 pg_tokenizer 的架构概览、安装方式与快速入门指南。

在 PostgreSQL 中快速上手 vchord_bm25:

了解为什么 BM25 + Block-WeakAnd 在相关性与性能上显著优于 PostgreSQL 原生 ts_rank 与 GIN 索引。

通过 Pigsty、Docker、Debian 软件包或源码编译安装 vchord_bm25 与 pg_tokenizer。

在几分钟内完成文本分词、构建 bm25 索引并运行 Top-K 全文排序查询。

1.1 - 架构概览

了解 VectorChord-BM25 如何为 PostgreSQL 带来原生 Okapi BM25 评分与 Block-WeakAnd Top-K 检索。

PostgreSQL 通过 tsvector、tsquery、ts_rank 和 GIN 索引提供了内置的全文检索能力。虽然它在布尔关键词匹配(@@)方面表现良好,但在现代搜索与检索增强生成(RAG)工作负载下,PostgreSQL 内置的相关性排序存在两个结构性局限:

  1. 排序公式不理想(ts_rank):PostgreSQL 的 ts_rank 和 ts_rank_cd 不会在索引中维护全语料库级别的逆文档频率(IDF)或平均文档长度(avgdl\text{avgdl})。常见词与罕见的高信息量词被赋予相同的权重,且词频(TF)缺乏合理的饱和度控制。
  2. ORDER BY ... LIMIT K 必须全量扫描:PostgreSQL 的 GIN 索引只能加速布尔匹配(WHERE tsv @@ tsquery)。为了返回得分最高的 Top-10 文档,PostgreSQL 必须从堆表(Heap)中取出所有匹配行,逐一计算 ts_rank,再对整个结果集进行排序。

VectorChord-BM25 如何解决这两个问题

VectorChord-BM25 (vchord_bm25) 引入了专为 Top-K 词法检索设计的 PostgreSQL 索引访问方法(USING bm25)和稀疏 BM25 评分操作符(<&>):

能力维度 PostgreSQL 内置 (GIN + ts_rank) VectorChord-BM25 (vchord_bm25)
排序模型 局部词频 / 词距 (ts_rank) 全语料库 Okapi BM25(IDF\text{IDF}、k1k_1、bb、avgdl\text{avgdl})
Top-K 执行方式 取出并计算所有匹配元组后再排序 索引内 Block-WeakAnd (Block-Max WAND) 提前剪枝
倒排链压缩 未压缩或基础变长整数 TID 列表 128 文档分块、对 SIMD 友好的差分位打包(Delta-Bitpacking)
文档长度归一化 可选的粗粒度位掩码标志 单字节 Fieldnorm 量化(256 项查找表)
查询语义 要求布尔 AND/OR 精确匹配 跨所有查询词的非合取(Non-conjunctive)Top-K 相关性排序
分词能力 内置 ts_debug / Snowball pg_tokenizer(LLM 分词器、jieba、lindera、自定义词表)或原生 tsvector

架构概览

flowchart LR
    subgraph Tokenization["1. 分词层 (Tokenization Layer)"]
        T1["原始文本 (Raw Text)"] --> T2["pg_tokenizer (v0.1–v0.3)\n或 to_tsvector (main)"]
        T2 --> T3["bm25vector / tsvector\n(词元 ID + 词频)"]
    end
    subgraph Index["2. BM25 索引 (USING bm25)"]
        I1["增长段 (Growing Segment)\n(仅追加页)"] --> I2["封闭段 (Sealed Segment)\n(Delta-Bitpacked 128 文档块)"]
        I2 --> I3["词项统计信息 (DF)\n+ 单字节 Fieldnorm 数组"]
    end
    subgraph Query["3. Top-K 查询 (ORDER BY vec <&> bm25query LIMIT k)"]
        Q1["块级最大得分上界 (Block-Max)"] --> Q2["Block-WeakAnd 枢轴剪枝"]
        Q2 --> Q3["Top-K 最小堆 (-BM25 得分)"]
    end
    T3 --> Index
    Index --> Query

两代 API 演进

在使用 vchord_bm25 时,请注意不同版本间的两代 API 设计:

  1. 稳定发布版(v0.1.0 – v0.3.0,模式 bm25_catalog):

    • 使用自定义数据类型 bm25vector(稀疏的 token_id:frequency 映射,例如 {1:1, 2:2})和 bm25query。
    • 配合 pg_tokenizer 扩展(tokenizer_catalog)使用,将文本转换为 INT[] 词元 ID 数组,并可隐式转换为 bm25vector。
    • 创建索引:CREATE INDEX idx ON documents USING bm25 (embedding bm25_ops);
    • 构造查询:to_bm25query('idx', tokenize('query text', 'tokenizer_name'))
  2. 新一代原生 tsvector 架构(main 分支):

    • 直接作用于 PostgreSQL 内置的 tsvector 数据类型(CREATE OPERATOR CLASS bm25_ops FOR TYPE tsvector USING bm25),无需额外维护单独的 bm25vector 列。
    • 创建索引:CREATE INDEX idx ON documents USING bm25 (vector bm25_ops) WITH (options = $$ k1 = 1.2, b = 0.75 $$);
    • 构造查询:to_bm25query(to_tsvector('english', 'query text'), 'idx')
提示

本指南在各章节中均并列提供了这两种工作流的完整示例,你可以根据实际部署的版本直接选用对应的写法。

1.2 - 安装指南

通过 Pigsty、Docker、Debian 软件包或源码编译安装 VectorChord-BM25 (vchord_bm25) 与 pg_tokenizer。

vchord_bm25 支持 Linux(x86_64 与 aarch64)及 macOS 平台上的 PostgreSQL 14、15、16、17 和 18。

Pigsty 为 EL 8/9/10、Debian 12/13 以及 Ubuntu 22.04/24.04 上的 PostgreSQL 14–18 提供了预构建的 vchord、vchord_bm25 和 pg_tokenizer 原生 RPM 与 DEB 软件包。

# 通过 Pigsty CLI 安装(自动识别 PG 版本与操作系统发行版)
pig ext install vchord_bm25
pig ext install pg_tokenizer

配置好 Pigsty 软件源后,也可以直接通过 apt 或 dnf 安装:

# Debian / Ubuntu(将 17 替换为你的 PostgreSQL 主版本号)
sudo apt-get install postgresql-17-vchord-bm25 postgresql-17-pg-tokenizer

# RHEL / Rocky / AlmaLinux(将 17 替换为你的 PostgreSQL 主版本号)
sudo dnf install vchord_bm25_17 pg_tokenizer_17

方式二:Docker(tensorchord/vchord-suite)

官方 tensorchord/vchord-suite Docker 镜像开箱即用地集成了 PostgreSQL、VectorChord (vchord)、VectorChord-BM25 (vchord_bm25) 以及 pg_tokenizer:

docker run \
  --name vchord-suite \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d tensorchord/vchord-suite:pg18-latest
说明

你也可以从 GitHub Container Registry 拉取镜像:ghcr.io/tensorchord/vchord-suite:pg18-latest(或 pg17-latest、pg16-latest、pg15-latest、pg14-latest)。

使用 psql 连接数据库:

psql -h localhost -p 5432 -U postgres

方式三:GitHub Release 软件包(.deb 与 RHEL .zip)

从 pgsty/vchord_bm25(上游 supervc-stack/VectorChord-bm25)与 pgsty/pg_tokenizer(上游 supervc-stack/pg_tokenizer.rs)的 GitHub Releases 页面下载对应 PostgreSQL 主版本(14–18)和 CPU 架构(amd64 / x86_64 或 arm64 / aarch64)的 .deb 或 RHEL 9+ .zip 软件包:

Debian / Ubuntu(.deb)

# 安装 vchord_bm25(以 x86_64 上的 PostgreSQL 16 为例)
curl -LO https://github.com/supervc-stack/VectorChord-bm25/releases/download/0.3.0/postgresql-16-vchord-bm25_0.3.0-1_amd64.deb
sudo apt-get install ./postgresql-16-vchord-bm25_0.3.0-1_amd64.deb

# 安装 pg_tokenizer(以 x86_64 上的 PostgreSQL 16 为例)
curl -LO https://github.com/supervc-stack/pg_tokenizer.rs/releases/download/0.1.1/postgresql-16-pg-tokenizer_0.1.1-1_amd64.deb
sudo apt-get install ./postgresql-16-pg-tokenizer_0.1.1-1_amd64.deb

RHEL 9+ / Enterprise Linux(.zip 归档包)

在 RHEL 9+ 系统(要求 glibc >= 2.34)中手动通过 Release .zip 归档包安装时:

# 1. 查询 PostgreSQL 的动态库目录与扩展共享目录
pg_config --pkglibdir
# 示例输出:/usr/pgsql-16/lib
pg_config --sharedir
# 示例输出:/usr/pgsql-16/share

# 2. 下载并解压发布归档包
wget https://github.com/supervc-stack/pg_tokenizer.rs/releases/download/0.1.1/pg16-pg_tokenizer-0.1.1-x86_64-unknown-linux-gnu.zip
unzip pg16-pg_tokenizer-0.1.1-x86_64-unknown-linux-gnu.zip

# 3. 拷贝共享库 (.so) 与扩展控制/SQL 脚本文件
sudo cp ./usr/lib/postgresql/16/lib/pg_tokenizer.so $(pg_config --pkglibdir)
sudo cp ./usr/share/postgresql/16/extension/* $(pg_config --sharedir)/extension

方式四:从源码编译(cargo-pgrx)

如需从源码编译 pgsty/vchord_bm25 与 pgsty/pg_tokenizer:

# 1. 安装 Rust 工具链及系统构建依赖(clang、libclang、pkg-config、PostgreSQL 开发头文件)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. 编译并安装 vchord_bm25
git clone https://github.com/pgsty/vchord_bm25.git
cd vchord_bm25
cargo install cargo-pgrx --version $(grep -o 'pgrx = "=[^"]*' Cargo.toml | cut -d = -f 3)
cargo pgrx init --pg17=$(which pg_config)
cargo pgrx install --release --pg-config $(which pg_config)

# 3. 编译并安装 pg_tokenizer
git clone https://github.com/pgsty/pg_tokenizer.git
cd pg_tokenizer
cargo pgrx install --release --pg-config $(which pg_config)
提示

编译期启用可选的 Lindera 词典特性:如果希望将 Lindera 中日韩形态学词典(ipadic、ipadic-neologd、unidic、ko-dic、cc-cedict)直接内嵌编译到 pg_tokenizer.so 中,请在编译 pg_tokenizer 时传入 --features 参数:

cargo pgrx install --release --pg-config $(which pg_config) \
  --features "lindera-ipadic lindera-ipadic-neologd lindera-unidic lindera-ko-dic lindera-cc-cedict"

本地开发与调试(cargo-pgrx)

在本地开发或调试 vchord_bm25 与 pg_tokenizer 时:

# 初始化 pgrx 托管的 PostgreSQL 实例(PG 14–18)
cargo pgrx init

# 使用 dev-opt 构建配置,在开启编译优化的同时保留调试符号
RUST_BACKTRACE=1 cargo pgrx run --profile dev-opt

# 在交互式 psql 会话中查询后端进程 PID,以便挂载 gdb 调试:
SELECT pg_backend_pid();

配置 PostgreSQL 并启用扩展

如果你使用 pg_tokenizer 并希望在数据库服务启动时预加载词表模型,请将 pg_tokenizer(以及同时使用的 vchord)添加到 postgresql.conf 的 shared_preload_libraries 中:

shared_preload_libraries = 'vchord.so, pg_tokenizer.so'

修改 shared_preload_libraries 后请重启 PostgreSQL。(vchord_bm25 本身不要求配置 shared_preload_libraries,可通过 CREATE EXTENSION 在会话中动态加载。)

2. 创建扩展并设置 search_path

连接到目标数据库并启用扩展:

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

在 v0.1.0–v0.3.0 版本中,vchord_bm25 会将其对象安装到 bm25_catalog 模式,而 pg_tokenizer 会安装到 tokenizer_catalog 模式。请将这两个模式加入你的 search_path:

-- 仅在当前会话生效
SET search_path TO "$user", public, bm25_catalog, tokenizer_catalog;

-- 或针对数据库 / 角色持久化保存
ALTER DATABASE postgres SET search_path TO "$user", public, bm25_catalog, tokenizer_catalog;

验证已安装的扩展版本:

SELECT extname, extversion FROM pg_extension WHERE extname IN ('vchord_bm25', 'pg_tokenizer');

1.3 - 快速开始

使用 vchord_bm25 在 PostgreSQL 中运行你的第一条 BM25 全文排序检索查询。

本指南将带你一步步创建数据表、对文档进行分词、构建 bm25 索引,并执行 Top-K BM25 检索查询。

工作流 A:bm25vector + pg_tokenizer(v0.1 – v0.3)

在已发布的 v0.1.0 至 v0.3.0 版本中,vchord_bm25 与 pg_tokenizer 配合使用,将文本分词并存储到 bm25vector 列中。

1. 启用扩展并配置 search_path

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;

2. 创建分词器

使用 create_tokenizer 搭配内置的 llmlingua2(或 bert_base_uncased)模型创建分词器:

SELECT create_tokenizer('my_tokenizer', $$
model = "llmlingua2"
$$);
提示

需要领域专属词表、中文分词(jieba)或日文分词(lindera)?请参阅 分词器 (pg_tokenizer) 章节。

3. 创建表并写入文档

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    passage TEXT NOT NULL,
    embedding bm25vector
);

INSERT INTO documents (passage) VALUES
  ('PostgreSQL is a powerful, open-source object-relational database system. It has over 35 years of active development.'),
  ('Full-text search is a technique for searching a single computer-stored document or a collection in a full-text database.'),
  ('BM25 is a bag-of-words retrieval function that ranks a set of documents based on the query terms appearing in each document.'),
  ('VectorChord-bm25 is a PostgreSQL extension that provides native BM25 ranking index using the Block-WeakAnd algorithm.');

4. 将文档分词为 bm25vector

tokenize(text, tokenizer_name) 返回一个由词元 ID 组成的 INT[] 数组,它可以隐式转换为 bm25vector(自动将重复的词元 ID 聚合为词频):

UPDATE documents
SET embedding = tokenize(passage, 'my_tokenizer');

-- 查看稀疏的 bm25vector 表示形式 ({token_id:term_frequency, ...})
SELECT id, embedding FROM documents LIMIT 2;

5. 构建 bm25 索引

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

6. 执行 Top-K BM25 查询

在升序 ORDER BY 子句中结合 <&> 操作符与 to_bm25query(index_name, query_vector) 执行检索:

SELECT
    id,
    passage,
    -(embedding <&> to_bm25query('documents_embedding_bm25', tokenize('PostgreSQL BM25 index', 'my_tokenizer'))) AS bm25_score
FROM documents
ORDER BY embedding <&> to_bm25query('documents_embedding_bm25', tokenize('PostgreSQL BM25 index', 'my_tokenizer'))
LIMIT 10;
重要

为什么 <&> 返回负数? PostgreSQL 仅支持按升序(ASC)执行索引驱动的 ORDER BY 扫描。因此,<&> 操作符返回的是负的 BM25 得分(例如 -2.8415)。数值越负代表相关性越高,从而使 ORDER BY embedding <&> to_bm25query(...) ASC LIMIT K 能够直接利用 Block-WeakAnd 索引扫描。如果希望展示正数得分,只需在 SELECT 列表中对该表达式取反(-(embedding <&> ...))即可。


工作流 B:原生 tsvector API(main 分支)

在新一代 main 分支重构中,vchord_bm25 可直接为 PostgreSQL 原生的 tsvector 类型建立索引——对于标准语言无需安装额外的分词器扩展。

1. 创建扩展与数据表

CREATE EXTENSION IF NOT EXISTS vchord_bm25 CASCADE;

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    passage TEXT NOT NULL,
    vector tsvector GENERATED ALWAYS AS (to_tsvector('english', passage)) STORED
);

INSERT INTO documents (passage) VALUES
  ('PostgreSQL is a powerful, open-source object-relational database system.'),
  ('Full-text search is a technique for searching documents in a full-text database.'),
  ('BM25 is a bag-of-words retrieval function that ranks documents based on query terms.'),
  ('VectorChord-bm25 provides a native BM25 ranking index in PostgreSQL.');

2. 在 tsvector 上构建 bm25 索引

CREATE INDEX documents_bm25_idx
ON documents
USING bm25 (vector bm25_ops)
WITH (options = $$
k1 = 1.2
b = 0.75
$$);

3. 使用 to_bm25query(tsvector, regclass) 查询

SELECT
    id,
    passage,
    -(vector <&> to_bm25query(to_tsvector('english', 'PostgreSQL BM25 index'), 'documents_bm25_idx')) AS bm25_score
FROM documents
ORDER BY vector <&> to_bm25query(to_tsvector('english', 'PostgreSQL BM25 index'), 'documents_bm25_idx')
LIMIT 10;

2 - 使用指南与混合检索

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

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

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

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

2.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.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;

2.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 统计信息,且对其他分区零无效检查开销。

2.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 基准测试对比。

2.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 最小堆。

3 - 分词器 (pg_tokenizer)

pg_tokenizer 完整指南:文本分析器、字符/词元过滤器、预训练与自定义词表模型,以及多语言分词。

pg_tokenizer(由 Pigsty 在 pgsty/pg_tokenizer 维护)是 vchord_bm25 (pgsty/vchord_bm25) 的配套 PostgreSQL 分词扩展,提供全文分析器与词表模型,可将原始文本转换为整数词元 ID 数组(INT[])及稀疏 bm25vector 向量。

了解文本分析器(Text Analyzer)与词表模型(Model)如何协同工作,将原始文本转化为词元 ID。

配置字符过滤器、预分词器(Unicode、正则、结巴 Jieba)、词干提取器(支持 30 种语言)、停用词、同义词与 N-gram。

使用内置预训练模型(llmlingua2、bert_base_uncased、wiki_tocken、gemma2b)、训练自定义语料模型,或导入 HuggingFace 与 Lindera 模型。

面向英文、中文(Jieba)、日文(Lindera)及特定领域自定义语料库的开箱即用配置方案。

3.1 - 分词器架构与基本用法

理解 pg_tokenizer 的两阶段流水线:文本分析器(Text Analyzer)与分词模型(Tokenization Model)。

两阶段分词流水线

在 pg_tokenizer 中,一个分词器(tokenizer)最多由两个阶段组成,负责将原始 TEXT 字符串转换为由整数词元 ID 构成的 INT[] 数组:

flowchart LR
    A["原始文本\n(TEXT)"] --> B["第一阶段:文本分析器 (Text Analyzer)\n1. character_filters(字符过滤器)\n2. pre_tokenizer(预分词器)\n3. token_filters(词元过滤器)"]
    B --> C["归一化词元数组\n(TEXT[])"]
    C --> D["第二阶段:模型 (Model)\n词表映射 / 子词 BPE"]
    D --> E["词元 ID 数组\n(INT[] → bm25vector)"]
  1. 文本分析器(text_analyzer):按顺序应用以下三级流水线,将原始文本切分为归一化后的字符串词元数组 TEXT[]:
    • 字符过滤器(character_filters):在切分前对原始字符串进行整体转换(例如 to_lowercase 小写转换、unicode_normalization Unicode 规范化)。
    • 预分词器(pre_tokenizer):将字符串初步切分为子串(支持 unicode_segmentation、regex 正则或中文 jieba 分词)。
    • 词元过滤器(token_filters):对单个词元进行过滤或转换(支持 skip_non_alphanumeric、stemmer 词干提取、stopwords 停用词、synonym 同义词、pg_dict、以及 ngram)。
  2. 分词模型(model):将归一化后的字符串词元(或对于 llmlingua2、bert_base_uncased、gemma2b、HuggingFace 及 lindera 等独立子词模型,直接将原始文本)映射为整数词元 ID(INT[])。
说明
  • 预训练子词模型(llmlingua2、bert_base_uncased、gemma2b)、HuggingFace 模型和 Lindera 模型已内置完整的分词规则,不需要(也不允许)配置单独的 text_analyzer 阶段。
  • 词典/语料模型(wiki_tocken 以及通过 create_custom_model 创建的自定义模型)则必须搭配 text_analyzer 阶段,先完成文本切分与归一化,再去模型的词表(Vocabulary Table)中查找词元 ID。

创建与使用分词器

pg_tokenizer 中的所有配置均采用 TOML 格式,并通过美元符号引用的字符串($$ ... $$)传入。

方式一:单阶段分词器(使用内置模型)

SELECT create_tokenizer('llm_tok', $$
model = "llmlingua2"
$$);

SELECT tokenize('PostgreSQL is an object-relational database system.', 'llm_tok');

方式二:内联文本分析器 + 模型

你可以直接在 create_tokenizer 内部定义 text_analyzer 流水线:

SELECT create_tokenizer('wiki_eng', $$
model = "wiki_tocken"

[text_analyzer.character_filters.to_lowercase]
[text_analyzer.pre_tokenizer.unicode_segmentation]
[[text_analyzer.token_filters.stopwords]]
words = "nltk_english"
[[text_analyzer.token_filters.stemmer]]
kind = "english_porter2"
$$);

SELECT tokenize('PostgreSQL is an object-relational database system.', 'wiki_eng');

方式三:可复用的命名文本分析器 + 自定义模型

你可以使用 create_text_analyzer 先创建一个独立的命名 text_analyzer,随后在多个自定义模型和分词器之间复用它:

-- 1. 创建可复用的文本分析器
SELECT create_text_analyzer('english_analyzer', $$
[character_filters.to_lowercase]
[pre_tokenizer.unicode_segmentation]
[[token_filters.skip_non_alphanumeric]]
[[token_filters.stopwords]]
words = "nltk_english"
[[token_filters.stemmer]]
kind = "english_porter2"
$$);

-- 预览文本分析器如何将文本切分为 TEXT[] 词元数组:
SELECT apply_text_analyzer('PostgreSQL is a powerful database system!', 'english_analyzer');
-- 输出:{postgresql,power,databas,system}

管理分词器

-- 查看已配置的分词器列表
SELECT name FROM tokenizer_catalog.tokenizer;

-- 删除指定分词器
SELECT drop_tokenizer('wiki_eng');

3.2 - 文本分析器与过滤器

pg_tokenizer 中字符过滤器、预分词器与词元过滤器的完整配置参考手册。

文本分析器(Text Analyzer) 通过三个顺序阶段处理原始文本:

  1. 字符过滤器(character_filters) —— 对完整输入字符串执行零个或多个字符级转换。
  2. 预分词器(pre_tokenizer) —— 使用唯一的一个切分器将字符串切分为初始词元。
  3. 词元过滤器(token_filters) —— 对切分出的词元执行零个或多个转换、扩展或过滤操作。
SELECT create_text_analyzer('analyzer1', $$
character_filters = [
    "to_lowercase",
    { unicode_normalization = "nfkd" },
]
pre_tokenizer = "unicode_segmentation"
token_filters = [
    "skip_non_alphanumeric",
    { stopwords = "nltk_english" },
    { stemmer = "english_porter2" },
]
$$);
提示

在将文本分析器绑定到模型之前,你可以直接运行 SELECT apply_text_analyzer('Sample text here', 'analyzer1'); 来测试并查看其输出的 TEXT[] 词元数组。


1. 字符过滤器(character_filters)

to_lowercase

将输入文本中的所有字符转换为小写。

character_filters = ["to_lowercase"]
# 或使用表语法:
[character_filters.to_lowercase]

unicode_normalization

应用 Unicode 规范化形式(UAX #15),将组合/分解字符、重音符号以及全角/半角变体标准化。

支持的模式包括:

  • "nfc" —— 规范化形式 C(标准分解后再执行标准组合)
  • "nfd" —— 规范化形式 D(标准分解)
  • "nfkc" —— 规范化形式 KC(兼容性分解后再执行标准组合)
  • "nfkd" —— 规范化形式 KD(兼容性分解)
[[character_filters]]
unicode_normalization = "nfkd"

2. 预分词器(pre_tokenizer)

每个文本分析器最多只能指定一个 pre_tokenizer。

unicode_segmentation

按照 Unicode 标准附件 #29 (UAX #29) 规范(底层基于 Rust unicode-segmentation 库)在 Unicode 词边界处切分文本。非常适合以空格分隔的西方语言。

pre_tokenizer = "unicode_segmentation"
# 或:
[pre_tokenizer.unicode_segmentation]

regex

提取匹配自定义正则表达式的词元(底层基于 Rust regex 库):

[pre_tokenizer]
regex = '''\w+'''

jieba(中文结巴分词)

使用 jieba-rs 词典与隐马尔可夫模型(HMM)将中文文本切分为词语:

参数 类型 默认值 说明
mode string "search" "full"(调用标准 jieba.cut 分词)、"precise"(调用 jieba.cut_all 扫描句中所有可成词的词语)或 "search"(搜索引擎模式,调用 jieba.cut_for_search 对长词再次切分以提高检索召回率)。
enable_hmm bool true 是否启用 HMM 新词发现能力(仅在 "full" 和 "search" 模式下生效)。
[pre_tokenizer.jieba]
mode = "search"
enable_hmm = true

3. 词元过滤器(token_filters)

词元过滤器会严格按照 TOML 数组中声明的顺序依次执行。

skip_non_alphanumeric

丢弃不包含任何字母或数字字符的词元(例如分词后残留的独立标点符号 ,、.、! 等)。

[[token_filters]]
skip_non_alphanumeric = {}
# 或使用数组简写形式:
token_filters = ["skip_non_alphanumeric"]

stemmer(Snowball 词干提取 —— 支持 30 种语言)

使用 Snowball 词干提取算法将屈折变化的单词还原为其词干形式。

[[token_filters]]
stemmer = "english_porter2"

支持的 stemmer 取值如下:

语言 stemmer 取值 语言 stemmer 取值
阿拉伯语 (Arabic) arabic 立陶宛语 (Lithuanian) lithuanian
亚美尼亚语 (Armenian) armenian 尼泊尔语 (Nepali) nepali
巴斯克语 (Basque) basque 挪威语 (Norwegian) norwegian
加泰罗尼亚语 (Catalan) catalan 葡萄牙语 (Portuguese) portuguese
丹麦语 (Danish) danish 罗马尼亚语 (Romanian) romanian
荷兰语 (Dutch) dutch 俄语 (Russian) russian
英语 (English Porter) english_porter 塞尔维亚语 (Serbian) serbian
英语 (English Porter2) english_porter2 西班牙语 (Spanish) spanish
世界语 (Esperanto) esperanto 瑞典语 (Swedish) swedish
爱沙尼亚语 (Estonian) estonian 泰米尔语 (Tamil) tamil
芬兰语 (Finnish) finnish 土耳其语 (Turkish) turkish
法语 (French) french 意第绪语 (Yiddish) yiddish
德语 (German) german
希腊语 (Greek) greek
印地语 (Hindi) hindi
匈牙利语 (Hungarian) hungarian
印度尼西亚语 (Indonesian) indonesian
爱尔兰语 (Irish) irish
意大利语 (Italian) italian

stopwords(停用词过滤)

移除几乎不携带有效语义信号的常见停用词。

  • 内置停用词典:
    • "lucene_english" —— Apache Lucene 标准英文停用词表
    • "nltk_english" —— NLTK 标准英文停用词表
    • "iso_english" —— Stopwords ISO 英文停用词表
  • 自定义停用词典:通过 create_stopwords(name, text) 创建(每行一个词),随后按名称引用。
-- 创建自定义停用词表(每行一个词)
SELECT create_stopwords('my_stopwords', $$
it
is
a
an
the
$$);

SELECT create_text_analyzer('text_analyzer1', $$
pre_tokenizer = "unicode_segmentation"
[[token_filters]]
stopwords = "my_stopwords"
$$);

SELECT apply_text_analyzer('it is a cat', 'text_analyzer1');
-- 输出:{cat}

synonym(同义词归一)

将同义词组归一化为同一个标准词元。使用 create_synonym(name, text) 创建同义词典,其中每行包含以空格分隔的一组同义词(同一行中的所有词都会被替换为该行的第一个词):

SELECT create_synonym('my_synonym', $$
postgres pgsql postgresql
index indices
$$);

SELECT create_text_analyzer('text_analyzer2', $$
pre_tokenizer = "unicode_segmentation"
[[token_filters]]
synonym = "my_synonym"
$$);

SELECT apply_text_analyzer('pgsql and postgresql have many indices', 'text_analyzer2');
-- 输出:{postgres,and,postgres,have,many,index}

pg_dict(PostgreSQL 全文检索词典)

将词元过滤委托给任意已有的 PostgreSQL 全文检索词典(pg_ts_dict,支持 Simple、Ispell、Snowball 和 Synonym 词典;注意不支持 Thesaurus 分类词典,因为后者作用于多词短语而非单个词元):

[[token_filters]]
pg_dict = "english_stem"

ngram

将每个词元切分为字符级 N-gram(适用于子串匹配、前缀匹配、无词典 CJK 检索,或代码/标识符检索):

参数 类型 默认值 取值范围 说明
min_gram int 1 1..=255 (<= max_gram) 生成 N-gram 的最小字符长度。
max_gram int 2 1..=255 (>= min_gram) 生成 N-gram 的最大字符长度。
preserve_original bool false true / false 当原始词元长度不在 [min_gram, max_gram] 范围内时,是否同时保留未切分的原始词元。
[[token_filters.ngram]]
min_gram = 2
max_gram = 3
preserve_original = true

3.3 - 分词词表模型

预训练模型(llmlingua2、bert_base_uncased、wiki_tocken、gemma2b)、自定义语料训练模型、HuggingFace 分词器与 Lindera 模型。

在 pg_tokenizer 中,模型(Model) 负责将字符串词元(或原始文本)映射为整数词元 ID(INT[])。你可以直接选用四个内置预训练模型、在 PostgreSQL 表上训练自定义词表模型、导入任意 HuggingFace tokenizer.json,或者使用 Lindera 形态学分析模型。

1. 内置预训练模型

模型名称 来源 / 架构 是否需要 text_analyzer? 内存占用 适用场景
llmlingua2 microsoft/llmlingua-2-xlm-roberta-large-meetingbank(XLM-RoBERTa 多语言子词模型) 否(禁止) ~200 MB(默认预加载) 开箱即用的多语言通用文本分词,无需训练自定义词表。
bert_base_uncased google-bert/bert-base-uncased(WordPiece,30,522 词表) 否(禁止) ~15 MB 通用英文文本;启动极快且词表紧凑。
wiki_tocken Snowolf/wiki-tocken(内置英文维基百科词元词典) 是(必需) ~30 MB 配合自定义词干提取器、停用词与同义词使用的英文全文检索。
gemma2b google/gemma-2b(SentencePiece BPE,256k 词表) 否(禁止) ~100 MB 代码、技术标识符以及多语言大语言模型工作负载。

示例:使用 bert_base_uncased 或 llmlingua2

SELECT create_tokenizer('bert_tok', $$
model = "bert_base_uncased"
$$);

SELECT tokenize('VectorChord-BM25 brings Block-WeakAnd to PostgreSQL.', 'bert_tok');

2. 自定义语料库模型(create_custom_model)

为了获得最高的 BM25 相关性精度与最小的内存占用,你可以直接基于自己的业务表列构建自定义词表模型。不同于大语言模型子词分词器(会将罕见的领域专有名词切碎为多个子词元),自定义模型会为 text_analyzer 产出的每个唯一归一化词元分配独立的 1:1 整数 ID,且在内存中仅占用几兆字节。

方式 A:一步到位辅助函数(create_custom_model_tokenizer_and_trigger)

该便捷函数会一步完成创建自定义模型、创建分词器、为所有存量行回填目标 bm25vector 列,并安装一个 BEFORE INSERT OR UPDATE 触发器——当数据行发生变化时,自动学习新词元并更新 bm25vector 列:

-- 1. 定义文本分析流水线
SELECT create_text_analyzer('my_analyzer', $$
character_filters = ["to_lowercase"]
pre_tokenizer = "unicode_segmentation"
token_filters = [
    "skip_non_alphanumeric",
    { stopwords = "nltk_english" },
    { stemmer = "english_porter2" },
]
$$);

-- 2. 创建模型与分词器、回填向量并挂载触发器
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'
);

方式 B:分步创建(不使用触发器的 create_custom_model)

如果你不希望挂载自动触发器(或者希望自行控制何时计算并回填 embedding 列):

-- 1. 基于现有表列构建自定义词表模型
SELECT create_custom_model('my_model', $$
table = "documents"
column = "passage"
text_analyzer = "my_analyzer"
$$);

-- 2. 创建引用该分析器与自定义模型的分词器
SELECT create_tokenizer('my_tokenizer', $$
text_analyzer = "my_analyzer"
model = "my_model"
$$);

-- 3. 手动回填或刷新词频向量列
UPDATE documents SET embedding = tokenize(passage, 'my_tokenizer');

3. 导入 HuggingFace 分词器(create_huggingface_model)

你可以使用 create_huggingface_model 将任意 HuggingFace tokenizers 的 JSON 配置文件(tokenizer.json)导入 PostgreSQL。

在 psql 中,可以使用 \set 直接从 HuggingFace Hub 下载 tokenizer.json 到变量中并传给 create_huggingface_model:

\set content `wget -q -O - https://huggingface.co/google-bert/bert-base-uncased/resolve/main/tokenizer.json`
SELECT create_huggingface_model('bert_import', :'content');

SELECT create_tokenizer('bert_import_tok', $$
model = "bert_import"
$$);

也可以直接将 tokenizer.json 文件的完整内容作为美元符号引用的字符串传入:

SELECT create_huggingface_model('bge_m3_model', $$
{
  "version": "1.0",
  ...
}
$$);

SELECT create_tokenizer('bge_m3_tok', $$
model = "bge_m3_model"
$$);

4. Lindera 形态学分词模型(create_lindera_model)

对于日文、韩文、中文等需要形态学词典分析的东亚语言,pg_tokenizer 集成了 lindera。

说明

如果你从源码编译 pg_tokenizer,请在编译时通过 --features 启用所需的 Lindera 词典特性:

cargo pgrx install --release --features "lindera-ipadic lindera-ipadic-neologd lindera-unidic lindera-ko-dic lindera-cc-cedict"

以下完整示例展示了基于 ipadic 词典配置日文叠字符(Iteration Mark)规范化、NFKC Unicode 规范化、字符映射、复合词合并、数词归一化、日文停用词性(Stop-tags)过滤、片假名词干提取以及变音符号移除:

SELECT create_lindera_model('japanese_ipadic', $$
[segmenter]
mode = "normal"
  [segmenter.dictionary]
  kind = "ipadic"

[[character_filters]]
kind = "japanese_iteration_mark"
  [character_filters.args]
  normalize_kanji = true
  normalize_kana = true

[[character_filters]]
kind = "unicode_normalize"
  [character_filters.args]
  kind = "nfkc"

[[character_filters]]
kind = "mapping"
  [character_filters.args.mapping]
  "リンデラ" = "Lindera"

[[token_filters]]
kind = "japanese_compound_word"
  [token_filters.args]
  kind = "ipadic"
  tags = [
    "名詞,数",
    "名詞,接尾,助数詞",
  ]
  new_tag = "名詞,数"

[[token_filters]]
kind = "japanese_number"
  [token_filters.args]
  tags = ["名詞,数"]

[[token_filters]]
kind = "japanese_stop_tags"
  [token_filters.args]
  tags = [
    "接続詞",
    "助詞",
    "助詞,格助詞",
    "助詞,格助詞,一般",
    "助詞,格助詞,引用",
    "助詞,格助詞,連語",
    "助詞,係助詞",
    "助詞,副助詞",
    "助詞,間投助詞",
    "助詞,並立助詞",
    "助詞,終助詞",
    "助詞,副助詞/並立助詞/終助詞",
    "助詞,連体化",
    "助詞,副詞化",
    "助詞,特殊",
    "助動詞",
    "記号",
    "記号,一般",
    "記号,読点",
    "記号,句点",
    "記号,空白",
    "記号,括弧閉",
    "その他,間投",
    "フィラー",
    "非言語音",
  ]

[[token_filters]]
kind = "japanese_katakana_stem"
  [token_filters.args]
  min = 3

[[token_filters]]
kind = "remove_diacritical_mark"
  [token_filters.args]
  japanese = false
$$);

SELECT create_tokenizer('jp_tok', $$
model = "japanese_ipadic"
$$);

5. 模型预加载(add_preload_model)

大型模型(如约 200 MB 的 llmlingua2、约 100 MB 的 gemma2b、japanese_ipadic)在每个新建的 PostgreSQL 后端进程首次查询时,需要数秒钟时间和可观的内存来加载。通过在 shared_preload_libraries 启动阶段预加载模型,PostgreSQL 只需在 postmaster 主进程中加载一次模型,即可通过写时复制(Copy-on-Write)内存在所有后端连接之间共享(llmlingua2 默认已加入预加载列表):

-- 将模型加入预加载列表(持久化保存在 $PGDATA/pg_tokenizer/preload_models)
SELECT add_preload_model('my_model');

-- 查看当前已配置的预加载模型列表(默认为 {llmlingua2})
SELECT list_preload_models();

-- 从预加载列表中移除模型(例如不使用 llmlingua2 时可释放约 200 MB 内存)
SELECT remove_preload_model('llmlingua2');

调用 add_preload_model(...) 后(请确保已在 postgresql.conf 中设置 shared_preload_libraries = 'pg_tokenizer.so'),重启 PostgreSQL 即可使预加载对所有后端连接生效。

3.4 - 多语言分词配方与限制

面向英文、中文(结巴 Jieba)、日文(Lindera)的完整生产级分词配置方案及已知使用限制。

本页提供可直接用于生产环境的英文、中文和日文全文 BM25 检索分词配置配方,并列出在实际运维中需要注意的关键限制事项。

配方一:带词干提取与停用词过滤的英文检索(wiki_tocken)

将 Unicode 词边界切分、NLTK 停用词过滤、Porter2 词干提取与内置维基百科词表(wiki_tocken)结合使用:

SELECT create_tokenizer('eng_wiki', $$
model = "wiki_tocken"

[text_analyzer.character_filters.to_lowercase]
[text_analyzer.pre_tokenizer.unicode_segmentation]
[[text_analyzer.token_filters.skip_non_alphanumeric]]
[[text_analyzer.token_filters.stopwords]]
words = "nltk_english"
[[text_analyzer.token_filters.stemmer]]
kind = "english_porter2"
$$);

SELECT tokenize('PostgreSQL databases are running analytical and vector workloads.', 'eng_wiki');

配方二:带自动触发器的英文自定义语料库模型

源自上游 pg_tokenizer 示例文档,该工作流直接基于 documents 表构建特定领域的英文词表,并通过触发器自动同步更新词频向量:

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    passage TEXT,
    embedding bm25vector
);

SELECT create_text_analyzer('text_analyzer1', $$
pre_tokenizer = "unicode_segmentation"
[[character_filters]]
to_lowercase = {}
[[character_filters]]
unicode_normalization = "nfkd"
[[token_filters]]
skip_non_alphanumeric = {}
[[token_filters]]
stopwords = "nltk_english"
[[token_filters]]
stemmer = "english_porter2"
$$);

SELECT create_custom_model_tokenizer_and_trigger(
    tokenizer_name => 'tokenizer1',
    model_name => 'model1',
    text_analyzer_name => 'text_analyzer1',
    table_name => 'documents',
    source_column => 'passage',
    target_column => 'embedding'
);

INSERT INTO documents (passage) VALUES
('PostgreSQL is a powerful, open-source object-relational database system. It has over 15 years of active development and a proven architecture that has earned it a strong reputation for reliability, feature robustness, and performance.'),
('Full-text search is a technique for searching a computer-stored document or a collection in a full-text database. Full-text search is distinguished from searches based on metadata or on parts of the original texts represented in databases (such as titles, abstracts, selected sections, or bibliographical references).'),
('BM25 is a ranking function used by search engines to estimate the relevance of documents to a given search query. It is based on the probabilistic retrieval framework developed in the 1970s and 1980s by Stephen E. Robertson, Karen Spärck Jones, and others.'),
('PostgreSQL supports advanced data types and performance optimization features, only available in the expensive commercial databases, such as Oracle and SQL Server. It is known for its resilience, integrity, and correctness.'),
('The BM25 algorithm computes a score for each document in a collection based on the query terms appearing in the document, regardless of their proximity within the document. It is a bag-of-words retrieval function that ranks a set of documents based on the query terms appearing in each document.'),
('PostgreSQL has a wide range of extensions and tools that make it suitable for various use cases, from small single-machine applications to large internet-facing applications with many concurrent users.'),
('Full-text search in PostgreSQL allows users to perform complex queries on text data, including phrase searching, ranking of results, and support for multiple languages through configurations and dictionaries.'),
('VectorChord-BM25 is a postgresql extension designed for bm25 ranking algorithm. Block-WeakAnd algorithm is implemented in it to accelerate the ranking process.'),
('pg_tokenizer is a postgresql extension designed for full-text search. It provides a set of functions to preprocess text data and generate tokens for indexing and searching.'),
('Look for organizations which use PostgreSQL for mission-critical or scaled applications. Look for organizations that have used PostgreSQL for a significant length of time.');

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

SELECT id, passage, embedding <&> to_bm25query('documents_passage_bm25', tokenize('PostgreSQL BM25', 'tokenizer1')) AS rank
FROM documents
ORDER BY rank
LIMIT 10;

配方三:中文全文检索(jieba + 自定义模型)

处理中文文本时,推荐将 jieba 预分词器(mode = "search",enable_hmm = true)与基于业务表训练的自定义语料词表模型结合使用(以下示例采用钱钟书《围城》选段作为测试语料):

CREATE TABLE zh_docs (
    id SERIAL PRIMARY KEY,
    passage TEXT,
    embedding bm25vector
);

-- 1. 创建启用 NFKD 规范化与结巴搜索引擎模式的中文文本分析器
SELECT create_text_analyzer('text_analyzer1', $$
[character_filters.to_lowercase]
[character_filters.unicode_normalization]
kind = "nfkd"
[pre_tokenizer.jieba]
mode = "search"
enable_hmm = true
[[token_filters.skip_non_alphanumeric]]
$$);

-- 2. 创建自定义词表模型、分词器,并绑定自动更新触发器
SELECT create_custom_model_tokenizer_and_trigger(
    tokenizer_name     => 'tokenizer1',
    model_name         => 'model1',
    text_analyzer_name => 'text_analyzer1',
    table_name         => 'zh_docs',
    source_column      => 'passage',
    target_column      => 'embedding'
);

-- 3. 写入中文语料(触发器会自动分词并填充 embedding 列)
INSERT INTO zh_docs (passage) VALUES
('红海早过了,船在印度洋面上开驶着,但是太阳依然不饶人地迟落早起,侵占去大部分的夜。'),
('夜仿佛纸浸了油,变成半透明体;它给太阳拥抱住了,分不出身来,也许是给太阳陶醉了,所以夕照霞隐褪后的夜色也带着酡红。'),
('到红消醉醒,船舱里的睡人也一身腻汗地醒来,洗了澡赶到甲板上吹海风,又是一天开始。'),
('这是七月下旬,合中国旧历的三伏,一年最热的时候。在中国热得更比常年利害,事后大家都说是兵戈之象,因为这就是民国二十六年【一九三七年】。'),
('这条法国邮船白康蒂号(VicomtedeBragelonne)正开向中国来。早晨八点多钟,冲洗过的三等舱甲板湿意未干,但已坐立满了人,法国人、德国流亡出来的犹太人、印度人、安南人,不用说还有中国人。'),
('海风里早含着燥热,胖人身体给风吹干了,蒙上一层汗结的盐霜,仿佛刚在巴勒斯坦的死海里洗过澡。'),
('毕竟是清晨,人的兴致还不没给太阳晒萎,烘懒,说话做事都很起劲。那几个新派到安南或中国租界当警察的法国人,正围了那年轻善撒娇的犹太女人在调情。'),
('俾斯麦曾说过,法国公使大使的特点,就是一句外国话不会讲;这几位警察并不懂德文,居然传情达意,引得犹太女人格格地笑,比他们的外交官强多了。'),
('这女人的漂亮丈夫,在旁顾而乐之,因为几天来,香烟、啤酒、柠檬水全靠这几个警察孝敬。船上买东西用法国钱,这犹太人只有一点儿沿着那位丹麦王子汉姆烈德(Hamlet)路线逃出来的德国马克。'),
('当时德国跟日本虽在谈恋爱,尚未结婚,到中国来的犹太人还是德国公民,有他们本国的保护。这船上的犹太人有过路到香港的,有到上海居留的,聚在一起在讨论生计,声音活像小洋磁盆里托的石子。'),
('几个年轻的安南人比手画脚,像猴子跳电线一样迅捷地交换着法语,华侨在旁羡慕地听着。这船上都是回家乡的中国人,像报纸上的标题,电影的字幕,标语,籍贯姓名年纪一张表上那几格,他们老早填满了,此番回来,不过把这些公式平滑无波地重演一次。'),
('只有鼻梁上架着一副黑眼镜的苏小姐,心算着她离国这几年,家里有什么变化,她父亲是否老了,她母亲新添了什么病痛,她弟弟结婚了没有。'),
('苏小姐在里昂研究法国文学,做了一篇《中国十八世纪的法国文学里的中国》的论文,刚得了博士。'),
('方鸿渐也到甲板上来了,在苏小姐身旁走过,向她脱帽招呼。苏小姐一壁应着,一壁想他真算厚脸皮,昨天向自己借了照相机,今天见了面,提都不提。');

-- 4. 创建 BM25 索引并执行检索
CREATE INDEX zh_docs_passage_bm25 ON zh_docs USING bm25 (embedding bm25_ops);

SELECT id, passage,
       embedding <&> to_bm25query('zh_docs_passage_bm25', tokenize('苏小姐 法国', 'tokenizer1')) AS rank
FROM zh_docs
ORDER BY rank
LIMIT 10;

配方四:日文全文检索(lindera IPADIC)

处理日文文本时,可创建基于 Lindera IPADIC 的形态学分词模型,并配置叠字符规范化、NFKC 规范化、日文停用词性(Stop-tags)过滤以及片假名词干提取(以下示例采用夏目漱石《吾輩は猫である》选段作为测试语料):

CREATE TABLE jp_docs (
    id SERIAL PRIMARY KEY,
    passage TEXT,
    embedding bm25vector
);

INSERT INTO jp_docs (passage) VALUES
('吾輩は猫である。名前はまだ無い。'),
('どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。'),
('吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。'),
('この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。'),
('ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。'),
('掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。この時妙なものだと思った感じが今でも残っている。'),
('第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。'),
('のみならず顔の真中があまりに突起している。そうしてその穴の中から時々ぷうぷうと煙を吹く。どうも咽せぽくて実に弱った。'),
('これが人間の飲む煙草というものである事はようやくこの頃知った。'),
('この書生の掌の裏でしばらくはよい心持に坐っておったが、しばらくすると非常な速力で運転し始めた。'),
('書生が動くのか自分だけが動くのか分らないが無暗に眼が廻る。胸が悪くなる。到底助からないと思っていると、どさりと音がして眼から火が出た。'),
('それまでは記憶しているがあとは何の事やらいくら考え出そうとしても分らない。'),
('ふと気が付いて見ると書生はいない。たくさんおった兄弟が一疋も見えぬ。肝心の母親さえ姿を隠してしまった。'),
('その上今までの所とは違って無暗に明るい。眼を明いていられぬくらいだ。果てな何でも容子がおかしいと、のそのそ這い出して見ると非常に痛い。'),
('吾輩は藁の上から急に笹原の中へ棄てられたのである。');

SELECT create_lindera_model('lindera_ipadic', $$
[segmenter]
mode = "normal"
  [segmenter.dictionary]
  kind = "ipadic"

[[character_filters]]
kind = "japanese_iteration_mark"
  [character_filters.args]
  normalize_kanji = true
  normalize_kana = true

[[character_filters]]
kind = "unicode_normalize"
  [character_filters.args]
  kind = "nfkc"

[[character_filters]]
kind = "mapping"
  [character_filters.args.mapping]
  "リンデラ" = "Lindera"

[[token_filters]]
kind = "japanese_compound_word"
  [token_filters.args]
  kind = "ipadic"
  tags = [
    "名詞,数",
    "名詞,接尾,助数詞",
  ]
  new_tag = "名詞,数"

[[token_filters]]
kind = "japanese_number"
  [token_filters.args]
  tags = ["名詞,数"]

[[token_filters]]
kind = "japanese_stop_tags"
  [token_filters.args]
  tags = [
    "接続詞", "助詞", "助詞,格助詞", "助詞,格助詞,一般", "助詞,格助詞,引用",
    "助詞,格助詞,連語", "助詞,係助詞", "助詞,副助詞", "助詞,間投助詞",
    "助詞,並立助詞", "助詞,終助詞", "助詞,副助詞/並立助詞/終助詞",
    "助詞,連体化", "助詞,副詞化", "助詞,特殊", "助動詞", "記号",
    "記号,一般", "記号,読点", "記号,句点", "記号,空白", "記号,括弧閉",
    "その他,間投", "フィラー", "非言語音"
  ]

[[token_filters]]
kind = "japanese_katakana_stem"
  [token_filters.args]
  min = 3

[[token_filters]]
kind = "remove_diacritical_mark"
  [token_filters.args]
  japanese = false
$$);

SELECT create_tokenizer('tokenizer1', $$
model = "lindera_ipadic"
$$);

UPDATE jp_docs SET embedding = tokenize(passage, 'tokenizer1');

CREATE INDEX jp_docs_passage_bm25 ON jp_docs USING bm25 (embedding bm25_ops);

SELECT id, passage,
       embedding <&> to_bm25query('jp_docs_passage_bm25', tokenize('人間 吾輩', 'tokenizer1')) AS rank
FROM jp_docs
ORDER BY rank
LIMIT 10;

运维注意事项与已知限制

  1. 对象名称命名规范:所有分词器、文本分析器、模型、停用词典和同义词典的名称只能包含字母、数字和下划线([A-Za-z0-9_]),且不能以数字开头。
  2. 事务回滚与会话级内存对象缓存:每个 PostgreSQL 后端连接都会在进程内存(DashMap)中缓存 tokenizer、text_analyzer、model、stopwords 和 synonym 对象。由于该内存对象池未挂载到 PostgreSQL 事务回滚回调机制上,当事务执行 ROLLBACK 时,磁盘上的 tokenizer_catalog 元数据表会回滚,但当前连接的内存缓存中仍会残留刚才创建的对象。在 ROLLBACK 之后,请显式调用对应的 drop_* 函数(或重新建立数据库连接)以清理内存缓存:
    BEGIN;
    SELECT create_text_analyzer('text_analyzer1', $$
    pre_tokenizer = "unicode_segmentation"
    $$);
    ROLLBACK;
    
    -- 清理当前会话内存中残留的缓存项(或者重新建立数据库连接)
    SELECT drop_text_analyzer('text_analyzer1');
    
    -- 现在可以正常重新创建同名对象
    SELECT create_text_analyzer('text_analyzer1', $$
    pre_tokenizer = "unicode_segmentation"
    $$);
  3. 只读从库上的模型预加载:add_preload_model 会将预加载模型列表写入主节点本地的 $PGDATA/pg_tokenizer 目录。由于 PostgreSQL 关系表以外的文件系统文件不会通过 WAL 日志复制到从库,如果你在只读从库上也需要预加载模型,请在从库上独立执行一次 add_preload_model。
  4. 内置大语言模型分词器的内存开销:llmlingua2(~200 MB)、gemma2b(~100 MB)和 lindera 等模型会占用较多内存。在生产环境中,请务必使用 add_preload_model 让模型在 postmaster 主进程中仅加载一次,避免在每个连接中重复占用内存;或者优先选用仅占用几兆字节内存的自定义语料模型(create_custom_model)。

4 - 参考手册与基准测试

vchord_bm25 与 pg_tokenizer 的完整 SQL 参考手册、GUC 参数、性能基准测试与版本演进历史。

vchord_bm25 与 pg_tokenizer 的完整技术参考资料:

v0.1–v0.3 (bm25_catalog.) 与 main 分支 (bm25.) 的会话 GUC 参数及索引存储选项。

4.1 - SQL 参考手册

vchord_bm25 与 pg_tokenizer 的完整 SQL 数据类型、操作符、类型转换、索引访问方法与函数参考。

vchord_bm25 数据类型

数据类型 所属模式 适用版本 文本表示形式 / 内部结构 说明
bm25vector bm25_catalog v0.1.0 – v0.3.0 {token_id:freq, ...}(例如 {1:1, 2:2, 15:1}) 将 32 位无符号词元 ID 映射为 32 位词频的稀疏向量。存储策略为 STORAGE = EXTERNAL。
bm25query bm25_catalog v0.1.0 – v0.3.0 (index_oid regclass, query_vector bm25vector) 将目标 bm25 索引 OID 与查询 bm25vector 配对的复合类型。
bm25query public / 扩展模式 main (vector tsvector, index regclass) 将查询 tsvector 与目标 bm25 索引 OID 配对的复合类型。

bm25vector 字面量语法(v0.1 – v0.3)

-- 空 bm25vector
SELECT '{}'::bm25vector;

-- 显式指定 token_id:frequency 键值对
SELECT '{1:1, 2:3, 10:2}'::bm25vector;

-- 从 int[] 隐式转换(自动将重复的词元 ID 聚合为词频计数)
SELECT ARRAY[1, 2, 2, 2, 10, 10]::bm25vector;
-- 输出:{1:1, 2:3, 10:2}

vchord_bm25 操作符

操作符 左操作数 右操作数 返回类型 适用版本 说明
<&> bm25vector bm25query real (f32) v0.1.0 – v0.3.0 计算负值 BM25 相关性得分($-\text{BM25}(D, Q)$)。支持使用 bm25_ops 执行索引加速的 ORDER BY ... ASC 扫描。
<&> tsvector bm25query float8 (f64) main 在原生 tsvector 上计算负值 BM25 相关性得分。支持使用 bm25_ops 执行索引加速的 ORDER BY ... ASC 扫描。
= bm25vector bm25vector bool v0.2.0 – v0.3.0 比较两个 bm25vector 值是否完全相等。
<> bm25vector bm25vector bool v0.2.0 – v0.3.0 比较两个 bm25vector 值是否不相等。

vchord_bm25 函数

函数签名 返回类型 适用版本 说明
to_bm25query(index_oid regclass, query_vector bm25vector) bm25query v0.1.1 – v0.3.0 根据 bm25 索引名称/OID 与查询 bm25vector(或经由隐式转换的 INT[])构造 bm25query。
to_bm25query(index_oid regclass, query_vector int[]) bm25query v0.3.0+ 直接接受 INT[] 词元 ID 数组的显式重载函数。
to_bm25query(vector tsvector, index regclass) bm25query main 根据原生 tsvector 与目标 bm25 索引名称/OID 构造 bm25query。
search_bm25query(target_vector bm25vector, query bm25query) real v0.1.0 – v0.3.0 bm25vector <&> bm25query 操作符的底层实现函数。
_bm25_evaluate(left tsvector, right bm25query) float8 main tsvector <&> bm25query 操作符的底层实现函数。
bm25_page_inspect(index regclass, blkno int) text v0.3.0 返回 bm25 索引第 blkno 号数据块的可读诊断元数据。

vchord_bm25 访问方法与操作符类

对象类型 名称 目标类型 排序操作符
访问方法 (Access Method) bm25 索引 (Index) 处理器 _bm25_amhandler
操作符族 (Operator Family) bm25_ops USING bm25 OPERATOR 1 pg_catalog.<&> FOR ORDER BY float_ops
操作符类 (Operator Class) bm25_ops bm25vector(v0.1–v0.3)/ tsvector(main) USING bm25 的默认操作符类

pg_tokenizer 函数(tokenizer_catalog)

所有 pg_tokenizer 函数均位于 tokenizer_catalog 模式中:

分词器管理与执行

函数签名 返回类型 说明
create_tokenizer(name TEXT, config TEXT) void 根据 TOML 配置字符串创建一个命名分词器。
drop_tokenizer(name TEXT) void 删除已有的分词器。
tokenize(text TEXT, tokenizer_name TEXT) INT[] 使用 tokenizer_name 对 text 进行分词,返回整数词元 ID 数组(可隐式转换为 bm25vector)。

文本分析器管理

函数签名 返回类型 说明
create_text_analyzer(name TEXT, config TEXT) void 创建可复用的命名文本分析器流水线(包含 character_filters、pre_tokenizer、token_filters)。
drop_text_analyzer(name TEXT) void 删除指定的命名文本分析器。
apply_text_analyzer(text TEXT, text_analyzer_name TEXT) TEXT[] 对 text 运行指定的文本分析器,返回归一化后的字符串词元数组(常用于调试分词效果)。

模型管理

函数签名 返回类型 说明
create_custom_model(name TEXT, config TEXT) void 使用指定的文本分析器,在现有表列上训练自定义词表模型。
drop_custom_model(name TEXT) void 删除自定义模型及其底层词表数据表。
create_custom_model_tokenizer_and_trigger(tokenizer_name TEXT, model_name TEXT, text_analyzer_name TEXT, table_name TEXT, source_column TEXT, target_column TEXT) void 一步创建自定义模型与分词器,回填 table_name 表中的 target_column 列,并绑定自动 BEFORE INSERT OR UPDATE 触发器。
create_huggingface_model(name TEXT, config TEXT) void 从 HuggingFace tokenizer.json 字符串导入模型。
drop_huggingface_model(name TEXT) void 删除已导入的 HuggingFace 模型。
create_lindera_model(name TEXT, config TEXT) void 根据 TOML 配置创建 Lindera 形态学分析模型(如 ipadic)。
drop_lindera_model(name TEXT) void 删除 Lindera 模型。

词典与预加载管理

函数签名 返回类型 说明
create_stopwords(name TEXT, config TEXT) void 创建自定义停用词典(每行一个停用词)。
drop_stopwords(name TEXT) void 删除自定义停用词典。
create_synonym(name TEXT, config TEXT) void 创建自定义同义词典(每行以空格分隔一组同义词,统一映射到该行第一个词)。
drop_synonym(name TEXT) void 删除自定义同义词典。
add_preload_model(name TEXT) void 注册在 PostgreSQL 服务启动(shared_preload_libraries)时预加载的模型。
remove_preload_model(name TEXT) void 从启动预加载列表中移除指定模型。
list_preload_models() TEXT[] 返回当前已注册为启动预加载的所有模型名称列表。

内部触发器与辅助函数(tokenizer_catalog)

函数签名 返回类型 说明
apply_text_analyzer_for_custom_model(text TEXT, text_analyzer_name TEXT) TEXT[] 调用 text_analyzer_name 分词,并在写入自定义模型词表前自动过滤掉长度超过 2600 字节(PostgreSQL B-tree UNIQUE 索引键最大长度限制)的超长词元。
custom_model_insert_trigger() trigger AFTER INSERT OR UPDATE 触发器函数,从源文本列提取词元并将新出现的词元自动追加写入 tokenizer_catalog."model_<name>" 词表。
custom_model_tokenizer_set_target_column_trigger() trigger 由 create_custom_model_tokenizer_and_trigger 安装的 BEFORE INSERT OR UPDATE 触发器函数,自动执行 NEW.<target_column> = tokenize(NEW.<source_column>, tokenizer_name)。

pg_tokenizer 元数据表(tokenizer_catalog)

表名 列定义 说明
tokenizer_catalog.tokenizer name TEXT NOT NULL UNIQUE, config TEXT NOT NULL 存储已注册的分词器名称及其序列化后的 JSON/TOML 配置。
tokenizer_catalog.text_analyzer name TEXT NOT NULL UNIQUE, config TEXT NOT NULL 存储独立命名且可复用的文本分析器定义。
tokenizer_catalog.model name TEXT NOT NULL UNIQUE, config TEXT NOT NULL 存储已注册的自定义模型、HuggingFace 模型与 Lindera 模型定义。
tokenizer_catalog.stopwords name TEXT NOT NULL PRIMARY KEY, config TEXT NOT NULL 存储内置(lucene_english、nltk_english、iso_english)及自定义停用词典。
tokenizer_catalog.synonym name TEXT NOT NULL PRIMARY KEY, config TEXT NOT NULL 存储自定义同义词典。
tokenizer_catalog."model_<name>" id SERIAL PRIMARY KEY, token TEXT NOT NULL UNIQUE 为每个自定义模型 <name> 自动创建的底层词表数据表,将唯一归一化词元映射为从 1 开始的自增整数 ID。

pg_tokenizer TOML 配置参数参考

分词器配置项(create_tokenizer)

参数名 类型 说明
text_analyzer string(可选) 预先创建的命名 text_analyzer 名称。若省略,可在支持文本分析器的模型中直接内联定义 character_filters、pre_tokenizer 和 token_filters。
model string(必填) 内置模型(llmlingua2、bert_base_uncased、wiki_tocken、gemma2b)或用户创建的模型(custom、huggingface、lindera)名称。

文本分析器配置项(create_text_analyzer)

参数名 类型 说明
character_filters array / table 在分词前对整段字符串依次应用的字符过滤器列表:to_lowercase、unicode_normalization(nfc、nfd、nfkc、nfkd)。
pre_tokenizer string / table 将字符串切分为初始词元的单个预分词器:unicode_segmentation、regex(Rust 正则表达式)或 jieba(mode = "full" | "precise" | "search",enable_hmm = true | false)。
token_filters array 按顺序执行的词元过滤器列表:skip_non_alphanumeric、stemmer(支持 30 种 Snowball 词干提取器)、stopwords(内置 lucene_english、nltk_english、iso_english 或自定义停用词典)、synonym、pg_dict(除 Thesaurus 外的任意 pg_ts_dict 词典)、ngram(min_gram、max_gram 范围 1..=255,preserve_original)。

自定义模型配置项(create_custom_model)

参数名 类型 说明
table string(必填) 包含源文本语料的目标表名称。
column string(必填) table 表中用于构建词表的源 TEXT 列名称。
text_analyzer string(必填) 用于将 column 列切分为词表词元的 text_analyzer 名称。

4.2 - GUC 与索引参数

vchord_bm25 会话级 GUC 参数与索引存储选项的完整参考手册。

稳定发布版(v0.1.0 – v0.3.0):bm25_catalog.* GUC 参数

在 v0.1.0 至 v0.3.0 版本中,所有会话级 GUC 参数均使用 bm25_catalog. 前缀,任何普通用户均可在会话或事务级别修改(GucContext::Userset):

参数名称 类型 默认值 有效范围 说明
bm25_catalog.bm25_limit integer 100 -1 .. 65535 单次 bm25 索引扫描返回的高分文档最大数量。设为 -1 时禁用 Block-WeakAnd 剪枝并执行全量暴力扫描;设为 0 时直接返回空结果。必须 ≥\ge SQL 查询中的 LIMIT 值。
bm25_catalog.enable_index boolean true true / false 在 PostgreSQL 查询规划器中启用或禁用 bm25 索引扫描。设为 false 可强制执行顺序扫描并调用 search_bm25query 精确求值。
bm25_catalog.enable_prefilter boolean true true / false 自 v0.2.2 引入。设为 true 时,会在将候选文档推入 Block-WeakAnd Top-K 最小堆之前,先行过滤掉不满足查询过滤条件或 MVCC 可见性检查的文档。
bm25_catalog.segment_growing_max_page_size integer 4096 1 .. 1000000 仅追加的增长段(Growing Segment)在被封存为差分位打包压缩的只读封闭段(Sealed Segment)之前的最大尺寸(以 8 KB 页面为单位,默认值 4096 即 32 MB)。

配置示例(v0.1 – v0.3)

-- 为深度分页(例如 LIMIT 500)调大 Top-K 候选窗口
SET bm25_catalog.bm25_limit = 500;

-- 强制对索引中的所有倒排项执行全量暴力扫描
SET bm25_catalog.bm25_limit = -1;

-- 为包含高选择性 WHERE 子句的查询启用预过滤
SET bm25_catalog.enable_prefilter = true;

-- 禁用索引扫描,用于对比顺序扫描基线性能
SET bm25_catalog.enable_index = false;

新一代 main 分支:bm25.* GUC 参数与索引存储参数

在 main 分支重构版本中,会话级 GUC 参数改用 bm25. 前缀,并与每个索引独立的存储参数(WITH (...))配合工作:

会话级 GUC 参数(main)

参数名称 类型 默认值 有效范围 说明
bm25.enable_scan boolean true true / false 在查询规划器中启用或禁用 bm25 索引扫描。
bm25.limit integer 100 0 .. 65535 单次 bm25 索引扫描返回的最大结果数。若设为 0,则回退使用索引自身的 limit 存储参数。
bm25.prefilter boolean false true / false 在 bm25 索引扫描期间启用预过滤(当设为 true 时,会覆盖索引自身的 prefilter 存储参数)。

索引存储参数(main 中的 WITH (...))

存储参数 类型 默认值 有效范围 说明
options string (TOML) k1 = 1.2, b = 0.75 k1: 1.2..2.0
b: 0.0..1.0
用于配置该索引 Okapi BM25 超参数 k1k_1 与 bb 的 TOML 字符串。
limit integer 0 0 .. 65535 当会话参数 bm25.limit = 0 时使用的索引级回退检索上限。
prefilter boolean false true / false 索引级的默认预过滤设置。

4.3 - 性能与精度基准测试

VectorChord-BM25 与 Elasticsearch、Lucene、ParadeDB 及 PostgreSQL GIN 的 QPS 吞吐量与 NDCG@10 检索精度对比基准测试。

吞吐量基准测试(QPS 对比 Elasticsearch、Lucene、ParadeDB、PG GIN)

凭借结合 128 文档差分位打包块与单字节 Fieldnorm 量化的 Block-WeakAnd 算法实现,VectorChord-BM25 在 Top-10 排序查询上的 QPS 吞吐量最高可达基于 Tantivy 的 PostgreSQL 扩展(ParadeDB / pg_search)的 3 倍,并比 PostgreSQL 原生 GIN 索引高出一个数量级:

VectorChord-BM25 RPS 基准测试VectorChord-BM25 NDCG@10 基准测试

为什么 vchord_bm25 能实现极高的 QPS

  1. 零跨引擎序列化开销:不同于外部数据包装器(FDW)或外挂 Tantivy/Lucene 桥接方案,vchord_bm25 将段数据直接存储在 PostgreSQL 缓冲池页面(PostgresRelation)中,彻底消除了跨引擎元组编组(Marshaling)开销。
  2. 激进的块级上界剪枝(Block-Max Pruning):在多词查询场景下,一旦 Top-10 阈值趋于稳定,Block-WeakAnd 即可直接跳过 80%–95% 以上的 128 文档倒排块,无需对其进行解压。
  3. 256 项预计算 BM25 查找表:将文档长度量化为单字节 fieldnorm,使原本昂贵的逐文档浮点运算简化为单次数组查表。

检索精度基准测试(BEIR 与 LoTTE 上的 NDCG@10)

下表(摘自我们的 BEIR / LoTTE 混合检索与 ColBERT 重排序评测报告)对比了以下三种方案的检索精度(NDCG@10):

  • PostgreSQL vchord 稠密向量检索(Vector)
  • PostgreSQL vchord_bm25 词法检索(BM25)
  • 混合检索(vchord + vchord_bm25 配合 ColBERT 重排序)
数据集 领域 稠密向量 (vchord) 词法 BM25 (vchord_bm25) 混合检索 + ColBERT 重排序
scifact 科学事实核查 0.676 0.665 0.751
nfcorpus 生物医学信息检索 0.340 0.325 0.370
fiqa 金融问答 0.405 0.236 0.415
trec-covid 生物医学 COVID-19 0.728 0.656 0.838
LoTTE-lifestyle (search) 生活方式问答(搜索) 0.735 0.635 0.779
LoTTE-lifestyle (forum) 生活方式论坛 0.662 0.506 0.701
LoTTE-recreation (search) 休闲娱乐问答 0.642 0.564 0.708
LoTTE-technology (search) 技术问答 0.562 0.483 0.647
LoTTE-writing (search) 写作问答 0.726 0.674 0.793
提示

在所有评测数据集上,将 vchord_bm25 与 vchord 结合使用均稳定超越单独使用稠密向量检索或单独使用词法 BM25 检索。如需获取开箱即用的 SQL 查询模板,请参阅混合检索 (BM25 + VectorChord)。

4.4 - 版本历史与 API 演进

VectorChord-BM25 (vchord_bm25) 从 v0.1.0 到 v0.3.0 再到 main 分支的版本变更日志与 SQL 模式演进。

本页记录 vchord_bm25(pgsty/vchord_bm25 / supervc-stack/VectorChord-bm25)的版本发布历史与 SQL 模式演进。

版本演进总览矩阵

版本 所属模式 索引列类型 分词方案 核心新增特性
main(新一代架构) 扩展所在模式 tsvector 原生 to_tsvector(或自定义) 基于原生 tsvector 完全重写存储与索引引擎;支持外部排序并行构建(ambuild);引入索引级存储参数(options、limit、prefilter);引入 bm25.* GUC 参数;支持 PostgreSQL 18。
v0.3.0 bm25_catalog bm25vector pg_tokenizer 新增 bm25_page_inspect(regclass, int) 函数、to_bm25query(regclass, int[]) 重载,以及 PostgreSQL 18 兼容性支持。
v0.2.2 bm25_catalog bm25vector pg_tokenizer 新增 bm25_catalog.enable_prefilter GUC 参数,支持带 WHERE 子句的精确过滤 Top-K BM25 检索。
v0.2.1 bm25_catalog bm25vector pg_tokenizer 修复增长段(Growing)向封闭段(Sealed)转换以及 VACUUM 清理过程中的若干缺陷。
v0.2.0 bm25_catalog bm25vector pg_tokenizer 为 bm25vector 新增 = 与 <> 相等性操作符,以及隐式类型转换 CREATE CAST (int[] AS bm25vector) AS IMPLICIT。
v0.1.1 bm25_catalog bm25vector pg_tokenizer 将分词能力解耦为独立的 pg_tokenizer 扩展;调整 to_bm25query(regclass, bm25vector) 函数签名。
v0.1.0 bm25_catalog bm25vector 内置 unicode 首个发布版本,提供 bm25vector、bm25query、USING bm25 及 Block-WeakAnd Top-K 扫描。

各版本升级变更详解

v0.3.0(sql/vchord_bm25--0.2.2--0.3.0.sql)

  • 新增页面诊断函数:
    CREATE FUNCTION bm25_page_inspect(index regclass, blkno INT) RETURNS TEXT;
  • 在 main / 0.3.x 后期版本中新增了直接接受 int[] 的便捷重载 to_bm25query(regclass, int[])。

v0.2.2(sql/vchord_bm25--0.2.1--0.2.2.sql)

  • 在 Block-WeakAnd 中新增预过滤支持(bm25_catalog.enable_prefilter GUC 参数,在 0.3.0 中默认启用)。
  • 改进索引扫描期间的 MVCC 可见性检查,防止死元组(Dead Tuples)或被过滤元组污染 Top-K 剪枝阈值。

v0.2.0(sql/vchord_bm25--0.1.1--0.2.0.sql)

  • 为 bm25vector 新增 = 与 <> 操作符。
  • 新增从 int[] 到 bm25vector 的隐式类型转换(_vchord_bm25_cast_array_to_bm25vector),允许将 tokenize(text, name) 的输出(INT[])直接赋值给 bm25vector 列或传入任何接受 bm25vector 的位置。

v0.1.1(sql/vchord_bm25--0.1.0--0.1.1.sql)

  • 将原本内置的 create_tokenizer / tokenize 函数抽离为独立的 pg_tokenizer 扩展(tokenizer_catalog),从而支持丰富的文本分析器、Snowball 词干提取、停用词、同义词、HuggingFace 模型、jieba 中文分词与 lindera 日文分词。
  • 简化 to_bm25query(index_oid regclass, query_vector bm25vector) 签名,直接接受 bm25vector 参数。

新一代架构重写(main 分支)

  • 用 PostgreSQL 原生 tsvector 替代自定义 bm25vector 类型(CREATE OPERATOR CLASS bm25_ops FOR TYPE tsvector USING bm25),允许用户直接使用 USING bm25 为 to_tsvector(...) 列建立索引。
  • 新增基于外部归并排序(External Merge Sort)的并行索引构建(bm25_parallel_build_main),保证恒定的维护内存消耗。
  • 支持通过 WITH (options = $$ k1 = 1.2, b = 0.75 $$) 配置索引级 BM25 超参数(k1∈[1.2,2.0]k_1 \in [1.2, 2.0],b∈[0.0,1.0]b \in [0.0, 1.0])。