ES查询-Function score query自定义打分查询

场景

ES 查询会为每个文档计算一个相关度得分,并且按照得分从高到低的顺序返回结果。但是在很多场景下,我们不仅需要搜索到相关文档,并且还想按照某些其他条件排序。

例如我们在查询 ElasticSearch 相关书籍,通过关键字elasticsearch搜索到书籍之后,但是还想将书籍的喜欢人数作为一个排序指标。如果直接使用sort进行排序,得到的结果太绝对。因为使用sort排序后,就不会计算文档的相关性得分了,结果并不太符合我们预期需求。

Function score query

面对上面的情况,ES 提供了Function score queryAPI,通过该 API 可以实现自定义打分机制。对于整个打分过程大致流程如下:

  • ES 根据用户搜索内容获取到对应的文档结果,然后为每个文档结果计算相关度得分。

  • 执行用户自定义打分函数,这一步会为文档得到一个新的得分。

  • 最终在计算文档得分时,将 ES 计算的文档得分和用户自定义打分函数的得分进行计算得到最后的得分。

function_score 函数

ES 默认提供了以下几种打分函数:

  • weight:加权

  • random_score:随机打分

  • field_value_factor:使用字段的数值参与计算分数

  • decay_funtion:衰减函数

  • script_score:自定义脚本

下面我们构建部分测试数据,来实验各种函数的效果,相关代码示例如下:

# 设置mapping
PUT book
{
"mappings": {
"properties": {
"title":{
"type": "text"
},
"content":{
"type": "text"
},
"time":{
"type": "date",
"format": ["yyyy-MM-dd HH:mm:ss"]
},
"like":{
"type": "integer"
}
}
}
}


# 插入测试数据
POST _bulk
{ "index" : { "_index" : "book"} }
{ "title":"elasticsearch stack","content":"elasticsearch for search","time":"2022-01-01 12:00:00" ,"like":10}
{ "index" : { "_index" : "book"} }
{ "title":"elasticsearch stack","content":"logstash for log","time":"2022-01-01 13:00:00" ,"like":9}
{ "index" : { "_index" : "book"} }
{ "title":"elasticsearch stack","content":"filebeat for log","time":"2022-01-01 13:00:00" ,"like":8}

weight

weight是最简单的打分函数,它的逻辑比较简单,就是给每个文档一个权重值。例如我们现在搜索 title 中elasticsearch stack相关文档内容,其结果如下:

GET book/_search
{
"query": {
"match": {
"title": "elasticsearch stack"
}
}
}
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 0.26706278,
    "hits" : [
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "pq7fnYQBrmJ4lO8zwtSX",
        "_score" : 0.26706278,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "elasticsearch for search",
          "time" : "2022-01-01 12:00:00",
          "like" : 10
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "p67fnYQBrmJ4lO8zwtSX",
        "_score" : 0.26706278,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "logstash for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 9
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "qK7fnYQBrmJ4lO8zwtSX",
        "_score" : 0.26706278,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "filebeat for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 8
        }
      }
    ]
  }
}

因为每个文档的 title 都是一样的,所以查出来的结果中每个文档的得分也是一样的。现在我们对content中含有log相关内容的文档增加权重,其示例代码如下:

GET book/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "elasticsearch stack"
}
},
"functions": [
{
"filter": {
"match":{
"content":"log"
}
},
"weight": 2
}
]
}
}
}

最后结果如下:

{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 0.53412557,
    "hits" : [
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "p67fnYQBrmJ4lO8zwtSX",
        "_score" : 0.53412557,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "logstash for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 9
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "qK7fnYQBrmJ4lO8zwtSX",
        "_score" : 0.53412557,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "filebeat for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 8
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "pq7fnYQBrmJ4lO8zwtSX",
        "_score" : 0.26706278,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "elasticsearch for search",
          "time" : "2022-01-01 12:00:00",
          "like" : 10
        }
      }
    ]
  }
}

从结果可以看出,content 中含有log的文档最后的得分都被改变了。

boost_mode

虽然我们通过weight函数自定计算得分,但是你现在一定好奇这个分数是怎么计算出来的。其实这个得分是由boost_mode决定的,默认情况下它有以下几种选项:

  • multiply:自定义得分与原得分相乘,这也是默认值。

  • replace:替换原得分。

  • sun:自定义得分和原得分相加。

  • avg:取两者平均值。

  • max:取两者中最大值。

  • max:取两者中最小值。

现在我们知道了,默认情况下它是将自定义得分值和原得分值相乘得到新的分数,那么上面的示例中,原得分值为0.26706278,自定义得分值为2,最后计算的分值就是0.53412557。我们尝试修改boost_modesum,再次测试:

GET book/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "elasticsearch stack"
}
},
"functions": [
{
"filter": {
"match":{
"content":"log"
}
},
"weight": 2
}
],
"boost_mode": "sum"
}
}
}

