一个 e-commerce 团队把召回率从 0.62 调到 0.90,靠的是换了个 HNSW 参数。上线后重排模型的 NDCG 没动,客服工单也没少。问题不在调参,在于他们算召回率用的"标准答案"——那份 top-1000 真值,是用他们自己那个近似索引跑出来的。被测系统和测量基准是同一个东西,调参只是在让这个索引更像它自己。
Qdrant 在 2026 年 9 月 1 日放出了一套 100 亿向量的公开数据集和配套的开源暴力检索工具,把"真值该从哪来"这件事的账摊开了。它自己在那篇发布里写得很直白:
These benchmarks show a 90% recall @ 10 on a purely synthetic RAG benchmark for a production claim. But for an e-commerce or marketplace company feeding top 1000+ results into a second-stage reranker, it's not good enough.
这句话的真正价值不是"90% 不够好",而是指出评测深度和语料规模一旦跟生产不匹配,数字好看但没有解释力。而比"k 取小了"更基础的错误,是那份真值本身就不成立。
数据集本身:10,074,324,060 篇文档,真值是暴力算出来的
Qdrant-FineWeb-10B 的构成很明确:10,074,324,060 篇 FineWeb 文档,每篇一条 768 维单位化稠密向量(gte-multilingual-base)加一条稀疏向量(词表 250,048),许可 odc-by。稠密和稀疏各 10.07B 条,对应 24.47 TB 向量、28.66 TB 原文与元数据。
关键在真值怎么来的。数据集卡上写得很清楚:
Exact top-1000 ground truth was computed with Supernova's nova-bf over the full 10-billion-vector corpus
配套的查询集是 119,953 条,覆盖稠密、稀疏和带过滤三类检索,整个计算量被描述为 over one quadrillion distance calculations。也就是说,这 12 万条查询的 top-1000 不是猜的、不是近似索引给的,是全量暴力扫出来的。
Supernova 用 Apache-2.0 开源(qdrant-labs/supernova,2026-03-27 建仓,最新 release v0.0.12),nova bf 就是干这件事的命令行工具。它的文档对自身定位写得很克制:
nova bf computes exact k-nearest-neighbor results for a query set. These results provide ground truth for evaluating approximate vector search systems using metrics such as recall@k.
三个让真值静默失效的开关
nova bf 的文档里藏着三句话,每句都对应一种"跑了但结果不算数"的用法。
第一句:
--max-files N is useful for performance testing, but its output is not valid full-corpus ground truth.
--max-files 是个性能测试开关,用来在小样本上估算吞吐。但它的输出经常被当成"抽一部分语料就够代表全量排名"的证据——官方明确否定了这一点。这背后是向量检索的一个基本性质:少一个候选,真实 top-k 里就可能少一个,而漏掉的那个不会被任何告警捕获。你只会看到 recall 数字漂亮。
第二句:
allow_tf32: enables faster matmuls but sacrifices exact float32 scoring.
真值的前提是"精确打分"。TF32 把尾数位砍掉换吞吐,在 GPU 上默认开启是常见做法。一旦开了,"精确"这个词就不再成立,而算出来的 top-k 会在分数接近的候选之间发生顺序交换。这类误差不会让 recall 崩掉,只会让它偏移几个千分点——刚好小到没人会去查。
第三句:
Exact-score ties are resolved deterministically using either corpus order (ordinal, default) or corpus.id_column.
并列分数怎么切是确定性的,但两种切法的结果不同。默认按语料顺序 ordinal,换成 corpus.id_column 会得到另一份真值。这不影响单次评测的内部一致性,但会让两次评测不可比——如果你换了索引、也顺带改了切法,召回率的变化里就混进了一个跟检索质量无关的项。真值文件应当连同生成参数一起版本化。
过滤检索里,recall@1000 的分母可能根本不到 1000
RAG 系统普遍带过滤条件:按租户、按时间、按文档类型。这类场景下 recall@k 的定义会变得微妙——如果整个语料里只有 300 篇满足过滤条件,那 recall@1000 的分母就是 300,而任何返回 300 条结果的检索器都是满分。
Qdrant 这批数据把这个现实问题量化了。4,953 条文本过滤查询里:
557 have fewer than 1,000 matching documents
另有 47 条因为没有匹配文档被直接丢弃。相比之下,结构化过滤查询每组 1,000 条且都返回满 1,000 条结果——正是这种"配对整齐"的样本让人误以为过滤检索的评测很简单。
实际生产中,"分母不足"会以两种方式误导人。一是过滤条件写得越细,召回率越容易好看,于是团队会不自觉地往细里调。二是当分母从 1000 掉到 300 时,绝对值不可比,但监控上的曲线是连续的一段,没人会注意到台阶在哪里。可行的做法是在评测输出里同时报出每条查询的候选集大小,分母低于 k 的查询要么单独归类,要么把 recall@k 换成 recall@min(k, |candidates|) 并把口径写进指标定义。
稠密、稀疏和混合检索的评测口径不是同一套
这套数据集每篇文档给了两条向量:一条 768 维单位化稠密向量(gte-multilingual-base),一条词表 250,048 的稀疏向量。查询集也按稠密、稀疏、带过滤三类分别准备。这个结构本身就是个提醒——很多团队的 RAG 评测只对稠密那一路算了召回率,而线上跑的是稠密加稀疏的混合检索。
混合检索的评测口径有几种常见做法,各有各的失真方式。把两路的召回率各算一遍再平均,会掩盖两路在不同查询类型上的互补性;只对融合后的结果算一次召回率,又分不清是融合策略的问题还是某一路拖了后腿。可用的折中是:三个指标都留下来——稠密单独、稀疏单独、融合后各一份,并且把每条查询的最优单路得分也记下来。当融合后不如单路时,看这条查询属于哪一类,比看总体平均分有用得多。
要命的是这件事在真实系统里没有标准答案:融合权重通常是调出来的,而调权重和调 HNSW 参数一样,都容易在"真值来自自身"的前提下变成自欺。真值固定下来之后,融合策略的 A/B 才有意义。
自建真值的成本账,文档也给了
如果不想用现成的,nova bf 这条路能走,代价是明确的。官方文档承认 I/O 和解码是主要瓶颈,文本过滤谓词在 CPU 上按 queries × rows 建掩码,查询集一大就很贵。two_pass 只是个安全跳过机制,不改变量级。
所以在"下载现成真值"和"自建真值"之间做选择时,合理的分界是语料规模而不是团队偏好:十万级向量的语料,本地暴力跑一遍是几分钟的事,直接自建、把真值文件签进仓库最省心;千万级以上再考虑用工具跑,或者直接用公开数据集校准自己的评测流程。
真值固定之后,那些调不动的参数才有意义
召回率不可信的更深一层代价是:它会让一整类参数变得不可调。HNSW 的 ef_search、M,量化方式的取舍,融合权重,reranker 的候选深度——这些参数的最优取值都会跟着真值口径一起移动。
有真值之前,团队调参靠的是"改了之后召回率涨了"。但这个信号里混着两个来源:检索确实变好了,以及被测索引更接近它自己给出的答案。后者在换索引、换模型时会全部消失,表现就是"参数明明调好了,换了个 embedding 模型又回到原点"。
真值固定之后,调参的因果关系才成立:新索引在固定基准上的召回率是可比的,改动的前后差异不会被基准漂移吞掉。更实际的一点是,它让"该不该上重排"这种架构问题有了判据——如果固定真值下 recall@1000 已经很高,重排能带来的上限就有限,投入产出比可以直接估算,而不是靠 A/B 上线后看业务指标。
一个必须写明的边界
这套基准是 Qdrant 自己发布的,跑分也主要用它自家的 Supernova 在自家和友商引擎上完成。厂商自建基准天然带着立场——哪怕数据和方法都公开,选哪批查询、怎么分组展示,仍然有取舍空间。把它当成"可复用的真值来源和一套方法论"是合理的,把它当成"第三方中立排名"不是。
另外值得记一笔的是,围绕这套数据的二手转述已经出现了比一手文档更乐观的说法,比如"抽三分之一语料就能代表全量排名"。这种表述在 nova bf 的官方文档里找不到出处,而且和上面那句 --max-files 的原文直接冲突。查这类数字时,以工具文档和数据集卡为准。
落到自己的系统上,先做一次真值校准
不必一步到位重做评测体系,先把"当前那份真值的来源"问清楚,通常就能发现问题。具体可以按这个顺序走:
- 找出评测代码里 top-k 真值的生成路径。如果它调用了你自己线上用的那套索引或同一个封装,问题已经确认了。
- 取一个能被暴力算的语料子集——比如你最大的那个租户,十万条以内——用同一个查询集分别跑暴力真值和现行真值,看两者的 top-k 重合多少。
- 如果重合度低于 0.95,先把线上评测的绝对数字作废,别急着调参。
- 把过滤查询单独拆出来统计,报出每条查询的候选集大小,确认分母足够。
- 真值文件连同生成参数(语料版本、
--max-files、TF32 开关、并列切法)一起入库,下次换索引时才有可比性。
校准本身花不了多少时间,真正的成本在改口径之后的沟通上。把线上评测的绝对数字作废,意味着过去几个月用来证明"检索改造有效"的那些图表要重新解释。比较务实的做法是两条曲线并行一段时间:旧口径继续作为回归基线(保证趋势可读),新口径作为质量真相(决定方向对错),等新口径稳定运行两三个迭代周期再切换主指标。这样既不会让历史数据断档,也不会让一份不成立的真值继续指导决策。
召回率是个比值,分子的质量取决于分母的质量。分母如果来自被测系统自身,这个数字衡量的是自洽性,不是检索质量。