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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM