ES 使用FunctionScore实现自定义评分

一、function_score 简介

function_score内容较多,此篇主要是对function_score内容做系统性的讲解,之后会出几篇实际应用的方法,参考以下链接

ElasticSearch - function_score (field_value_factor具体实例)(https://blog.csdn.net/weixin_40341116/article/details/80913067)

ElasticSearch - function_score (weight具体实例)(https://blog.csdn.net/weixin_40341116/article/details/80931573)

ElasticSearch - function_score (衰减函数 linear、exp、gauss 具体实例)(https://blog.csdn.net/weixin_40341116/article/details/81003513)


1. 在使用ES进行全文搜索时,搜索结果默认会以文档的相关度进行排序,而这个 "文档的相关度",是可以透过 function_score 自己定义的,也就是说我们可以透过使用function_score,来控制 "怎麽样的文档相关度更高" 这件事

  • function_score是专门用于处理文档_score的DSL,它允许爲每个主查询query匹配的文档应用加强函数, 以达到改变原始查询评分 score的目的
  • function_score会在主查询query结束后对每一个匹配的文档进行一系列的重打分操作,能够对多个字段一起进行综合评估,且能够使用 filter 将结果划分爲多个子集 (每个特性一个filter),并爲每个子集使用不同的加强函数

2. function_score 提供了几种加强_score计算的函数

  • weight : 设置一个简单而不被规范化的权重提升值

    • weight加强函数和 boost参数类似,可以用于任何查询,不过有一点差别是weight不会被Lucene nomalize成难以理解的浮点数,而是直接被应用 (boost会被nomalize)
    • 例如当 weight 爲 2 时,最终结果爲new_score = old_score * 2
  • field_value_factor : 将某个字段的值乘上old_score

    • 像是将 字段shareCount 或是 字段likiCount 作爲考虑因素,new_score = old_score * 那个文档的likeCount的值
  • random_score : 爲每个用户都使用一个不同的随机评分对结果排序,但对某一具体用户来说,看到的顺序始终是一致的

  • 衰减函数 (linear、exp、guass) : 以某个字段的值为基准,距离某个值越近得分越高

  • script_score : 当需求超出以上范围时,可以用自定义脚本完全控制评分计算,不过因为还要额外维护脚本不好维护,因此尽量使用ES提供的评分函数,需求真的无法满足再使用script_score

3. function_scroe其他辅助的参数

  • boost_mode : 决定 old_score 和 加强score 如何合併
    • multiply(默认) : new_score = old_score * 加强score
    • sum : new_score = old_score + 加强score
    • min : old_score 和 加强score 取较小值,new_score = min(old_score, 加强score)
    • max : old_score 和 加强score 取较大值,new_score = max(old_score, 加强score)
    • replace : 加强score直接替换掉old_score,new_score = 加强score
  • score_mode : 决定functions裡面的加强score们怎麽合併,会先合併加强score们成一个总加强score,再使用总加强score去和old_score做合併,换言之就是会先执行score_mode,再执行boost_mode
    • multiply (默认)
    • sum
    • avg
    • first : 使用首个函数(可以有filter,也可以没有)的结果作为最终结果
    • max
    • min
  • max_boost : 限制加强函数的最大效果,就是限制加强score最大能多少,但要注意不会限制old_score
    • 如果加强score超过了max_boost限制的值,会把加强score的值设成max_boost的值
    • 假设加强score是5,而max_boost是2,因为加强score超出了max_boost的限制,所以max_boost就会把加强score改为2
    • 简单的说,就是加强score = min(加强score, max_boost)

4. function_score查询模板

  • 如果要使用function_score改变分数,要使用function_score查询

  • 简单的说,就是在一个function_score内部的query的全文搜索得到的_score基础上,给他加上其他字段的评分标准,就能够得到把 "全文搜索 + 其他字段" 综合起来评分的效果

  • 单个加强函数的查询模板

    GET 127.0.0.1/mytest/doc/_search
    {
        "query": {
            "function_score": {
                "query": {.....}, //主查询,查询完后这裡自己会有一个评分,就是old_score
                "field_value_factor": {...}, //在old_score的基础上,给他加强其他字段的评分,这裡会产生一个加强score
    ,如果只有一个加强function时,直接将加强函数名写在query下面就可以了
                "boost_mode": "multiply", //指定用哪种方式结合old_score和加强score成为new_score
                "max_boost": 1.5 //限制加强score的最高分,但是不会限制old_score
            }
        }
    }
    
  • 多个加强函数的查询模板

    • 如果有多个加强函数,那就要使用functions来包含这些加强函数们,functions是一个数组,裡面放著的是将要被使用的加强函数列表

    • 可以为functions裡的加强函数指定一个filter,这样做的话,只有在文档满足此filter的要求,此filter的加强函数才会应用到文挡上,也可以不指定filter,这样的话此加强函数就会应用到全部的文挡上

    • 一个文档可以一次满足多条加强函数和多个filter,如果一次满足多个,那麽就会产生多个加强score,因此ES会使用score_mode定义的方式来合併这些加强score们,得到一个总加强score,得到总加强score之后,才会再使用boost_mode定义的方式去和old_score做合併

    • 像是下面的例子,field_value_factor和gauss这两个加强函数会应用到所有文档上,而weight只会应用到满足filter的文档上,假设有个文档满足了filter的条件,那他就会得到3个加强score,这3个加强score会使用sum的方式合併成一个总加强score,然后才和old_score使用multiply的方式合併

      GET 127.0.0.1/mytest/doc/_search
      {
          "query": {
              "function_score": {
                  "query": {.....},
                  "functions": [   //可以有多个加强函数(或是filter+加强函数),每一个加强函数会产生一个加强score,因
      此functions会有多个加强score
                      { "field_value_factor": ... },
                      { "gauss": ... },
                      { "filter": {...}, "weight": ... }
                  ],
                  "score_mode": "sum", //决定加强score们怎麽合併,
                  "boost_mode": "multiply" //決定總加強score怎麼和old_score合併
              }
          }
      }
      

5. 不要执著在调整function_score上

  • 文档相关度的调整非常玄,"最相关的文档" 是一个难以触及的模糊概念,每个人对文档排序有著不同的想法,这很容易使人陷入持续反覆调整,但是确没有明显的进展

  • 为了避免跳入这种死循环,在调整function_score时,一定要搭配监控用户操作,才有意义

    • 像是如果返回的文档是用户想要的高相关的文档,那麽用户就会选择前10个中的一个文档,得到想要的结果,反之,用户可能会来回点击,或是尝试新的搜索条件
    • 一旦有了这些监控手段,想要调适完美的function_score就不是问题
  • 因此调整function_score的重点在于,要透过监控用户、和用户互动,慢慢去调整我们的搜索条件,而不要妄想一步登天,第一次就把文档的相关度调整到最好,这几乎是不可能的,因为,连用户自己也不知道他自己想要什麽

二、具体实例

1.首先准备数据和索引,在ES插入三笔数据,其中title是text类型,like是integer类型(代表点赞量)

{ "title": "ES 入门", "like": 2 }
{ "title": "ES 进阶", "like": 5 }
{ "title": "ES 最高难度", "like": 10 }

2.先使用一般的query,查看普通的查询的评分会是如何

GET 127.0.0.1/mytest/doc/_search
{
    "query": {
        "match": {
            "title": "ES"
        }
    }
}
"hits": [
    {
        "_score": 0.2876821,
        "_source": { "title": "ES 入门", "like": 2 }
    },
    {
        "_score": 0.20309238,
        "_source": { "title": "ES 进阶", "like": 5 }
    },
    {
        "_score": 0.16540512,
        "_source": { "title": "ES 最高难度", "like": 10 }
    }
]

3.使用function_score 的 field_value_factor改变_score,将old_score乘上like的值

  • 本来 "ES最高难度" 的score是0.16540512,经过field_value_factor的改变,乘上了那个文档中的like值(10)之后,新的score变为 1.6540513
GET 127.0.0.1/mytest/doc/_search
{
    "query": {
        "function_score": {
            "query": {
                "match": {
                    "title": "ES"
                }
            },
            "field_value_factor": {
                "field": "like"
            }
        }
    }
}
"hits": [
    {
        "_score": 1.6540513, //原本是0.16540512
        "_source": { "title": "ES 最高难度", "like": 10 }
    },
    {
        "_score": 1.0154619, //原本是0.20309238
        "_source": { "title": "ES 进阶", "like": 5 }
    },
    {
        "_score": 0.5753642, //原本是0.2876821
        "_source": { "title": "ES 入门", "like": 2 }
    }
]

4.加上max_boost,限制field_value_factor的最大加强score

  • 可以看到ES入门的加强score是2,在max_boost限制裡,所以不受影响

  • 而ES进阶和ES最高难度的field_value_factor函数产生的加强score因为超过max_boost的限制,所以被设为3

{
    "query": {
        "function_score": {
            "query": {
                "match": {
                    "title": "ES"
                }
            },
            "field_value_factor": {
                "field": "like"
            },
            "max_boost": 3
        }
    }
}
"hits": [
    {
        "_score": 0.6092771, //原本是0.20309238
        "_source": { "title": "ES 进阶", "like": 5 }
    },
    {
        "_score": 0.5753642, //原本是0.2876821
        "_source": { "title": "ES 入门", "like": 2 }
    },
    {
        "_score": 0.49621537, //原本是0.16540512
        "_source": { "title": "ES 最高难度", "like": 10 }
    }
]

5.有时候线性的计算new_score = old_score * like值的效果并不是那麽好,field_value_factor中还支持 modifier、factor 参数,可以改变like值对old_score的影响

  • modifier参数支持的值

    • none : new_score = old_score * like值
      • 默认状态就是none,线性
    • log1p : new_score = old_score * log(1 + like值)
      • 最常用,可以让like值字段的评分曲线更平滑
    • log2p : new_score = old_score * log(2 + like值)
    • ln : new_score = old_score * ln(like值)
    • ln1p : new_score = old_score * ln(1 + like值)
    • ln2p : new_score = old_score * ln(2 + like值)
    • square : 计算平方
    • sqrt : 计算平方根
    • reciprocal : 计算倒数
  • factor参数

    • factor作为一个调节用的参数,没有modifier那麽强大会改变整个曲线,他仅改变一些常量值,设置factor>1会提昇效果,factor<1会降低效果
    • 假设modifier是log1p,那麽加入了factor的公式就是new_score = old_score * log(1 + factor * like值)
  • 对刚刚的例子加上 modifier、factor

    GET 127.0.0.1/mytest/doc/_search
    {
        "query": {
            "function_score": {
                "query": {
                    "match": {
                        "title": "ES"
                    }
                },
                "field_value_factor": {
                    "field": "like",
                    "modifier": "log1p",
                    "factor": 2
                }
            }
        }
    }
    
  • 就算加上了modifier,但是 "全文评分 与 field_value_factor函数值乘积" 的效果可能还是太大,我们可以通过参数boost_mode来决定 old_score 和 加强score 合併的方法

    • 如果将boost_mode改成sum,可以大幅弱化最终效果,特别是使用一个较小的factor时

    • 加入了boost_mode=sum、且factor=0.1的公式变为new_score = old_score + log(1 + 0.1 * like值)

    • GET 127.0.0.1/mytest/doc/_search
      {
          "query": {
              "function_score": {
                  "query": {
                      "match": {
                          "title": "ES"
                      }
                  },
                  "field_value_factor": {
                      "field": "like",
                      "modifier": "log1p",
                      "factor": 0.1
                  },
                  "boost_mode": "sum"
              }
          }
      }
      

三、java 代码分析

List<FunctionScoreQueryBuilder.FilterFunctionBuilder> filterFunctionBuilders = new ArrayList<>();

// 用户端默认排序优先使用是否置顶参数
//将置顶的数据设置权重100。old_score*100,提升_score分数
FunctionScoreQueryBuilder.FilterFunctionBuilder isTop = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery(EsConstant.IS_TOP, 1),ScoreFunctionBuilders.weightFactorFunction(100));
//将非置顶的数据设置权重1。old_score*1,_score分数保持不变
FunctionScoreQueryBuilder.FilterFunctionBuilder noTop = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery(EsConstant.IS_TOP, 0), ScoreFunctionBuilders.weightFactorFunction(1));
filterFunctionBuilders.add(isTop);
filterFunctionBuilders.add(noTop);

// 评论数 log1p。子分数= log(1 + 0.1 * EsConstant.COMMENT_NUM值)
ScoreFunctionBuilder<FieldValueFactorFunctionBuilder> commentNumScoreFunction = new FieldValueFactorFunctionBuilder(EsConstant.COMMENT_NUM).modifier(FieldValueFactorFunction.Modifier.LOG1P).factor(0.1f);
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(commentNumScoreFunction));
// 销量数 log1p。子分数= log(1 + 0.1 * EsConstant.SOLD_NUM值)
ScoreFunctionBuilder<FieldValueFactorFunctionBuilder> saleNumScoreFunction = new FieldValueFactorFunctionBuilder(EsConstant.SOLD_NUM).modifier(FieldValueFactorFunction.Modifier.LOG1P).factor(0.1f);
filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(saleNumScoreFunction));

filterFunctionBuilders.toArray();
//scoreMode是加强分之间的处理方式:加和,boostMode是old_score与总加强分之间的关系:加和
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(boolQueryBuilder, ArrayUtil.toArray(filterFunctionBuilders, FunctionScoreQueryBuilder.FilterFunctionBuilder.class))
                    .scoreMode(FunctionScoreQuery.ScoreMode.SUM).boostMode(CombineFunction.SUM);
// 封装所有的查询条件(带有function score)
searchSourceBuilder.query(functionScoreQueryBuilder);

站在巨人肩膀上摘苹果

https://blog.csdn.net/weixin_40341116/article/details/80913067

https://blog.csdn.net/weixin_40341116/article/details/80913045

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html

https://my.oschina.net/u/3734816/blog/3105125

https://www.cnblogs.com/yinjihuan/p/13570778.html

https://www.jianshu.com/p/0bfdc8e5e975

posted @ 2022-04-19 17:27  未月廿三  阅读(2121)  评论(0编辑  收藏  举报