LEANN 基线基准测试指南:BM25 与 DiskANN 检索延迟对比
【免费下载链接】LEANN[MLsys2026 Best Paper]: https://arxiv.org/abs/2506.08276. RAG on Everything with LEANN. Enjoy 97% storage savings while running a fast, accurate, and 100% private RAG application on your personal device.项目地址: https://gitcode.com/GitHub_Trending/le/LEANN
LEANN(benchmarks/bm25_diskann_baselines/)提供了一套独立的检索基线基准:一个是基于 Pyserini/Lucene 的 BM25 稀疏检索,另一个是基于 leann-backend-diskann 的 DiskANN 图索引检索。本文将以 benchmarks/bm25_diskann_baselines/README.md 为主线,完整讲解两个基准的索引数据获取、运行命令、参数含义与实测结果,并结合 run_bm25.py、run_diskann.py 与 diskann_backend.py 的源码,深入拆解每个开关背后对延迟与吞吐的影响。读完本文,你将掌握如何在本地复现这套"稀疏基线 vs 图索引基线"的延迟对比,并理解 DiskANN 搜索参数(complexity、beam_width、cache_mechanism 等)的底层语义。
基准定位:为什么要同时测 BM25 与 DiskANN
RAG 系统的检索环节存在两类主流方案:以 BM25 为代表的词法稀疏检索,与以 DiskANN 为代表的向量近似最近邻(ANN)图索引检索。二者在索引结构、查询路径、硬件依赖上差异极大,单独报告任何一方的延迟都缺乏参照系。LEANN 在benchmarks/bm25_diskann_baselines/下同时提供两个独立脚本,用于在同一台机器、同一份查询集上分别测量两种基线的纯检索延迟,作为评估 LEANN 自身检索性能的外部参照。
两个脚本的设计原则(见 README.md 的 Notes 部分):
- DiskANN 只统计search-only 延迟:查询的 embedding 事先计算好并从计时中剔除;
- BM25 直接对文本查询做词法检索,天然不涉及 embedding;
- 两个基准使用同一份真实查询集(Natural Questions,NQ);
- 结果明确标注为machine-specific(机器相关),仅代表在"当前仓库 + 当前机器"上测得的本地数据。
数据准备:拉取索引与查询集
README 给出的第一步是从 AWS S3 同步两个预构建索引到本地:
aws s3 sync s3://powerrag-diskann-rpj-wiki-20250824-224037-194d640c/bm25_rpj_wiki/index_en_only/ benchmarks/data/indices/bm25_index/ aws s3 sync s3://powerrag-diskann-rpj-wiki-20250824-224037-194d640c/diskann_rpj_wiki/ benchmarks/data/indices/diskann_rpj_wiki/两条命令分别把:
bm25_rpj_wiki/index_en_only/同步到benchmarks/data/indices/bm25_index/(Pyserini 的 Lucene BM25 索引目录);diskann_rpj_wiki/同步到benchmarks/data/indices/diskann_rpj_wiki/(DiskANN 磁盘图索引目录)。
从 bucket 命名中的rpj_wiki可以推断,两个索引均构建自同一份 Wiki 语料(推测为 RedPajama 系列 Wiki 语料,英文子集),且索引前缀默认为ann(对应脚本参数--index-prefix的默认值,磁盘上形如ann_disk.index及相关 PQ 文件)。
查询集固定为benchmarks/data/queries/nq_open.jsonl(Natural Questions 的 open-domain 抽取版),两个脚本默认都指向它。
注意:S3 bucket 中的索引文件不在仓库内,运行基准前需自行执行同步并确保
awsCLI 已安装配置。
DiskANN 基线:run_diskann.py
运行命令与结果
uv run --script benchmarks/bm25_diskann_baselines/run_diskann.pyREADME 记录的本机实测结果:
| 指标 | 数值 |
|---|---|
| 平均延迟 | 0.011093 s/query |
| QPS | 90.15 |
| p50 | 0.010731 s |
| p95 | 0.015000 s |
对应设置(与脚本默认值一致):
recompute_embeddings=False:关闭重算重排,只走 PQ 近似搜索路径;- embeddings 预计算(
use_server=False,不计时); - batching off:逐条查询,每次只送 1 个向量(
embs[i : i + 1]); - 缓存关闭:
cache_mechanism=2、num_nodes_to_cache=0。
脚本机制逐段拆解
run_diskann.py使用 PEP 723 内联元数据声明依赖leann-backend-diskann(run_diskann.py),由uv run --script自动解析执行。
1. 参数面(默认值即 README 记录的基准设置)
| 参数 | 默认值 | 说明 |
|---|---|---|
--index-dir | benchmarks/data/indices/diskann_rpj_wiki | DiskANN 索引文件目录 |
--index-prefix | ann | 索引文件前缀(C++ 层会拼出ann_disk.index等文件) |
--queries-file | benchmarks/data/queries/nq_open.jsonl | NQ 查询集 |
--num-queries | 200 | 参与计时的查询条数 |
--top-k | 10 | 返回的近邻数 |
--complexity | 62 | 搜索候选列表规模(L=62),越大越准越慢 |
--threads | 1 | 搜索线程数 |
--beam-width | 1 | 每轮并发的 I/O 请求数 |
--cache-mechanism | 2 | 缓存机制(见下文源码语义) |
--num-nodes-to-cache | 0 | 缓存节点数,0 即不缓存 |
2. embedding 预计算(run_diskann.py):
embs = _compute( queries, model_name="facebook/contriever-msmarco", mode="sentence-transformers", use_server=False, ).astype(np.float32)关键点:模型固定为facebook/contriever-msmarco(与索引构建时的向量空间必须一致),use_server=False表示不走 ZMQ embedding server,直接本地计算。这正是 api.py 中compute_embeddings的分支语义:use_server=True走端口转发给 server(适合查询场景),use_server=False走embedding_compute的直连计算(适合 build 场景)。预计算完成后向量在计时循环之外,从而保证测到的是纯索引搜索延迟。
3. 构造 Searcher(run_diskann.py):
searcher = _DiskannSearcher( index_prefix_path, num_threads=int(args.threads), cache_mechanism=int(args.cache_mechanism), num_nodes_to_cache=int(args.num_nodes_to_cache), )底层对应 DiskannSearcher。注意index_prefix_path传的是基础路径(不含_disk.index后缀),C++ 层会自动拼出_disk.index;若目录下检测到*_disk_graph.index与*_partition.bin两个分区文件,还会自动启用分区前缀走图分区索引。
4. 计时循环(run_diskann.py):
先跑 1 次 warmup(不计时),然后对每条查询执行:
searcher.search( embs[i : i + 1], # 单条查询,batching off top_k=args.top_k, complexity=args.complexity, beam_width=args.beam_width, prune_ratio=0.0, # 不做近似剪枝 recompute_embeddings=False, batch_recompute=False, dedup_node_dis=False, )这些开关直接对应 diskann_backend.py 的search签名:
complexity:候选列表大小,决定 PQ 距离遍历的广度,是"准确率—延迟"的核心旋钮;beam_width:每轮并行 I/O 请求数,影响磁盘读取的并发度;prune_ratio:用近似距离剪枝邻居的比例(0.0–1.0),基准关闭(0.0);recompute_embeddings=False时走use_deferred_fetch=False的纯 PQ 路径,C++ 层遍历始终用 PQ 距离,不触发重排(见 diskann_backend.py 的策略注释);cache_mechanism的语义在源码中明确注释:1= 用 sample data 初始化缓存,2= 就绪缓存但不初始化,其他值 = 禁用缓存(diskann_backend.py)。基准取2配合num_nodes_to_cache=0,等价于彻底关闭缓存,测的是冷缓存的原始索引访问能力。
结果按 p50/p95 输出(p50 即排序后len(times)//2处的值),QPS 用1.0 / avg计算。
BM25 基线:run_bm25.py
运行命令与结果
uv run --script benchmarks/bm25_diskann_baselines/run_bm25.pyREADME 记录的本机实测结果:
| 指标 | 数值 |
|---|---|
| 平均延迟 | 0.028589 s/query |
| QPS | 34.97 |
| p50 | 0.026060 s |
| p90 | 0.043695 s |
| p95 | 0.053260 s |
| p99 | 0.055257 s |
对应设置:k=10、k1=0.9、b=0.4、queries=100。
环境准备:JDK 21 与 Pyserini
run_bm25.py依赖pyserini(Lucene 之上的 Python 检索库),而 Pyserini 需要 JDK。脚本头部注释给出了 Arch Linux 下的完整配置流程(run_bm25.py):
sudo pacman -S jdk21-openjdk export JAVA_HOME=/usr/lib/jvm/java-21-openjdk sudo archlinux-java status sudo archlinux-java set java-21-openjdk # fish shell 下持久化: set -Ux JAVA_HOME /usr/lib/jvm/java-21-openjdk fish_add_path --global $JAVA_HOME/bin set -Ux LD_LIBRARY_PATH $JAVA_HOME/lib/server $LD_LIBRARY_PATH which javac # 应输出 /usr/lib/jvm/java-21-openjdk/bin/javac其他发行版只需保证JAVA_HOME指向可用的 JDK(21 及以上)即可,脚本会在缺失时提示pip install pyserini。
参数面
| 参数 | 默认值 | 说明 |
|---|---|---|
--bm25-index | benchmarks/data/indices/bm25_index | Pyserini Lucene 索引目录 |
--queries | benchmarks/data/queries/nq_open.jsonl | 查询文件 |
--k | 10 | Top-k 检索数 |
--k1 | 0.9 | BM25 词频饱和参数 |
--b | 0.4 | BM25 文档长度归一化参数 |
--limit | 100 | 最多执行的查询条数(README 结果即 100) |
--warmup | 5 | 预热查询数(不计时) |
--fetch-docs | off | 额外抓取命中文档内容(更慢,默认关) |
--report | 无 | 可选,输出 JSON 报告路径 |
脚本机制要点
- 查询加载(run_bm25.py):优先解析 JSONL,依次尝试
query/text/question字段;解析失败的行按纯文本处理;非 JSONL 文件按"每行一条查询"读取。 - BM25 参数注入(run_bm25.py):
LuceneSearcher打开索引后调用searcher.set_bm25(k1=args.k1, b=args.b),部分 pyserini 构建版本不要求显式设置,因此用 try/except 兜底。 - 计时:先跑
warmup条预热,再逐条searcher.search(q, k=args.k)计时;若开--fetch-docs,还会对每个命中调用searcher.doc(h.docid)把文档读取 I/O 计入。 - 报告:
--report可把 queries、k、k1、b、avg、p50/p90/p95/p99、total_time、qps、索引绝对路径等结构化写入 JSON(run_bm25.py),便于后续对比分析。
结果对比与解读
把两个基线的 README 结果放到一起:
| 基线 | avg | QPS | p50 | p95 |
|---|---|---|---|---|
| DiskANN(NQ,search-only) | 0.011093 s | 90.15 | 0.010731 s | 0.015000 s |
| BM25(k=10, k1=0.9, b=0.4) | 0.028589 s | 34.97 | 0.026060 s | 0.053260 s |
在 README 记录的这台机器上,DiskANN 的纯搜索延迟约为 BM25 的 1/2.6,吞吐约为 2.6 倍。解读时需注意三点:
- 口径不同:DiskANN 侧 embedding 已预计算并排除在计时外,BM25 侧则是纯词法打分,两者都是"纯检索",但 DiskANN 的实际端到端延迟还需加上 embedding 计算成本;
- 关闭了缓存与重排:DiskANN 的
cache_mechanism=2、num_nodes_to_cache=0意味着没有利用节点缓存加速,若开启缓存(cache_mechanism=1或更大的num_nodes_to_cache)延迟通常还会进一步下降; - machine-specific:README 明确声明结果是在当前仓库、当前机器上测得的本地数据,不代表通用结论;换机器、换索引、换查询集后数字会变化,复现时应以自身环境为准。
源码纵深:LEANN 内置 BM25 与外部基线的区别
值得说明的是,run_bm25.py使用的 Pyserini/Lucene 是外部独立基线,与 LEANN 自身代码无关。而 LEANN 核心也内置了一个基于 SQLite FTS5 的 BM25 索引Fts5BM25Index(见 api.py):
BM25Index抽象基类定义契约:fit(documents)建索引、查询时命中bm25()打分;Fts5BM25Index用CREATE VIRTUAL TABLE bm25_passages USING fts5(...)持久化词项/倒排数据,并在建表时按_cjk_ngrams开关决定是否对中日韩文本做 unigram/bigram 切分(api.py);- 由于 SQLite FTS5 的
bm25()返回值是"越小越好",LEANN 内部取负号统一成"越大越好"的分数语义,方便与向量分数做混合融合(见 api.py 的注释)。
也就是说:bm25_diskann_baselines目录下的 BM25 基准用于横向对标外部检索实现;如果要在 LEANN 内部做混合检索(sparse + dense),则应使用Fts5BM25Index这条内置路径。二者得分口径、索引后端均不同,做对比实验时不要混用。
复现注意事项
- 索引不在仓库内:必须先用
aws s3 sync拉取benchmarks/data/indices/下的两个索引目录,脚本才会通过目录存在性检查(run_bm25.py检查os.path.isdir,run_diskann.py检查index_dir.is_dir(),否则直接退出); - 查询集固定:默认都指向
benchmarks/data/queries/nq_open.jsonl,若换用自定义查询文件,需保证向量空间与索引构建时一致(DiskANN 侧为facebook/contriever-msmarco); - 环境差异:Pyserini 需要 JDK 21 且
JAVA_HOME正确;DiskANN 侧leann-backend-diskann(当前版本 0.3.8,见 pyproject.toml)需随uv run --script自动安装或预装; - 结果可复现性:两个脚本都提供 warmup 机制排除冷启动,且
run_bm25.py支持--report输出 JSON 报告,建议复现时固定同一批参数并多次运行取稳定值。
通过这套双基线基准,你可以快速在同一环境下回答一个关键问题:给定你的语料与查询分布,"词法稀疏检索"与"图索引向量检索"各自的纯检索延迟处在什么水平,从而为 LEANN 的检索链路选型(或混合检索比例)提供可量化的参照。
【免费下载链接】LEANN[MLsys2026 Best Paper]: https://arxiv.org/abs/2506.08276. RAG on Everything with LEANN. Enjoy 97% storage savings while running a fast, accurate, and 100% private RAG application on your personal device.项目地址: https://gitcode.com/GitHub_Trending/le/LEANN
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考