ES|QL|rescore_oversample 可以填 0 了,填上就等于跳过重打分

举报
岳涛 发表于 2026/11/08 08:24:13 2026/11/08
【摘要】 9.1 起 ES|QL 的 rescoreoversample 允许填 0。

9.1 起 ES|QL 的 rescore_oversample 允许填 0。0 是"别过采样、也别重打分",不是数值意义上的零,不是"过采样 0 倍"——这是个特殊值。

说明

实测环境:本地起的 Elasticsearch 9.4.2 单节点实例(官方 tar 包直接跑,堆内存 512MB,安全与机器学习关闭),curl 连本地 9200 端口。本文「实测」小节的命令和输出都出自这个实例。

来源和边界先交代:本文的依据出自 v9.4.2 标签下的官方文件(9.1.0.yml、KnnQuery.java、Knn.java),文中引用的原文均来自这些文件。示例取自官方示例;哪些是文档写明、哪些是我的推断,正文里逐处标注。

01-9.1.0

图 1:官方文件 9.1.0.yml(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 9.1.0.yml

官方条目

9.1 的 changelog 标为 enhancement(9.1.0.yml 第 720–723 行 @ v9.4.2),area: Vector Search:

Allow zero for `rescore_vector.oversample` to indicate by-passing oversample and rescoring

只有一行 summary,但这句话本身信息量够——它明确说了 0 的含义是"跳过"。

02-KnnQuery

图 2:官方文件 KnnQuery.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 KnnQuery.java

先说重打分和过采样

向量检索里有个常见做法:先用量化向量快速粗排,再用原始向量精排。

  • 量化向量(BBQ、int8 等)体积小、算得快,但精度有损;
  • 原始向量(float32)精度高,但算得慢、占空间大。

所以流程是:

  1. 用量化向量找一批候选;
  2. 对候选用原始向量重算一次分数,重新排序。

第 2 步就是重打分(rescoring)。

过采样(oversample) 解决的是第 1 步要取多少候选的问题。如果只要 Top 10,直接取 10 个量化候选去重排,可能漏掉"量化算出来排第 12、但真实分数排第 3"的那条。

所以做法是:多取一些。oversample = 3 表示取 10 × 3 = 30 个候选,重排后返回前 10。

权衡很清楚:oversample 越大,召回越好,但要重排的候选越多,也就越慢。 这个旋钮调的就是"愿意为精度多付多少时间"。

03-Knn

图 3:官方文件 Knn.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 Knn.java

0 是什么意思

按数值理解,0 倍过采样 = 取 0 个候选 = 什么都返回不了。这显然不是它想表达的。

官方那行 summary 已经说清了:0 表示 by-passing oversample and rescoring——跳过过采样和重打分。

也就是说:不做重打分这一步,直接用量化向量的结果返回。

把它和相邻取值放在一起看会更清楚:

值 行为
> 1(如 3.0) 多取候选,重打分
1 取 k 个,重打分(等于没多取,但重排了)
0 跳过整个重打分环节
不填 走默认行为

所以 0 是个开关,不是数值。 它跟"1 倍"差的不止一点点——1 还会重排,0 连重排都不做。

为什么需要这个开关

重打分是有代价的:要读原始向量。

量化向量的意义就是"用小体积换速度"。而重打分要把原始 float32 向量读出来重算——这部分开销可能抵消掉量化带来的收益。

所以有些场景宁可不重排:

  • 对精度要求不高的粗筛。比如先粗筛出几百条,后面还有别的排序逻辑;
  • 量化后精度损失本来就很小的场景,重排的收益有限;
  • 延迟敏感、资源紧张,宁可要快一点。

判断:这个开关的价值在于把"要不要重排"的决定权交给你。以前只能靠 oversample 的数值间接影响开销,现在可以直接关掉。我的建议是:确定不需要重排的场景就写 0,别留着默认值白花开销。

什么时候重打分其实是浪费

上面说"重打分有代价",这里把它讲具体,因为它决定了你该不该填 0。

情形一,向量没有量化。 前面提过但值得再说一遍:如果字段存的是原始 float32 向量,根本就没有"用量化粗排、用原始精排"这回事——一步就是精排。这时 oversample 相关的一切都不适用。

情形二,k 相对候选集很大。 假设 k=1000,oversample=3 意味着要重排 3000 条。重排的开销随候选数线性增长,k 大时这个数字会很可观。而 k 大的场景(比如"要尽量多的相似结果")往往对 Top N 的精确顺序并不敏感——重排的收益在这个场景下最小,成本却最大。

情形三,后面还有别的排序。 如果 ES|QL 里 kNN 之后还要按别的字段排(SORT 一个业务分数),那向量部分的精确顺序到后面就被覆盖了。这时花在重排上的开销,很可能没体现在最终结果里。

这三种情况有个共同点:重排投入的成本,没有转化成最终结果的改善。

推断标准就一句话:看最终返回给用户的是什么。如果向量相似度就是最终排序依据 → 重排有价值;如果后面还有一层排序 → 先确认重排的结果有没有被用上。

源码里怎么走的

ES|QL 侧的处理在 KnnQuery.java 第 51–54 行(源码 @ v9.4.2):

Float oversample = (Float) options.get(RESCORE_OVERSAMPLE_FIELD);
if (oversample != null) {
    rescoreVectorBuilder = new RescoreVectorBuilder(oversample);
}

关键点是:只有 oversample != null 时才构造 RescoreVectorBuilder。

也就是说:

  • 不填 → oversample 为 null → 不构造 → 走默认;
  • 填了任何值(包括 0) → 构造 RescoreVectorBuilder。

所以 0 是作为一个明确的值传下去的,由下游的 RescoreVectorBuilder 去解释它的含义。ES|QL 这一层不做特殊处理——它只负责"你填了就传下去"。

(说明:0 的具体判定逻辑在 RescoreVectorBuilder 里,未逐个读那部分代码,所以"0 在哪里被识别成跳过"给不出精确位置。上面这段能确定的只有 ES|QL 侧的传递行为。)

参数定义在 Knn.java 第 154–161 行(源码 @ v9.4.2):

name = "rescore_oversample",
type = "double",
valueHint = { "3.5" },
description = "Applies the specified oversampling for rescoring quantized vectors. "
    + "See [oversampling and rescoring quantized vectors] ... for details."

注意两个细节:

  • 类型是 double,不是整数。所以 3.5 这种小数值是合法的;
  • 文档的 valueHint 给的是 3.5,没有提到 0 这个特殊值。这是 changelog 有、函数文档没有的信息。

第二点要看清楚:如果你只看函数文档,会不知道 0 有特殊含义。这类"文档滞后于代码"的情况在快速迭代的功能上很常见——changelog 有时比参考文档更新得快,排查功能边界时两边都该看。

ES|QL 里怎么写

rescore_oversample 是 KNN 函数的命名参数。大致写法:

FROM my-index
| WHERE KNN(my_vector, [0.1, 0.2, ...], 10, {"rescore_oversample": 0.0})
| LIMIT 10

这里的 0.0 就是要跳过重打分。

注意类型是 double,写 0 还是 0.0 取决于解析是否严格——这一点未实测,稳妥起见写 0.0 更符合声明的类型。

(说明:上面这条查询按参数定义构造,未实际执行过。语法细节以你版本的文档为准。)

和 Query DSL 里该参数的关系

如果你用过 Query DSL 的 kNN 查询,会见过 rescore_vector.oversample——changelog 里用的就是这个名字(rescore_vector.oversample)。

而 ES|QL 里叫 rescore_oversample。两个是同一件事的两种写法,只是入口不同。

判断这次改动两边都生效(changelog 用的是 DSL 侧的名字,而 ES|QL 侧的实现是通用的),但这一点未逐个确认——如果你只关心 ES|QL,测一下就知道。

这是个"把内部行为暴露成开关"的改动

放一起看会更清楚。9.x 里这类改动前面写过几个:

篇 改动 暴露了什么
none 分块策略 none 分块策略 暴露"不切"这个选项
HNSW 早停默认开启 HNSW 早停开关 暴露"是否提前停"
本篇 oversample = 0 暴露"是否重打分"

共同点:这些行为以前就存在,只是你没法控制。ES 内部一定有"什么时候值得重排"的判断,现在把开关交出来了。

这个方向合理,理由跟前两篇一样:取舍该由使用者决定。官方可以定一个对多数人合理的默认,但不该假设所有场景都一样。

代价是参数变多了、理解成本上升。 所以建议是:先按默认跑,遇到具体问题再动这些开关——而不是一开始就把所有旋钮都拧一遍。

这类"0 有特殊含义"的设计尤其要当心。 因为它违反直觉:看到数值参数会默认是连续的,结果中间藏着一个开关。做法是——凡是文档里说"某值表示关闭/跳过"的,用的时候就写注释说明,免得半年后自己都忘了为什么填 0。

这个开关怎么用

具体到 oversample,给一个可操作的顺序:

第一步:先开着重打分跑,拿效果基线。 记录召回质量和延迟。

第二步:填 0 再跑一遍同样的数据。 对比两组的差异。

第三步:看差异有多大。

  • 两份结果几乎一样 → 场景里重打分收益不大,可以填 0 换性能;
  • 差异明显 → 老老实实开着,调 oversample 的数值。

不要用"理论上应该更快"来决定。 量化带来的精度损失跟数据分布有关,别人的结论搬不过来。

还有一个容易忽略的:先确认字段真的用了量化。 如果向量本来就是 float32 没量化,那重打分这一步本来就不存在,填 0 也就没有意义。

测试时容易犯的一个错:只比"开了和关了差多少",却没固定其他变量。k 的大小、过滤条件、数据量都会影响结果。改一个变量测一次,否则两组数据都说明不了问题。

还有个务实的提醒:对比要在生产量级的数据上做。小数据集上量化和原始向量的差异往往体现不出来,测出来的"几乎一样"可能只是因为数据太少,不是真的没差别。

在 9.4.2 上跑一遍

同一个 knn retriever,只改 rescore_vector.oversample 的值:

oversample

图 4:本地 9.4.2 单节点实例上的实测输出。上:填 0;下:填 -1

curl -s -X POST "" \
  -H 'Content-Type: application/json' \
  -d '{"retriever":{"knn":{"field":"v","query_vector":[1,2,3],"k":1,"num_candidates":10,"rescore_vector":{"oversample":0}}}}'

curl -s -X POST "" \
  -H 'Content-Type: application/json' \
  -d '{"retriever":{"knn":{"field":"v","query_vector":[1,2,3],"k":1,"num_candidates":10,"rescore_vector":{"oversample":-1}}}}' \
  | jq -r '"HTTP \(.status): \(.error.root_cause[0].reason)"'

填 0 被接受,查询正常返回;填 -1 返回 HTTP 400: Failed to build [rescore_vector] after last required field arrived。

要留意两件事。一是参数位置:它叫 rescore_vector.oversample,在 retriever 的 knn 段里,不是顶层;写成 rescore_oversample 会拿到 unknown field ... did you mean [rescore_vector]。二是 -1 的报错既没提字段名也没提允许范围,只说"构造失败"——排查时如果只看到这句,很难一眼知道是 oversample 越界。

结论是:0 现在是合法值,填上等于跳过重打分,量化索引又只需要原始精度时可以用它省掉一轮计算;但生成这个参数的模板里要自己加下界校验,别指望 ES 给你一句能看懂的报错。

写在最后

很小的改动:一个参数多接受一个值,且这个值是特殊含义。

一,0 表示跳过过采样和重打分,不是"0 倍采样"。它和 1 的区别是:1 还会重排,0 连重排都不做——这个区别容易看漏。

二,它是个开关不是数值。 用来在"精度"和"速度"之间做选择,不是用来微调的。这类"数值参数里藏开关"的设计违反直觉,我的建议是用的时候在代码里写注释说明,别让下一个人猜。

三,决定前先测,且要在生产量级的数据上测。 量化精度损失跟你自己的数据有关,用两组对比数据说话,不要用"理论上更快"下结论。

另外诚实标注了三处:0 在 RescoreVectorBuilder 里的具体判定位置未逐个读代码;示例查询未执行(含"写 0 还是 0.0"这个细节未实测);是否同时作用于 Query DSL 和 ES|QL 两侧未逐个确认。

参考

本文对照 Elasticsearch v9.4.2 标签的 changelog 与源码核对,引文与代码逐字取自原文。"重打分与过采样的代价"部分为机制说明;0 的具体判定位置未逐个读 RescoreVectorBuilder 代码;图 4 的两次查询已在本地 9.4.2 单节点实例上执行(0 通过、-1 被拒);是否同时作用于 Query DSL 与 ES|QL 未逐个确认,已在正文标注。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。