mcpField
R
rapidbm25

rapidbm25

本地服务
github.com其他0今天更新

关于 rapidbm25

rapidbm25 是基于 Rust 的 BM25 搜索库,面向构建本地文本或代码与文档搜索的开发者。它提供兼容 rank_bm25 的评分,并支持增量索引、批量搜索和 MCP 服务。

主要功能

  1. 01BM25Okapi、BM25L 和 BM25Plus 评分
  2. 02增量更新索引
  3. 03批量查询与 top-n 搜索
  4. 04可选分词器与混合融合
  5. 05保存和加载索引

适用场景

  1. 01搜索本地代码或文档目录
  2. 02随文档变化更新文本索引
  3. 03融合 BM25 与向量搜索排名
  4. 04在 MCP 工作流中使用 BM25 检索

MCP 相关

包含可直接运行的 MCP 服务,向 MCP 客户端提供该项目的搜索能力。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"maverick0351a-rapidbm25": {
"command": "uvx",
"args": [
"rapidbm25"
],
"env": {
"RAPIDBM25_ROOTS": "<RAPIDBM25_ROOTS>"
}
}
}
}
启动命令uvx rapidbm25
需要配置的环境变量
RAPIDBM25_ROOTSDirectories bm25_index_path may read (os.pathsep-separated). Default: the working directory.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypirapidbm250.1.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

rapidbm25

wheels

可直接替代 rank_bm25 的 BM25Okapi、BM25L 和 BM25Plus 的 Rust 原生实现。 构造函数和属性相同,得分逐位完全一致,get_scores 快约 1,470 倍, 还提供快速的 top_n、保存/加载、增量索引、批量搜索、 可选分词器、混合融合、可直接运行的 MCP 服务和 Agent 技能。

安装

pip install rapidbm25

预构建 wheel:适用于 Linux(manylinux_2_28 和 musllinux_1_2,x86_64 和 aarch64)上的 CPython 3.9–3.15、 macOS(x86_64 10.13+、arm64 11.0+)和 Windows(x86_64)。其他平台需从 sdist 构建, 并需要 Rust 工具链(1.85+)。

使用

# from rank_bm25 import BM25Okapi
from rapidbm25 import BM25Okapi

docs = ["Hello there good man!", "It is quite windy in London", "How is the weather today?"]
bm25 = BM25Okapi([d.split(" ") for d in docs])        # also: tokenizer=..., k1, b, epsilon

query = "windy London".split(" ")
scores = bm25.get_scores(query)                        # numpy float64, identical to rank_bm25
top = bm25.get_top_n(query, docs, n=1)                 # ['It is quite windy in London']
ids, best = bm25.top_n(query, n=2)                     # new: Rust partial select, no full sort
使用哪个 rank_bm25?

pip install rank_bm25 安装的是 0.2.2,其 BM25L 和 BM25Plus 与项目后续的 git main(47aa3dd)有所不同:0.2.2 的 BM25L 会乘以词频(不含该词的文档得分为 0),其 BM25Plus 的 idf 为 ln((N+1)/df),而不是 ln(N+1) - ln(df)。BM25Okapi 则完全相同。 rapidbm25 默认匹配 0.2.2;对 BM25L / BM25Plus 传入 compat="47aa3dd",即可使用 git-main 的行为。两种行为均经过逐位精确测试。

from rapidbm25 import BM25L
BM25L([d.split(" ") for d in docs], compat="47aa3dd")

与 rank_bm25 相同之处

  • 类:BM25Okapi(corpus, tokenizer=None, k1=1.5, b=0.75, epsilon=0.25)、 BM25L(..., delta=0.5)、BM25Plus(..., delta=1)。
  • 属性:corpus_size、avgdl、doc_len、doc_freqs、idf、average_idf(Okapi)、k1、b、epsilon/delta。
  • get_scores、get_batch_scores、get_top_n 的参数和断言均相同。
  • 得分逐位匹配,包括重复查询词、未知词、空文档、Okapi 的负 idf 下限,以及 NaN 情况(k1=0、b=1 且存在空文档)。

不同之处