最后的结果如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 2.2670627,
    "hits" : [
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "p67fnYQBrmJ4lO8zwtSX",
        "_score" : 2.2670627,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "logstash for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 9
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "qK7fnYQBrmJ4lO8zwtSX",
        "_score" : 2.2670627,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "filebeat for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 8
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "pq7fnYQBrmJ4lO8zwtSX",
        "_score" : 1.2670628,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "elasticsearch for search",
          "time" : "2022-01-01 12:00:00",
          "like" : 10
        }
      }
    ]
  }
}

score_mode

上面我们介绍了原得分和自定义得分的合并规则,可以通过boost_mode来控制。事实上functions中可以定义多个自定义得分函数,如果一个文档同时有好几个得分函数时,那么最后如何将多个自定义得分函数合并呢?

其实多个自定义函数得分合并模式就是通过score_mode来控制的,其默认值和可选值与boost_mode基本一致。不同的在于score_mode中没有replace,但是有first,它的意思是取第一个自定义得分函数中的第一个得分。

GET book/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "elasticsearch stack"
}
},
"functions": [
{
"filter": {
"match":{
"content":"log"
}
},
"weight": 2
},
{
"filter": {
"term":{
"like":9
}
},
"weight": 3
}
],
"boost_mode": "sum",
"score_mode": "multiply"
}
}
}

最后结果如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 6.2670627,
    "hits" : [
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "p67fnYQBrmJ4lO8zwtSX",
        "_score" : 6.2670627,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "logstash for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 9
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "qK7fnYQBrmJ4lO8zwtSX",
        "_score" : 2.2670627,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "filebeat for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 8
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "pq7fnYQBrmJ4lO8zwtSX",
        "_score" : 1.2670628,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "elasticsearch for search",
          "time" : "2022-01-01 12:00:00",
          "like" : 10
        }
      }
    ]
  }
}

对于排名第一的文档其计算结果如下:原得分为0.2670627functions中第一个自定义得分函数得分为2,第二个得分函数得分为3

  • 合并第一个和第二个得分函数的结果。score_modemultiply,最后的合并结果为2*3 = 6

  • 原得分和自定义得分合并。boost_modesum,那么最后的得分就是0.2670627 + 6 = 6.2670627

其整个得分计算过程如下图所示:

ES查询-Function score query自定义打分查询
ES自定义得分

Random

通过名字就可以知道,它是随机打分。该方式生成[0,1)之间均匀分布的随机分数值。

GET book/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "elasticsearch stack"
}
},
"functions": [
{
"filter": {
"match":{
"content":"log"
}
},
"random_score": {
}
}
]
}
}
}

上面就是random_score最简单的使用方式。默认情况下它使用 Lucene 文档 ID 作为随机性来源,但是文档可能会通过合并重新编号导致 ID 变化。如果对于同一用户的请求,官方建议设置相同的seedfield设置为_seq_no。该方式也不是保证一定不变的,如果文档发生更新,_seq_no同样也会发生变化。

Field Value Factor

field_value_factor使用字段的数值参与计算分数。例如我们上面的例子中,想根据用户喜欢数参与计算分值。

GET book/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "elasticsearch stack"
}
},
"functions": [
{
"filter": {
"match_all":{}
},
"field_value_factor": {
"field": "like",
"factor": 2,
"missing":1,
"modifier": "none"
}
}
],
"boost_mode": "sum"
}
}
}

最后的结果如下:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 20.267063,
    "hits" : [
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "pq7fnYQBrmJ4lO8zwtSX",
        "_score" : 20.267063,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "elasticsearch for search",
          "time" : "2022-01-01 12:00:00",
          "like" : 10
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "p67fnYQBrmJ4lO8zwtSX",
        "_score" : 18.267063,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "logstash for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 9
        }
      },
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "qK7fnYQBrmJ4lO8zwtSX",
        "_score" : 16.267063,
        "_source" : {
          "title" : "elasticsearch stack",
          "content" : "filebeat for log",
          "time" : "2022-01-01 13:00:00",
          "like" : 8
        }
      }
    ]
  }
}
  • field:这个代表参与计算的字段。上面例子中就是喜欢人数like

  • factor:乘积因子,默认情况下为 1。我们这里设置的为 2,对于第一个文档 like 的实际值为 10,通过乘积因子之后就变成了 20。

  • missing:该字段的意思是,如果文档中不存在字段,那么该文档的默认的值为多少。

  • modifier:计算函数,为了避免分差过大,用于平滑分数。默认情况下是不处理。其取值如下表:

取值 说明
none factor * field
log log(factor * field)
log1p log(1 + factor * field)
log2p log(2 + factor * field)
ln ln(factor * field)
ln1p ln(1 + factor * field)
ln2p ln(2 + factor * field)
square
sqrt
reciprocal 1/(factor * field)

根据我们设置的查询规则,那么第一个文档它的分数计算就是:0.267063 + 10 * 2 = 20.267063

Decay functions

decay_function衰减函数这个实际应用常见也挺丰富的。例如订外卖时,根据我们当前的位置越远的商家它的排名越靠后。其使用方式如下:

"DECAY_FUNCTION": {
    "FIELD_NAME": {
          "origin""",
          "scale"""
    }
}

其中DECAY_FUNCTION就是衰减函数。ES 一共提供了 3 种:

  • linear:线性函数,它是一条直线,一旦直线与横轴相交,所有其他值的评分都是 0。

  • exp:指数函数,该函数是先剧烈衰减然后变缓。

  • guass:高斯函数,这个也是最常用的。它的特征是先衰减缓慢,然后变快,最后再变缓。

衰减函数支持的参数有以下:

  • origin:中心点,它代表了字段可能的最佳值。如果字段值在该点上文档的评分为满分 1。它支持数值、时间以及经纬度地理坐标字段。必填。

  • scale:衰减率,即一个文档从origin衰减时分数的改变速度。必填。

  • offset:以origin为中心,为它设置一个偏移量范围。在该范围中的文档得分一致。该值默认为 0。

  • decay:从origin衰减到scale所得的评分。该值默认为0.5

为了演示效果,我们构建以下测试数据。

POST _bulk
{ "index" : { "_index" : "book2"} }
{ "start": 103}
{ "index" : { "_index" : "book2"} }
{ "start": 100}
{ "index" : { "_index" : "book2"} }
{ "start": 99}
{ "index" : { "_index" : "book2"} }
{ "start": 90}
{ "index" : { "_index" : "book2"} }
{ "start": 89}
{ "index" : { "_index" : "book2"} }
{ "start": 88}
{ "index" : { "_index" : "book2"} }
{ "start": 80}
{ "index" : { "_index" : "book2"} }
{ "start": 70}
{ "index" : { "_index" : "book2"} }
{ "start": 55}

查询语句如下:

GET book2/_search
{
"query": {
"function_score": {
"query": {"match_all": {}},
"functions": [
{
"gauss": {
"start": {
"origin": 100,
"scale": 10,
"offset": 1,
"decay": 0.5
}
}
}
],
"boost_mode": "sum"
}
}
}

最后运行的结果如下:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 9,
      "relation" : "eq"
    },
    "max_score" : 2.0,
    "hits" : [
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "Oa65noQBrmJ4lO8zgdzu",
        "_score" : 2.0,
        "_source" : {
          "start" : 100
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "Oq65noQBrmJ4lO8zgdzu",
        "_score" : 2.0,
        "_source" : {
          "start" : 99
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "OK65noQBrmJ4lO8zgdzu",
        "_score" : 1.9726549,
        "_source" : {
          "start" : 103
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "O665noQBrmJ4lO8zgdzu",
        "_score" : 1.5703819,
        "_source" : {
          "start" : 90
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "PK65noQBrmJ4lO8zgdzu",
        "_score" : 1.5,
        "_source" : {
          "start" : 89
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "Pa65noQBrmJ4lO8zgdzu",
        "_score" : 1.4322686,
        "_source" : {
          "start" : 88
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "Pq65noQBrmJ4lO8zgdzu",
        "_score" : 1.0818996,
        "_source" : {
          "start" : 80
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "P665noQBrmJ4lO8zgdzu",
        "_score" : 1.0029399,
        "_source" : {
          "start" : 70
        }
      },
      {
        "_index" : "book2",
        "_type" : "_doc",
        "_id" : "QK65noQBrmJ4lO8zgdzu",
        "_score" : 1.0000014,
        "_source" : {
          "start" : 55
        }
      }
    ]
  }
}

这里我们的查询条件是match_all,也就是说我们所有文档的原评分为 1。

  • start 为10099的自定义评分为 1,100 就是原点所以评分为 1 这么什么好说的,而 99 之所以为 1,这是因为offset为 1 导致的。如果 start 为 101 或者 99,那么都认为是原点所以分数为 1。

  • 其中 start 为 89 的文档评分为 1.5。这是因为其距离原点距离为 10,也就是 scale 的值,同时 decay 为 0.5。注意我们这里设置了offset为 1,所以实际原点为[101,99]。

从整体得分上可以看出,距离原点越远,其得分越少。

Script score

如果上面的所有打分模型都不符合你的要求,那么你可以自定义脚本打分。例如下面就是一个最简单的脚本打分示例:

GET book2/_search
{
"query": {
"function_score": {
"query": {"match_all": {}},
"functions": [
{
"script_score": {
"script": "_score * doc['start'].value"
}
}
],
"boost_mode": "sum"
}
}
}

上面脚本就是在原有的分数基础上再乘以start得到自定义分数。当然实际上并没有什么意义,关于如何书写脚本不是本文的重点,具体请官方文档中参考Scripting相关内容。

总结

以上就是Function score query相关内容,在实际开发应用中没有绝对好的方式,还是需要自己结合不同业务采取不同的手段来处理。另外就是即使你有了一个自定义打分标准,在后续随着业务进行,之前的打分机制也可能不太适合新的业务,总体来说还是需要根据业务后续端更新调整。


原文始发于微信公众号(一只菜鸟程序员):ES查询-Function score query自定义打分查询

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72823.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!