ES 0.90 的三个打分查询,怎样被 function_score 收成一个

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_factorfunction_score 的 boost_factor系数是否作用于同一层
custom_score 的 scriptscript_score是否设置 boost_mode: replace
custom_filters_score 的 filtersfunctions 数组score_mode 是否保持原值
max_boostmax_boost封顶发生在合并的哪一步

小版本里的改名,不宜解释为仓促

文档在 score_mode 的取值列表中同时列出两项,见第 67–68 行:

multiply:: scores are multiplied (default) mult:: scores are multiplied (default), deprecated [0.90.5, Renamed to multiply]

可以确认的是 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 取值是 mult0.90.5 之前的命名
范围条件写 from / to0.90 早期区间写法,后期有 gte / lt
脚本里出现 _score迁移到 function_score 时要确认 boost_mode
响应里判断 ok 字段0.90 年代响应格式的常见痕迹

认出它们之后,迁移顺序建议是:先统一到 function_score 的同一结构,再处理区间写法,最后处理脚本语言。一次只改一类,比对集合与排序,避免多个变量同时变化导致无法归因。

这次合并留下了什么

三个旧查询各管一摊,边界还重叠;function_score 把需求收进“查询 + 一组函数 + 两个合并维度”。它真正留下的是两个正交维度:函数之间怎么合,以及函数结果与原始分怎么合。

对维护老代码的人来说,最实用的一条是:迁移不等于等价。旧脚本结果就是最终分,新结构默认会把查询分再乘一次,需要显式 boost_mode: replace 才能贴近原行为。这类差异不会报错,只会让排序漂移,所以必须用固定数据比对集合与顺序。

这一代写法的速查表

需求0.90 早期写法function_score 写法
乘一个固定系数custom_boost_factorboost_factor
用脚本改分custom_scorescript_score,注意 boost_mode
命中过滤器再加权custom_filters_scorefunctions + 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。用固定数据、包含边界值的数据分别验证集合与排序,比只看查询能否执行可靠得多。

参考

本文核对了固定标签文档的废弃标记、函数列表、合并模式与迁移对照。「在 7.17.29 上跑一遍」里已实际发送三个旧打分查询:均报 unknown query,custom_score 的报错还提示了 function_score;function_score 命中 4 条。本机无 0.90 二进制,得分与排序为语义推导、未实测;衰减函数与脚本相乘未测。

0
0
0
0
评论
未登录
暂无评论