rank_bm25rapidbm25
get_top_n 平分时的顺序numpy 不稳定 argsort 的反向顺序(因 numpy 构建版本而异)得分降序,然后按较小的文档 id 排序(确定性)
词元任意可哈希对象str(语料中的非 str 词元会引发 TypeError)
构建后编辑 .idf / .doc_freqs会影响后续得分无影响(得分来自原生索引)
tokenizer=在多进程池中运行在进程内运行
BM25L / BM25Plus 的 compat= 关键字不适用"0.2.2"(默认值)或 "47aa3dd"
Pickle / save / load仅支持 pickle支持 pickle,另有带版本的文件(load 会复制;mmap=True 为可选项)
fixes=不适用compat 类默认值为 False(逐位精确)。LiveIndex 默认值为 True

保存、加载和 pickle

okapi_ix = BM25Okapi([d.split(" ") for d in docs])
okapi_ix.save("/tmp/rapidbm25-okapi.rbm25")
okapi_loaded = BM25Okapi.load("/tmp/rapidbm25-okapi.rbm25")   # copies into memory

save 会写入格式 1:魔数 RBM25、JSON 头部,然后是小端序数值列。 浮点数以原始 IEEE-754 位存储,因此 load 后的 get_scores 与保存时的索引 逐位一致。默认的 load 会将各列复制到内存中,因此之后截断或原地编辑文件都不会改变得分 或导致进程崩溃。 load(mmap=True) 会映射调用方的文件,如果该文件发生变化则不安全: 截断可能引发 SIGBUS,而原地写入可能改变得分。Pickle 会保留可 pickle 的 tokenizer;文件 格式存储的是索引中已有的词元,而非任意可调用对象。

实时索引

LiveIndex 是一个新类(兼容类仍保持不可变且逐位精确)。它会分配稳定的字符串 ID,并在每次 add、delete 和 update 时更新 N、文档频率、avgdl 和 IDF,同时修补倒排列表,而不是重新构建。分数向量的顺序与插入顺序一致;update 会保留文档的位置;delete 会填补空位。使用相同的 k1、b、epsilon 或 delta、compat 和 fixes 时,get_scores 的结果与基于 live.corpus() 构建的 BM25Okapi / BM25L / BM25Plus 一致。空的实时索引也是有效的。 LiveIndex 的 fixes 默认为 True,兼容类则默认为 False。传入 fixes=False 可同时保留 rank_bm25 的公式和原有 bug。bm25_index_path 使用的文件夹索引是一个 LiveIndex,因此采用此默认值。

from rapidbm25 import LiveIndex, Tokenizer, fuse
words = Tokenizer(lowercase=True, stopwords="english", stemmer="english")
live_ix = LiveIndex(tokenizer=words)
live_ix.add("dogs", "The running dogs chased the ball")
live_ix.add("cats", "A sleeping cat", metadata={"kind": "pet"})
live_ix.add("weather", "It is quite windy in London")
pet_hits = live_ix.search("sleeping cats", k=1, where={"kind": "pet"})
live_ix.save("/tmp/rapidbm25-live.rbm25")
reloaded = LiveIndex.load("/tmp/rapidbm25-live.rbm25")
batch_scores = reloaded.get_scores_many(["dogs", "london wind"])
fused = fuse([["dogs", "cats"], [("cats", 0.9), ("weather", 0.2)]], method="rrf")

where 是一个字典($eq、$ne、$in、$nin、$gt、$gte、$lt、$lte、$exists)或可调用对象。$in 和 $nin 接受列表,而不是字符串。布尔值不会匹配 0 或 1。可调用对象在索引锁之外运行。add / delete / update 会获取锁,因此多个线程可以共享一个 LiveIndex。

variant 为 "okapi"(默认值)、"l" 或 "plus"。compat 为 "0.2.2" 或 "47aa3dd",与静态类相同。L 的 delta 默认为 0.5,Plus 的 delta 默认为 1。

分词器

