ES自定义搜索结果打分机制

Elasticsearch 虽提供强大搜索功能,但默认排序在复杂业务场景下渐显局限。电商需综合商品热度、评分等因素排序;新闻平台看重时效性与权威性;企业知识管理系统要考虑文档重要性等。Function Score Query 应运而生,它允许开发者依据业务规则自定义打分机制,实现精准个性化排序,以满足多样化需求,提升用户搜索体验与业务竞争力。

ES的query结果中的存在一个相关度得分score,默认按照score从高到低排序

Function score query可以实现对最终score的自定义打分

打分逻辑

三个名词概念, 本文中将会用到

  • ES的默认打分 def_score
  • 函数打分值 fun_score
  • 最终文档排序使用的打分 ult_score

在不使用function score query的情况下 def_score 等于 ult_score

使用function score query后, ult_score的计算过程如下:

  1. 执行query获取 def_score
  2. 执行自定义的打分函数,每个文档获取一个新的打分值,记为 fun_score
  3. fun_score 和 def_score 按照某种计算方式(默认相乘),计算得出 ult_score

基本使用方式:

1
2
3
4
5
6
7
8
9
POST index/_search
{
"query": {
"function_score": {
"query": {"match": {"material_no": "acti"}},
"boost_mode": "multiply"
}
}
}

计算方式由boost_mode定义

  • multiply : 相乘(默认),ult_score = def_score * fun_score
  • replace : 替换,ult_score = fun_score
  • sum : 相加,ult_score = def_score + fun_score
  • avg : 取两者的平均值,ult_score = Avg(def_score, fun_score)
  • max : 取两者之中的最大值,ult_score = Max(def_score, fun_score)
  • min : 取两者之中的最小值,ult_score = Min(def_score, fun_score)

打分函数

权重 weight

基本使用:

1
2
3
4
5
6
7
8
9
POST index/_search
{
"query":{
"function_score":{
"query":{"match":{"material_no":"acti"}},
"weight":5
}
}
}

这种方式会给所有匹配文档加权, 等于是将所有def_score进行了等比放大,并不会影响排序结果

通过filter去控制哪些文档进行加权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST index/_search
{
"query":{
"function_score":{
"query:"{"match":{"material_no":"acti"}},
"functions":[
{
"filter":{"match":{"brand_name":"西门子"}},
"weight":5
}
]
}
}
}

这种情况下,只有brand_name 等于 西门子的文档的def_score会被加权, 这种方式可以让我们想要的文档排序是排到前面去

随机打分 random_score

random_score函数会生成 [0,1) 区间的随机数

基本使用

1
2
3
4
5
6
7
8
9
POST index/_search
{
"query":{
"function_score":{
"query":{"match":{"material_no":"acti"}},
"random_score":{}
}
}
}

这种情况下,每次的排序结果都会不同

如果在某些情况下需要同一用户的随机结果保持前后一致,可以通过为每个用户指定seed来实现

1
2
3
4
5
6
7
8
9
10
11
POST index/_search
{
"query":{
"function_score":{
"query":{"match":{"material_no":"acti"}},
"random_score":{
"seed":10
}
}
}
}

这种写法可以实现我们上面的要求,但是es会报警告

image-2022052682009192 PM

大概意思为7.0之后,设置seed必须提供field参数

这是因为不设置field时,会使用Lucene doc ids作为随机源,会消耗大量内存

官方建议设置field为 _seq_no (索引序列号)

注意 : 如果索引进行了更新,则_seq_no也会进行更新,则随机数也会改变

1
2
3
4
5
6
7
8
9
10
11
12
POST index/_search
{
"query":{
"function_score":{
"query":{"match":{"material_no":"acti"}},
"random_score":{
"seed":10,
"field":"_seq_no"
}
}
}
}

字段值打分 field_value_factor

使用文档中指定字段的值计算ult_score

比如brand表中有用于排序的sort字段,我们想要匹配文档按照sort字段进行排序,可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST index/_search
{
"query":{
"function_score":{
"query":{"match_all":{}},
"field_value_factor":{
"field":"sort",
"factor":1.5,
"missing":0,
"modifier":"ln"
}
}
}
}
  • field : 指定字段
  • factor : 乘积因子, 与指定字段值相乘, 默认为1
  • missing : 缺省值, 如果field不存在,则使用missing值
  • modifier : 计算函数,为了避免分数相差过大,用于平滑分数,有如下几种
    • none : 不处理,默认
    • log : 自然对数, log(factor * field_value)
    • log1p :自然对数, log(1 + factor * field_value)
    • log2p :自然对数 , log(2 + factor * field_value)
    • ln : 自然对数, ln(factor * field_value)
    • ln1p : 自然对数, ln(1 + factor * field_value)
    • ln2p : 自然对数, ln(2 + factor * field_value)
    • square : 平方, (factor * field_value)^2
    • sqrt : 开方, sqrt(factor * field_value)
    • reciprocal : 倒数,1/(factor * field_value)

在上面的写法中 , 假设文档A的 def_score = 0.8 , sort = 5

则 ult_score = 0.8 * ln( 1.5 * 5 ) = 1.612

衰减函数 decay_function

以某一数值(日期,数值,位置)作为中心点, 按照设置的比例逐渐衰减

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST index/_search
{
"query":{
"function_score":{
"query":{"match_all":{}},
"gauss":{
"created_at":{
"origin":"2000-01-05",
"scale":"10d",
"offset":"5d",
"decay":0.5
}
}
}
}
}

三种函数 :

  • linear :线性函数
  • exp: 指数函数
  • gauss: 高斯函数

参数含义:

  • origin : 中心点 (数值, 日期 ,坐标 )
  • scale : 到中心点的距离
  • offset : 偏移量
  • decay: 衰减指数

origin 等于 2000-01-05, scale 等于 10天

意味着 created_at 在 2000-01-01 到 2000-01-10 区间内的文档的权重为 1

created_at在 scale + offset = 15天 之外的文档的权重为0.5

script_score 脚本打分

自由度最高的一种打分函数

1
2
3
4
5
6
7
8
9
10
11
12
13
POST index/_search
{
"query":{
"function_score":{
"query":{"match_all":{}},
"script_score":{
"script":{
"source":"Math.log(2+doc['sort'].value)"
}
}
}
}
}

使用 doc[‘field_name’].value 引用指定字段的值

source可以设置为任意的算法

<更新>

去除硬编码, 使用params参数使脚本更灵活,使source不用重复编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST index/_search
{
"query":{
"function_score":{
"query":{"match_all":{}},
"script_score":{
"script":{
"source":"Math.log('multiplier'+doc['sort'].value)",
"params":{"multiplier":2}
}
}
}
}
}

注意:脚本编译有频率限制,编译非常耗时,这是es的自我保护机制 ; 默认情况下,每 5 分钟最多可以编译 150 个脚本

调整方法:

1
2
3
4
PUT _cluster/settings
{
"transient" : {"script.max_compilations_rate" : "100/1m"}
}

使用存储脚本的API来存储和检索脚本 , 存储的脚本可缩短编译时间并加快搜索速度。

1
2
3
4
5
6
7
POST /_scripts/calc_score
{
"script": {
"lang": "painless",
"source": "Math.log(_score * 2) + params['multiplier']"
}
}

使用存储的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

POST product4/_search
{
"query": {
"function_score": {
"query": { "match_all": {}},
"script_score": {
"script": {
"id": "calc_score",
"params": {"multiplier": 2}
}
}
}
}
}

删除

1
DELETE _scripts/calc_score