0.90 早期改分要靠三个查询:
custom_boost_factor乘系数、custom_score写脚本、custom_filters_score按过滤器加权。function_score在 0.90.4 加入,同一版把三个旧查询标为废弃。本文按固定标签核对函数列表、两个合并维度、迁移对照和脚本相乘这个易错点。
说明
实测环境:本地另起一个 Elasticsearch 7.17.29 单节点实例(官方 tar 包,端口 9200,独立 path.data);本机没有 0.90 / 1.x / 2.x 二进制,所以这里验的是这些旧写法在较新版本上的当前行为,不是当年的执行过程。文末「在 7.17.29 上跑一遍」里的命令与响应均取自该实例。
本文只讨论 Elasticsearch 0.90 的历史实现,不表示当前云服务支持这些旧接口。示例用于准备隔离的旧版本验证环境,不得在生产集群执行,也不要对外开放旧版本服务。文末实测小节里的命令与响应取自本机 7.17.29 实例;0.90 当年的执行过程无实例可对照。
先看三个查询各自解决什么
0.90 早期没有统一的改分入口,需求分散在三个查询里。
custom_boost_factor:包住一个查询,把得分乘以给定系数。文档给出的理由是,查询上直接写的 boost 会被归一化,而这里的 boost_factor 不会,见该查询文档第 11 行附近的表述。
"custom_boost_factor" : {
"query" : { .... },
"boost_factor" : 5.2
}
custom_score:用脚本计算得分,可以引用文档里的数值字段。
"custom_score" : {
"query" : { .... },
"script" : "_score * doc['my_numeric_field'].value"
}
文档同时建议:脚本会被缓存,需要变化的值应通过 params 传入,而不是拼进脚本字符串。这条建议与后来的脚本实践一致,只是语言不同。
custom_filters_score:执行查询,若命中给定的有序过滤器,就用该过滤器关联的 boost 或 script 计算得分。
{
"custom_filters_score" : {
"query" : { "match_all" : {} },
"filters" : [
{ "filter" : { "range" : { "age" : {"from" : 0, "to" : 10} } }, "boost" : "3" },
{ "filter" : { "range" : { "age" : {"from" : 10, "to" : 20} } }, "boost" : "2" }
],
"score_mode" : "first"
}
}
它的 score_mode 决定多个过滤器命中时如何取用,max_boost 用于给结果封顶。文档提到的性能理由是过滤器容易被缓存,这与同一时期“能过滤就别查询”的思路一致,参见 filtered 查询与过滤器那篇。
三个查询之间还有重叠:想加权,可以用 boost、可以用 custom_boost_factor、也可以在脚本里乘。选哪个取决于当年文档的说法,而不是某个统一模型。
0.90.4 新查询加入,旧查询同版废弃
function_score 文档开头标明 added[0.90.4],并说明它提供与另外三个查询相同的功能,还额外增加了距离与时效性打分。
同时,三个旧查询在 v0.90.13 的文档里都带有同样的标记:
deprecated[0.90.4,Replaced by <>]
可在custom_score、custom_boost_factor、custom_filters_score三个文件中分别核对。
需要区分两件事:文档标注废弃,与运行时是否仍可用。废弃标记说明官方不再推荐,也提示用户迁移;不能直接读成“0.90.4 起解析器已拒绝这些查询名”。真实行为应按具体 patch 的解析器注册与测试确认,本文不推断一个精确的移除时间点。
结构上,function_score 把输入分成三层:一个查询或过滤器、一组打分函数、以及合并方式。
"function_score": {
"(query|filter)": {},
"boost": "boost for the whole query",
"functions": [
{ "filter": {}, "FUNCTION": {} },
{ "FUNCTION": {} }
],
"max_boost": number,
"score_mode": "(multiply|max|...)",
"boost_mode": "(multiply|replace|...)"
}
这里出现两个正交维度,是这套结构能延续的关键:score_mode 规定多个函数结果之间如何合并,boost_mode 规定函数结果与原始查询得分如何合并。旧 custom_filters_score 的 score_mode 概念被保留下来,位置移到“多个函数”这一层。
0.90.13 实际提供哪些打分函数
按同一份文档整理,v0.90.13 的 function_score 至少提供四类函数。这说明“0.90.4 引入时只有空壳”并不成立,函数在后续 patch 中继续补齐。
| 函数 | 作用 | 核对位置 |
|---|---|---|
script_score | 用脚本计算,可引用 _score 与数值字段 | 文档第 94–130 行 |
boost_factor | 把得分乘以给定系数 | 文档第 132–141 行 |
random_score | 按伪随机算法生成得分 | 文档第 144–151 行 |
decay(linear / exp / gauss) | 按距离或时间衰减打分 | 文档第 156–181 行 |
decay 函数需要指定 origin、scale,可选 offset 与 decay;字段必须是数值字段,若是地理点或日期,scale、offset 要带单位,见文档第 166–181 行。
也就是说,官方在 0.90.4 说明里提到的 distance 与 recency scoring,对应的是这组 decay 函数。但它们在同一标签的文档里才完整描述,不能由 0.90.4 的说明直接推断 0.90.4 那次发布就已经包含全部衰减函数。查具体能力时应按实际运行版本核对。
本文不把 weight、field_value_factor 放进 0.90 示例,因为它们属于更晚的形态,本轮没有在 0.90 标签中核实。写历史示例时混用后来参数,会让读者误以为旧版本支持。
迁移时脚本结果会被再乘一次
文档给出了三组对照,其中最有价值的是 custom_score 到 function_score 的转换:
"function_score": {
"boost_mode": "replace",
"query": {...},
"script_score": {
"params": { "param1": 2, "param2": 3.1 },
"script": "_score * doc['my_numeric_field'].value / pow(param1, param2)"
}
}
对照原文见文档第 428–440 行。注意它显式写了 boost_mode: replace。
原因在第 128–130 行:
Note that unlike the <>, the score of the query is multiplied with the result of the script scoring. If you wish to inhibit this, set
"boost_mode": "replace"
这处差异需要强调。旧脚本里写 _score * doc['f'].value 时,脚本结果就是最终分;迁移到 function_score 后若保持默认 boost_mode,会把查询得分再乘一次,可能得到 Q² × B 这类量纲变化。
正确做法是:迁移 custom_score 时明确写出 boost_mode,并按业务需要选择 replace 或保持相乘。本文没有实跑这些查询,上述是公式推导,实际组合逻辑还应以目标版本的解析器为准。
custom_boost_factor 的对应写法是把 boost_factor 直接放进 function_score,见第 402–409 行。custom_filters_score 则转为 functions 数组,每个元素带 filter 与 boost_factor 或 script_score,并保留 score_mode,见第 467–490 行。
| 旧写法 | 对应位置 | 需要额外确认的点 |
|---|---|---|
custom_boost_factor 的 boost_factor | function_score 的 boost_factor | 系数是否作用于同一层 |
custom_score 的 script | script_score | 是否设置 boost_mode: replace |
custom_filters_score 的 filters | functions 数组 | score_mode 是否保持原值 |
max_boost | max_boost | 封顶发生在合并的哪一步 |
小版本里的改名,不宜解释为仓促
文档在 score_mode 的取值列表中同时列出两项,见第 67–68 行:
multiply:: scores are multiplied (default)mult:: scores are multiplied (default), deprecated [0.90.5, Renamed tomultiply]
可以确认的是 0.90.5 把 mult 改名为 multiply。至于 0.90.4 当时是否只能写 mult,需要对照对应 patch 的解析器确认,本文不把这个推断写成事实。
同样不建议从这里推导开发节奏或动机。“新查询加入同时旧查询废弃”是文档记录的事实;“因此当时很急”属于没有依据的动机叙述,本文不采用。
能确定的是:这类改名发生在小版本序列内,说明 0.90 时期的接口稳定性与后来的版本策略不同。维护老代码时,遇到 mult 应知道它是 0.90.5 之前的写法,而不是某个不同的语义。
准备一组能暴露公式差异的数据
下面使用旧版 REST 请求记法,供隔离的 v0.90.13 环境验证。旧版本没有现代 Dev Tools 配套,需要通过受控的 HTTP 客户端发送对应方法、路径与 JSON;不要把这些请求提交给当前生产版本。
索引使用显式 mapping,数值字段声明为 integer,字符串不分词,避免动态 mapping 的类型差异影响对照:
PUT /score_lab_090
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"item": {
"properties": {
"title": { "type": "string", "index": "not_analyzed" },
"popularity": { "type": "integer" },
"age": { "type": "integer" }
}
}
}
}
PUT /score_lab_090/item/a
{"title":"elasticsearch guide","popularity":2,"age":5}
PUT /score_lab_090/item/b
{"title":"elasticsearch guide","popularity":8,"age":15}
PUT /score_lab_090/item/c
{"title":"other book","popularity":8,"age":15}
POST /score_lab_090/_refresh
先逐项确认创建与写入成功;索引名已存在时应换用专用测试名称,不要为运行示例删除未知索引。
数据有意让 a、b 的 title 相同、popularity 不同,c 的 title 不同。这样基础查询分会出现差异,脚本乘上 popularity 之后排序可能变化,能够暴露“是否额外乘了一次查询分”的问题。
旧的 custom_score 写法:
GET /score_lab_090/item/_search
{
"size": 20,
"query": {
"custom_score": {
"query": { "term": { "title": "elasticsearch guide" } },
"script": "_score * doc['popularity'].value"
}
}
}
按默认合并方式迁移后,查询分会被再乘一次。要保留旧行为,应显式 replace:
GET /score_lab_090/item/_search
{
"size": 20,
"query": {
"function_score": {
"query": { "term": { "title": "elasticsearch guide" } },
"boost_mode": "replace",
"script_score": {
"script": "_score * doc['popularity'].value"
}
}
}
}
预期检查的是集合与排序:c 不匹配 term 查询,应不在结果中;a、b 的先后顺序取决于脚本值与是否再次相乘。本文不给出假定的分数值,也不比较不同版本的浮点精度。
再做一次只改 boost_mode 的对照,把 replace 换成默认(删掉该参数即可),比较两次的得分与排序是否不同。这一步能把“迁移保持等价”从假设变成可观察的事实。
按过滤器加权的场景用 functions 与 score_mode: first 验证。这里用 age 区间作为过滤器,并保留旧式区间写法以便对照:
GET /score_lab_090/item/_search
{
"size": 20,
"query": {
"function_score": {
"query": { "match_all": {} },
"functions": [
{ "filter": { "range": { "age": { "from": 0, "to": 10 } } }, "boost_factor": 3 },
{ "filter": { "range": { "age": { "from": 10, "to": 20 } } }, "boost_factor": 2 }
],
"score_mode": "first",
"boost_mode": "multiply"
}
}
}
注意区间默认包含两端,边界处理参见另外那篇对 from/to 与 gte/lt 的核对。这里应准备 age 恰为 10 的文档,确认它落在哪一个过滤器,以及 score_mode: first 取的是哪一项。
后来这条线怎么走的
三个旧查询在后续版本逐步退出;具体在哪一版不再可用,应按对应迁移文档与解析器核实,本文不把废弃标记当作移除时间。1.0 的移除与迁移写法由相邻那篇处理,这里只说明 0.90 内部的模型变化。
脚本语言这条线是独立的:function_score 本身不变,变的是可用脚本语言与默认语言。这类变更影响的是脚本内容,不是打分结构,排查时应分别核对。
从设计上看,值得记的是结构稳定性:function_score 后续增加能力时,主要是在 functions 里增加新的函数类型,score_mode 与 boost_mode 两个合并维度没有改。判断一个抽象是否站得住,可以看新需求是“加一项”还是“改结构”。
衰减函数的参数要按字段类型核对
decay 函数用 origin 表示中心点,scale 表示衰减速率,可选 offset 与 decay。文档对单位有明确要求:字段是地理点时 origin 可用地理格式、scale 与 offset 要带单位;字段是日期时可以用天、周等表示,见第 181–199 行。
文档给出的日期示例是:
"DECAY_FUNCTION": {
"FIELD_NAME": {
"origin": "2013-09-17",
"scale": "10d",
"offset": "5d",
"decay" : 0.5
}
}
其中 offset 与 decay 可省略。省略 decay 时,距离等于 scale 的文档得 0.5,见第 199–209 行。
还有一条容易忽略:不定义 origin 时使用当前时间,且 origin 的格式取决于 mapping 里的日期格式,见第 196 行。对时效性打分来说,这等于让排序随查询时刻变化,回归测试必须固定 origin,否则同一批数据在不同日期会得到不同排序。
三种衰减形状 gauss、exp、linear 决定曲线形状,不影响参数含义。选择哪一种应由业务对“多远算远”的判断决定,本文不给出未实测的选择建议。
用前面的测试索引验证时,可以额外增加一个日期字段,并固定 origin,比较三种形状的排序差异。由于本文未运行这些查询,这里只列出待执行的检查项:同一数据下三种形状的排序是否不同,decay 取不同值时相邻文档的差距如何变化,以及不写 origin 时结果是否随时间变化。
老代码里怎么认出这一代写法
维护 0.90 或 1.x 时代的查询时,可以按几个特征判断它属于哪一代:
| 特征 | 说明 |
|---|---|
查询名是 custom_score、custom_boost_factor、custom_filters_score | 属于 function_score 之前的写法 |
score_mode 取值是 mult | 0.90.5 之前的命名 |
范围条件写 from / to | 0.90 早期区间写法,后期有 gte / lt |
脚本里出现 _score | 迁移到 function_score 时要确认 boost_mode |
响应里判断 ok 字段 | 0.90 年代响应格式的常见痕迹 |
认出它们之后,迁移顺序建议是:先统一到 function_score 的同一结构,再处理区间写法,最后处理脚本语言。一次只改一类,比对集合与排序,避免多个变量同时变化导致无法归因。
这次合并留下了什么
三个旧查询各管一摊,边界还重叠;function_score 把需求收进“查询 + 一组函数 + 两个合并维度”。它真正留下的是两个正交维度:函数之间怎么合,以及函数结果与原始分怎么合。
对维护老代码的人来说,最实用的一条是:迁移不等于等价。旧脚本结果就是最终分,新结构默认会把查询分再乘一次,需要显式 boost_mode: replace 才能贴近原行为。这类差异不会报错,只会让排序漂移,所以必须用固定数据比对集合与顺序。
这一代写法的速查表
| 需求 | 0.90 早期写法 | function_score 写法 |
|---|---|---|
| 乘一个固定系数 | custom_boost_factor | boost_factor |
| 用脚本改分 | custom_score | script_score,注意 boost_mode |
| 命中过滤器再加权 | custom_filters_score | functions + filter |
| 多个结果如何合并 | score_mode(first 等) | score_mode,位置移到函数层 |
| 与原始查询分如何合并 | 无此层,脚本结果即最终分 | boost_mode(multiply / replace 等) |
| 按距离或时间衰减 | 无 | linear / exp / gauss |
| 随机排序 | 无 | random_score |
表里“无”表示本文核对的这一代文档中没有对应能力,不代表更早或更晚版本的情况。实际排障时仍应回到运行版本的文档与解析器确认。
在 7.17.29 上跑一遍
三个 0.90 打分查询逐个发,再看收拢之后的 function_score。
图 1:7.17.29 单节点实例的实测输出。custom_boost_factor / custom_score / custom_filters_score 三个都报 unknown query,其中 custom_score 还提示了替代项;function_score 命中 4 条。
for q in custom_boost_factor custom_score custom_filters_score; do \
curl -s -X POST 'http://localhost:9200/old-idx/_search' -H 'Content-Type: application/json' \
-d "{\"query\":{\"$q\":{\"query\":{\"match_all\":{}}}}}" \
| jq -r '.error.root_cause[0].reason' | cut -c1-110; done
# unknown query [custom_boost_factor]
# unknown query [custom_score] did you mean any of [constant_score, function_score]?
# unknown query [custom_filters_score]
curl -s -X POST 'http://localhost:9200/old-idx/_search' -H 'Content-Type: application/json' -d '{"query":{"function_score":{"query":{"match_all":{}},"boost":3}}}' \
| jq -c '.hits.total.value'
# 4
图 1 把「三个查询被 function_score 收成一个」落到了当前版本的具体报错上,custom_score 的报错甚至直接点出了 function_score。得分与排序本身仍是语义推导,本机只验了查询能否被接受与命中条数。
边界:
-
图 1 出自 7.17.29 单节点实例,没有 0.90 / 1.x / 2.x 二进制;
-
验的是旧写法在当前版本是否被接受,当年的执行过程与性能未测;
-
其余同类旧写法未逐个枚举。
-
未比较得分数值;衰减函数与脚本相乘行为未测。
写在最后
0.90 的这次整合发生在小版本里:function_score 在 0.90.4 加入,三个旧查询同版被标记废弃,随后 0.90.5 还改过一次合并模式的名字。文档记录的是这些事实,动机部分本文不作推断。
迁移时按三组对照改,并额外确认 boost_mode。用固定数据、包含边界值的数据分别验证集合与排序,比只看查询能否执行可靠得多。
参考
- Function Score Query @ v0.90.13
- Custom Score Query(废弃标记)@ v0.90.13
- Custom Boost Factor Query @ v0.90.13
- Custom Filters Score Query @ v0.90.13
本文核对了固定标签文档的废弃标记、函数列表、合并模式与迁移对照。「在 7.17.29 上跑一遍」里已实际发送三个旧打分查询:均报
unknown query,custom_score的报错还提示了function_score;function_score命中 4 条。本机无 0.90 二进制,得分与排序为语义推导、未实测;衰减函数与脚本相乘未测。