Tokenizer 是可选功能。支持 Unicode 词语(UAX #29)、可选的小写转换、原始英语停用词列表(或自定义列表)、Snowball 词干提取器(stemmer="english" 使用 Porter2;也接受其他 Snowball 语言),以及对汉字、平假名、片假名和韩文字母使用重叠字符二元组的 cjk=True。向兼容类传入 tokenizer= 是启用分词的唯一方式;省略它则不会改变这些类的行为。

批量搜索

get_scores_many(queries) 返回形状为 (n_queries, corpus_size) 的 float64 数组。 top_n_many(queries, n) 为每个查询返回一个 (ids, scores) 对。两者都会释放 GIL,并在线程池上为查询评分。第 i 行与单查询调用的结果逐位相同。 LiveIndex 也有这两个方法。

fixes=True

对于 BM25Okapi、BM25L 和 BM25Plus,fixes=False 是默认值,结果与 rank_bm25 一致,包括其 bug。LiveIndex 默认使用 fixes=True。无论哪种设置,Okapi IDF 公式都保持为 ln(N - df + 0.5) - ln(df + 0.5)。不会用平滑后的公式替代。

issuefixes=Falsefixes=True
#19 重复的查询词每次重复都会再次添加该词每个词只计一次,按首次出现顺序
#36 空语料库兼容类会引发 ZeroDivisionError空静态索引,get_scores 返回形状为 (0,) 的结果
#39 IDF 恰好为 0当某个词出现在恰好一半的文档中时,Okapi IDF 为 0当下限为正时,该 IDF 使用现有的 epsilon * average_idf 下限

#39 不会改变任何其他 IDF。当 epsilon * average_idf 为零或负数时,此修复不会生效。常见的 2 文档情况就是如此:每个仅出现在一篇文档中的词都出现在一半语料库中,平均 IDF 不为正,因此该词的得分仍为 0。BM25L 和 BM25Plus 不存在此零值。此修复仅适用于 Okapi。对于实际的文档频率,L 和 Plus 的 IDF 不会恰好为 0。

混合融合

fuse(rankings, method="rrf", k=60) 是倒数排名融合:weight / (k + rank),排名从 1 开始。k=60 是默认值(通常使用的 Cormack 常数)。输入分数会被忽略,因此 BM25 列表和向量存储列表不需要使用相同的分数范围。 method="weighted" 会将每个列表的分数最小-最大归一化到 [0, 1],并返回加权和。分数都相等且为正数的列表会贡献其权重;全为零的列表则不产生贡献。

集成

可选扩展。两者都是基于 LiveIndex 的轻量级检索器,提供 from_texts、k 和 save / load。如果未安装依赖项,测试会跳过。

pip install "rapidbm25[langchain]"      # langchain-core
pip install "rapidbm25[llama-index]"    # llama-index-core
from rapidbm25.integrations.langchain import BM25Retriever
retriever = BM25Retriever.from_texts(["sunny Paris", "windy London"], k=1)
assert retriever.invoke("windy")[0].page_content == "windy London"

BM25Retriever.from_documents 接受 LangChain Document 对象。 rapidbm25.integrations.llama_index.BM25Retriever 实现 _retrieve 并返回 NodeWithScore 节点。

基准测试

工作负载:16,000 个 Python 标准库源码和文档分块(平均 193 个词元,词汇量 58,073),200 个 留出查询(平均 8.7 个词元),BM25Okapi。机器:Intel Xeon(Sapphire Rapids)KVM 虚拟机, 8 个 vCPU,Linux x86_64,CPython 3.13.5,numpy 2.5.4。共享虚拟机,因此细微差距可视为噪声。

共 5 轮;每轮中,每种实现都在全新进程中运行一次,并按轮换顺序执行。 以下为中位数;rapidbm25 各行的耗时是在每个进程中运行 20 个批次得出。

实现索引构建get_scores × 200端到端 × 200(分词 + 评分 + top-10)
rank_bm25 0.2.20.433 s4.225 s4.322 s
rapidbm25,通过 np.lexsort 获取 top-100.119 s(3.6×)2.87 ms(1,470×)53.6 ms(81×)
rapidbm25,top_n(q, 10)0.124 s2.90 ms16.4 ms(263×)

使用 np.lexsort 时,端到端耗时主要花在对每个查询的 16,000 个分数进行排序上;top_n 在 Rust 中执行部分选择,速度快 3.3×。

0.2 保存、增量更新、批量搜索

python bench/bench_v02.py,单进程,预热后取 5 次运行的中位数。机器:Intel Xeon, 4 个 vCPU,Linux x86_64,CPython 3.12.3,发布构建。除非另有说明,均使用合成语料: 8,000 个文档,每个 48 个词元,词汇量 4,000。保存的文件大小为 7,924,566 字节。

操作n中位数
静态 BM25Okapi 构建8,00010.7 ms
save8,00015.9 ms
load(mmap=True) + 一次查询8,0002.01 ms
load(mmap=False) + 一次查询8,0002.81 ms
对每个文档执行 LiveIndex.add8,00085.3 ms
LiveIndex.delete8006.82 ms
静态重建剩余文档7,2008.53 ms
对已构建的索引再添加 200 个文档,使用 LiveIndex.add2001.57 ms
静态重建整个文档集8,2009.58 ms

一次调用 BM25Okapi(...) 构建 8,000 个文档,比逐个执行 8,000 次 add 更快。已有动态索引后,添加 200 个文档耗时 1.57 ms,而重建全部 8,200 个文档需 9.58 ms;删除 800 个文档耗时 6.82 ms,而重建其余文档需 8.53 ms。

批量搜索的对比也采用端到端耗时(循环调用 get_scores 与调用 get_scores_many), 取 5 次运行的中位数:

工作负载顺序执行get_scores_many加速比
Okapi,8,000×48,词汇量 4,000,64 个查询(短倒排列表)0.331 ms1.021 ms0.32×
Okapi,30,000×40,词汇量 200,64 个查询(长倒排列表)4.94 ms3.64 ms1.36×
BM25Plus,30,000×32,词汇量 4,000,48 个查询(为每个文档评分)7.59 ms3.66 ms2.08×

在这台 4-vCPU 虚拟机上,稀疏 Okapi 内核开销很低,因此当每个查询只需遍历较短的倒排列表时, 线程池反而会拖慢速度。查询需要遍历较长的倒排列表时,线程池就能提升性能;对于 BM25Plus,由于它会将一个词项的贡献加到每个文档上,速度约快 2×。 无论采用哪种方式,结果都与单查询分数逐位一致。

复现 0.1 版本的对比:

pip install rapidbm25 rank-bm25==0.2.2
python bench/gen_workload.py --out /tmp/bm25-wl          # chunks your Python stdlib (numbers will differ by machine and corpus)
python bench/bench_interleaved.py --workloads /tmp/bm25-wl --out /tmp/bench.json

正确性证据

  • 契约回放:contract/bm25.contract.json 包含通过运行 PyPI 上的 rank_bm25 0.2.2 得到的 407 组输入/输出向量;contract/bm25-47aa3dd.contract.json 包含来自 git 47aa3dd 的 407 组向量。tests/replay_contract.py 会逐项精确比较(浮点数以十六进制表示):两组数据均为 407/407。将分数偏移 1 ulp 的对照构建(tests/control_ulp_off.py)有 263 项未通过,因此回放测试并非形同虚设。
  • 模糊测试(tests/fuzz_equivalence.py):针对每个参考实现运行 1,000 个随机用例,覆盖全部三个变体和边界参数;对分数、批量分数、idf、doc_freqs、avgdl、average_idf 和 top-n 逐位比较:0 次失败。
  • CI 中的每个 wheel都会安装到各自的平台上,并运行测试套件、针对 rank_bm25 0.2.2 的 300 个用例模糊测试,以及两组契约回放。
  • Wheel 使用 cargo-auditable 构建(依赖列表嵌入 .dep-v0 段中)。

MCP 服务

rapidbm25-mcp 是随 wheel 一同提供的 stdio MCP 服务(不依赖 MCP SDK): bm25_index_texts、bm25_index_path、bm25_search、bm25_list_indexes、bm25_drop_index。 文本索引存储在内存中。bm25_index_path 会将文件夹索引缓存在 RAPIDBM25_CACHE 下(默认值为 ~/.cache/rapidbm25,目录权限为 0700,文件权限为 0600)。 只有当文件大小、修改时间以及已编入索引的字节的 SHA-256 均未变化时,才会复用缓存文件;读取后会再次对路径执行 stat。不会跟随符号链接。具有其他硬链接(st_nlink > 1)的文件会被跳过。除非 glob 的文件名以 . 开头,否则会跳过点文件。读取范围仅限于 RAPIDBM25_ROOTS(默认值为工作目录)。任何内容都不会离开本机。

{ "mcpServers": { "rapidbm25": { "command": "uvx", "args": ["--from", "rapidbm25", "rapidbm25-mcp"],
  "env": { "RAPIDBM25_ROOTS": "/path/to/repo" } } } }

详情:mcp/README.md。注册表元数据:server.json。

Agent 技能

skill/rapidbm25-search/(SKILL.md + scripts/bm25_search.py),供运行 shell 命令的 Agent 使用:

python skill/rapidbm25-search/scripts/bm25_search.py --path ./src --query "parse_header timeout" -k 10

从源码构建

pip install maturin && maturin build --release      # or scripts/build_auditable.sh

许可证

Apache-2.0。请参阅 LICENSE 和 NOTICE(其中注明了与 rank_bm25 的 API 兼容性)。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。