ES系列13:搞懂徹底掌握相關度:從TF-IDF、BM25到對相關度的控制

帶着問題學習才高效 

 

ES 5.0 之前,默認的相關性算分採用的是 TF-IDF,而之後則默認採用 BM25。

 

1、什麼是相關性/相關度?Lucene 是如何計算相關度的?

2、TF-IDF 和 BM25 究竟是什麼?

3、相關度控制的方式有哪些?各自都有什麼特點?

 

本文從相關性概念入手,到 TF-IDF 和 BM25 講解和數學公式學習,再到詳細介紹多種常用的相關度控制方式。相信對你一定有用!

本文知識導航

 

ps:xmind源文件獲取方式,見文末。

 

 

 

01 什麼是相關性

 

相關性描述的是⼀個⽂檔和查詢語句匹配的程度。ES 會對每個匹配查詢條件的結果進⾏算分_score。_score 的評分越高,相關度越高。

 

對於信息檢索工具,衡量其性能有3大指標:

1)查準率 Precision:儘可能返回較少的無關文檔;

2)查全率 Recall:儘可能返回較多的相關文檔;

3)排序 Ranking:是否能按相關性排序。

前兩者更多與分詞匹配相關,而後者則與相關性的判斷與算分相關。【本文將詳細介紹相關性系列知識點,分詞部分後續TeHero會單獨講解!】

 

 

 

 

02 TF-IDF 和 BM25 是什麼

 

 

2.1 詞頻 TF(Term Frequency)

 

檢索詞在文檔中出現的頻度是多少?出現頻率越高,相關性也越高。

 

關於TF的數學表達式,參考ES官網,如下:

tf(t in d) = √frequency  

詞 t 在文檔 d 的詞頻( tf )是該詞在文檔中出現次數的平方根。

概念理解:比如說我們檢索關鍵字“es”,“es”在文檔A中出現了10次,在文檔B中只出現了1次。我們不會認爲文檔B與“es”的相關性更高,而是文檔A。

 

 

2.2 逆向⽂檔頻率 IDF(Inverse Document Frequency)

 

每個檢索詞在索引中出現的頻率,頻率越高,相關性越低。

 

關於 IDF 的數學表達式,參考ES官網,如下:

idf(t) = 1 + log ( numDocs / (docFreq + 1))  

詞 t 的逆向文檔頻率( idf )是:索引中文檔數量除以所有包含該詞的文檔數,然後求其對數。

注意: 這裏的log是指以e爲底的對數,不是以10爲底的對數。

概念理解:比如說檢索詞“學習ES”,按照Ik分詞會得到兩個Token【學習】【ES】,假設在當前索引下有100個文檔包含Token“學習”,只有10個文檔包含Token“ES”。那麼對於【學習】【ES】這兩個Token來說,出現次數較少的 Token【ES】就可以幫助我們快速縮小範圍找到我們想要的文檔,所以說此時“ES”的權重就比“學習”的權重要高。

 

 

2.3 字段長度準則 field-length norm

 

字段的長度是多少?字段越短,字段的權重越高。檢索詞出現在一個內容短的 title 要比同樣的詞出現在一個內容長的 content 字段權重更大。

 

關於 norm 的數學表達式,參考ES官網,如下:

norm(d) = 1 / √numTerms  

字段長度歸一值( norm )是字段中詞數平方根的倒數。

 

以上三個因素——詞頻(term frequency)、逆向文檔頻率(inverse document frequency)和字段長度歸一值(field-length norm)——是在索引時計算並存儲的。最後將它們結合在一起計算單個詞在特定文檔中的權重。

 

 

 

2.4 Lucene 中的 TF-IDF 評分公式

 

該公式參考自官網:


 

 

score(q,d) =
queryNorm(q)
· coord(q,d)
· ∑ (
tf(t in d)
· idf(t)²
· t.getBoost()
· norm(t,d)
) (t in q)

score(q,d)  文檔d對查詢q的相關性得分

 

queryNorm(q)  查詢的規範化因子

 

coord(q,d)  協調因子

 

文檔d的查詢q中每個詞t的權重之和

 

tf(t in d)  文檔d中t詞的詞頻(出現次數)

 

idf(t)  t詞的逆文檔頻率

 

t.getBoost() 已應用於查詢的boost

 

norm(t,d)  是字段長度歸一值,與檢索時字段的Boost (如果存在)相結合。

雖然現在es的相關性評分算法改爲了BM25,但對於該公式,我們還是應該掌握,這有利於我們理解後續對相關度的控制。

 

 

2.5 BM25

 

整體而言 BM25 就是對 TF-IDF 算法的改進,對於 TF-IDF 算法,TF(t) 部分的值越大,整個公式返回的值就會越大。

 

BM25 就針對這點進行來優化,隨着TF(t) 的逐步加大,該算法的返回值會趨於一個數值。

該圖來自ES官網

 BM25 有一個比較好的特性就是提供了兩個可調參數:

該公式來源於阮一鳴老師的課程

k1:這個參數控制着詞頻結果在詞頻飽和度中的上升速度。默認值爲1.2。值越小飽和度變化越快,值越大飽和度變化越慢。

 

b:這個參數控制着字段長歸一值所起的作用,0.0會禁用歸一化,1.0會啓用完全歸一化。默認值爲0.75。

該公式"."的前部分就是 IDF 的算法,後部分就是 TF+Norm 的算法。

 

 

 

03 explain

 

使用 explain 查看搜索相關性分數的計算過程。這非常有助於我們理解ES的相關度計算過程。下面通過示例來學習:

 

1)導入測試數據


 

 

#創建index
PUT /blogs_index
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 1
}
},
"mappings": {
"_doc": {
"dynamic": false,
"properties": {
"id": {
"type": "integer"
},
"author": {
"type": "keyword"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"tag": {
"type": "keyword"
},
"influence": {
"type": "integer_range"
},
"createAt": {
"type": "date",
"format": "yyyy-MM-dd HH:mm"
}
}
}
}
}

# 導入數據
POST _bulk
{"index":{"_index":"blogs_index","_type":"_doc","_id":"1"}}
{"id":1,"author":"方纔兄","title":"es的相關度","content":"這是關於es的相關度的文章","tag":[1,2,3],"influence":{"gte":10,"lte":12},"createAt":"2020-05-24 10:56"}
{"index":{"_index":"blogs_index","_type":"_doc","_id":"2"}}
{"id":2,"author":"方纔兄","title":"相關度","content":"這是關於相關度的文章","tag":[2,3,4],"influence":{"gte":12,"lte":15},"createAt":"2020-05-23 10:56"}
{"index":{"_index":"blogs_index","_type":"_doc","_id":"3"}}
{"id":3,"author":"方纔兄","title":"es","content":"這是關於關於es和編程的必看文章","tag":[2,3,4],"influence":{"gte":12,"lte":15},"createAt":"2020-05-22 10:56"}
{"index":{"_index":"blogs_index","_type":"_doc","_id":"4"}}
{"id":4,"author":"方纔","title":"關注我,系統學習es","content":"這是關於es的文章,介紹了一點相關度的知識","tag":[1,2,3],"influence":{"gte":10,"lte":15},"createAt":"2020-05-24 10:56"}
 

 

2)使用explain


 

 

GET /blogs_index/_search
{
"query": {
"match": {
"title": "es的相關度"
}
},
"explain": true
}

3)結果與分析


 

 

{

  "took": 1,

  "timed_out": false,

  "_shards": {

    "total": 1,

    "successful": 1,

    "skipped": 0,

    "failed": 0

  },

  "hits": {

    "total": 4,

    "max_score": 2.5933092,

    "hits": [

      {

        "_shard": "[blogs_index][0]",

        "_node": "VAAU48LMQ_iQfscqT2gjLA",

        "_index": "blogs_index",

        "_type": "_doc",

        "_id": "1",

        "_score": 2.5933092,

        "_source": {

          "id": 1,

          "author": "方纔兄",

          "title": "es的相關度",

          "content": "這是關於es的相關度的文章",

          "tag": [

            1,

            2,

            3

          ],

          "influence": {

            "gte": 10,

            "lte": 12

          },

          "createAt": "2020-05-24 10:56"

        },

"_explanation": {
"value": 2.593309,
"description": "sum of:",
"details": [
{
"value": 0.31387395,
"description": "weight(title:es in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.31387395,
"description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:",
"details": [
{
"value": 0.35667494,
"description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
"details": [
{
"value": 3,
"description": "docFreq",
"details": []
},
{
"value": 4,
"description": "docCount",
"details": []
}
]
},
{
"value": 0.88,
"description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
"details": [
{
"value": 1,
"description": "termFreq=1.0",
"details": []
},
{
"value": 1.2,
"description": "parameter k1",
"details": []
},
{
"value": 0.75,
"description": "parameter b",
"details": []
},
{
"value": 3,
"description": "avgFieldLength",
"details": []
},
{
"value": 4,
"description": "fieldLength",
"details": []
}
]
}
]
}
]
},
{
"value": 1.059496,
"description": "weight(title:的 in 0) [PerFieldSimilarity], result of:",
"details": [
……………………………………………………

 

我們簡單分析下文檔1的相關性算分過程,去理解ES的相關性算分:

1)"description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",根據該公式,docCount  = 4,docFreq = 3,計算出 value = log10/7 = ln 10/7 = 0.3566749440

 

2)"description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:", 根據details的信息,計算出 value = (1*(1.2+1)/(1+1.2 *(1-0.75+0.75*4/3)))=  0.88

 

3)BM25(es)= idf * tfNorm = 0.3566749440 * 0.88 = 0.3138739947

 

4)同理得到 BM25(的)= 1.059496,BM25(相關)= 0.6099695,BM25(度)= 0.6099695;

 

5)根據"description": "sum of:",當檢索【es的相關度】,文檔1的_score = BM(es)+ BM25(的)+ BM25(相關)+ BM25(度)= 2.5933092

 

上述算分過程,建議自己使用 explain 實踐一波。畢竟紙上得來終覺淺!

 

 

 

 

04 相關度控制

 

通過上面的學習,我們已經知道了什麼是TF-IDF,什麼是BM25,同時通過explain大致瞭解了ES的相關性算分過程。

 

那麼如果ES默認的相關性算分不符合我們的使用需求,我們可以通過哪些方式去改變或控制相關度評分呢?

 

TeHero通過官網和網課的學習,並結合自身實踐,目前總結了以下4種常用的相關度控制方式,供大家參考:

 

4.1 boost  參數【常用】

 

我們檢索博客時,我們一般會認爲標題 title 的權重應該比內容 content 的權重大,那麼這個時候我們就可以使用 boost 參數進行控制:


 

 

GET /blogs_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": {
"query": "es",
"boost": 2
}
}
},
{
"match": {
"content": "es"
}
}
]
}
},
"explain": true
}

通過 explain 查看下算分過程:

根據結果,我們可以看到:對應文檔的_score = BM25(es in title) + BM25(es in content);其中BM25(es in title)= boost * idf *  tfNorm

boost 參數值範圍:

boost>1 相關度相對性提升

 

0<boost<1,相對性降低

 

boost<0,貢獻負分

 

注意:1)boost 可用於任何查詢語句;2)這種提升或降低並不一定是線性的,新的評分 _score 會在應用權重提升之後被歸一化 ,每種類型的查詢都有自己的歸一算法。

 

 

4.2 查詢方式改變

 

我們也可以通過使用不同的組合查詢來實現對相關度的控制,關於組合查詢,TeHero之前就只講解了布爾查詢【Bool QueryES系列12:Compound queries 之  Bool query】,是因爲剩餘的4種組合查詢涉及到相關度。

 

今天,我們先簡單瞭解下剩餘的4種組合查詢,具體深入的使用,TeHero後面會結合實踐詳細和大家一起交流分享。

 

1)constant_score query

嵌套一個 filter 查詢,爲任意一個匹配的文檔指定一個常量評分,常量值爲 boost 的參數值【默認值爲1】 ,忽略 TF-IDF 信息。


 

 

GET /blogs_index/_search
{
"query": {
"constant_score" : {
"filter" : {
"term" : { "title": "es"}
},
"boost" : 1.2
}
}
}

結果展示:

 

2)function_score query

Function Score Query 允許我們修改通過 query 檢索出來的文檔的分數。

在使用時,我們必須定義一個查詢和一個或多個函數,這些函數爲查詢返回的每個文檔計算一個新分數。

從上圖就可以看到,Function Score Query 涉及的參數很多。此處先簡單看個DSL示例,具體的分析後續專門講解:


 

 

GET /blogs_index/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"boost": "5",
"functions": [
{
"filter": {
"match": {
"title": "es"
}
},
"random_score": {},
"weight": 23
},
{
"filter": {
"match": {
"title": "相關度"
}
},
"weight": 42
}
],
"max_boost": 42,
"score_mode": "max",
"boost_mode": "multiply",
"min_score": 10
}
},
"explain": true
}

備註:function_score query 的用法非常多,適用場景也比較廣,比如說:1)通過文檔中的字段值影響相關度,比如可以讓博客的點贊數越多,相關度越高;2)隨機分數【可應用於千人千面】;3)根據距離參考值的衰減函數計算相關度,比如說地理位置查詢,距離參考點越遠的,相關性越低;4)更復雜的場景也可以用自定義腳本完全控制評分計算,實現所需邏輯。

 

關於對 function_score query 的詳細講解,TeHero後續會和大家分享的。

 

 

3)dis_max query

dis_max query 使用單個最佳匹配查詢子句的分數。同時,也可以通過參數 tie_breaker 【默認值爲0】 控制其他查詢子句的分數對 _score 的影響

 

相關性得分計算公式:_score = max(BM25) + ∑ other(BM25)*tie_breaker


 

 

GET /blogs_index/_search
{
"query": {
"dis_max": {
"tie_breaker": 0.5,
"boost": 1.2,
"queries": [
{
"term": {
"content": "es"
}
},
{
"match": {
"content": "相關度"
}
}
]
}
},
"explain": true
}

注意:queries 下的查詢子句間的布爾關係是OR。

dis_max query 有一個非常好的使用場景就是,利用參數 tie_breaker 能夠確保滿足多個條件的文檔的相關性得分一定比只滿足單個條件的文檔的得分要高。

 

 

4)boosting query【常用】

boosting query 可用於有效降級與給定查詢匹配的結果。與布爾查詢中的“ NOT”子句不同的是,它仍會選擇包含不良詞的文檔,但會降低其總體得分。

 

參數解釋:

positive:用於獲取返回結果

 

negative:對上述結果的相關性打分進行調整

 

negative_boost:調整參數:升權(>1), 降權(>0 and <1)

來看一個DSL示例,我們希望檢索title 包含“es”“相關性”的文章,同時認爲如果content包含“編程”,那我們認爲這個文檔的相關性應該被降低:


 

 

GET /blogs_index/_search
{
"query": {
"boosting": {
"positive": {
"bool": {
"should": [
{
"term": {
"title": "es"
}
},
{
"term": {
"title": "相關性"
}
}
]
}
},
"negative": {
"term": {
"content": "編程"
}
},
"negative_boost": 0.2
}
},
"explain": true
}

DSL分析:

1)根據 positive 下的查詢語句檢索,得到結果集;

 

2)在上述的結果集中,對於那些同時還匹配 negative 查詢的文檔,將通過文檔的原始 _score 與 negative_boost 相乘的方式重新計算相關性得分。

注意:

negative_boost 的值>1,是正向評分,增加匹配 negative 查詢的文檔的權重。

 

 

 

4.3 rescore 結果集重新評分

 

先query,再在結果集基礎上 rescore。query 目前唯一支持的重新打分算法。參數 window_size 是每一分片進行重新評分的頂部文檔數量。

 

看個示例,先檢索content包含“es的相關度”或者 title 包含“es”的文檔,再此基礎上,對於前3個文檔,利用match_phrase 重新計算相關度。


 

 

GET /blogs_index/_search
{
"query": {
"bool": {
"should": [

{
"match": {
"content": {
"query": "es的相關度",
"minimum_should_match": "30%"
}
}
},
{
"match": {
"title": {
"query": "es"
}
}
}
]
}
},
"rescore": {
"window_size": 3,
"query": {
"rescore_query": {
"match_phrase": {
"content": {
"query": "es的相關度",
"slop": 50
}
}
}
}
}
}

rescore 和 上面的 Boosting Query 是比較相似的,都是在 query 結果集的基礎上重新修改相關性得分。但是修改的算法是不一樣的,根據場景需求,選擇即可。

 

同時 rescore  可以利用 window_size 參數控制重新計算得分的文檔數量,在數據量較大的情況,適當控制 window_size 參數,性能上會比 Boosting Query好。

 

 

4.4 更改BM25 參數 k1 和 b 的值

 

在介紹BM25算法時,我們知道 k1 參數【默認值1.2】控制着詞頻結果在詞頻飽和度中的上升速度。b 參數【默認值0.75】控制着字段長歸一值所起的作用。

 

那麼我們就可以通過手動定義這兩個參數的值從而去改變相關性算分。

 

只能在創建index的時候定義字段的similarity ,在後續,可以通過關閉索引,更新索引設置,開啓索引這個過程進行更新 my_bm25 的 參數值。這樣可以無須重建索引又能試驗不同的相似度算法配置。


 

 

PUT /my_index
{
"settings": {
"similarity": {
"my_bm25": {
"type": "BM25",
"b": 0.8,
"k1": 1.5

}
}
},
"mappings": {
"doc": {
"properties": {
"title": {
"type": "text",
"similarity": "my_bm25"
}
}
}
}
}

注意:一般情況不建議更改這兩個參數值。

 

 

05 被破壞的相關度

 

每個分片都會根據該分片內的所有文檔計算一個本地 IDF。這會導致打分偏離,特別是數據量很少時。

 

相關性算分的IDF 在分⽚之間是相互獨⽴。當⽂檔總數很少的情況下,如果主分⽚⼤於 1,主分⽚數越多 ,相關性算分會越不準。

 

ps:瞭解該現象,主要是爲了解決很多小夥伴在做測試時的疑惑。簡單瀏覽即可。

 

5.1 現象示例:


 

 

PUT /blogs_index1
{
"settings": {
"index": {
"number_of_shards": 10,
"number_of_replicas": 0
}
},
"mappings": {
"_doc": {
"dynamic": false,
"properties": {
"id": {
"type": "integer"
},
"author": {
"type": "keyword"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"tag": {
"type": "keyword"
},
"influence": {
"type": "integer_range"
},
"createAt": {
"type": "date",
"format": "yyyy-MM-dd HH:mm"
}
}
}
}
}
POST _bulk
{"index":{"_index":"blogs_index1","_type":"_doc","_id":"1"}}
{"id":1,"author":"方纔兄","title":"es的相關度","content":"這是關於es的相關度的文章","tag":[1,2,3],"influence":{"gte":10,"lte":12},"createAt":"2020-05-24 10:56"}
{"index":{"_index":"blogs_index1","_type":"_doc","_id":"2"}}
{"id":2,"author":"方纔兄","title":"相關度","content":"這是關於相關度的文章","tag":[2,3,4],"influence":{"gte":12,"lte":15},"createAt":"2020-05-23 10:56"}
{"index":{"_index":"blogs_index1","_type":"_doc","_id":"3"}}
{"id":3,"author":"方纔兄","title":"es","content":"這是關於關於es和編程的必看文章","tag":[2,3,4],"influence":{"gte":12,"lte":15},"createAt":"2020-05-22 10:56"}
{"index":{"_index":"blogs_index1","_type":"_doc","_id":"4"}}
{"id":4,"author":"方纔","title":"關注我,系統學習es","content":"這是關於es的文章,介紹了一點相關度的知識","tag":[1,2,3],"influence":{"gte":10,"lte":15},"createAt":"2020-05-24 10:56"}

查詢:


 

 

GET /blogs_index1/_search
{
"_source": "title",
"query": {
"match": {
"title": {
"query": "es"

}
}
}
}

結果:

根據我們前面學的TF-IDF和BM25 算法,很明顯,該結果違背了預期。

 

 

5.2 兩種方式解決

 

1)當數據量不大時,將主分片數設置爲1。【學習過程建議設置爲1】


 

 

PUT /tehero_index
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}

2)搜索的URL 中指定參數 “_search?search_type=dfs_query_then_fetch”


 

 

GET /blogs_index1/_search?search_type=dfs_query_then_fetch
{
"query": {
"match": {
"title": {
"query": "es"

}
}
}
}

使用該參數查詢時,es會到每個分⽚把各分⽚的詞頻和⽂檔頻率進⾏蒐集,然後完整的進⾏⼀次相關性算分,耗費更加多的 CPU 和內存,執⾏性能低下,⼀般不建議使⽤。

 

5.3 該現象不用深究

 

在實際應用中,這並不是一個問題,本地和全局的 IDF 的差異會隨着索引裏文檔數的增多漸漸消失,在真實世界的數據量下,局部的 IDF 會被迅速均化,所以上述問題並不是相關度被破壞所導致的,而是由於數據太少。

 

 

 

06 相關度控制最後要做的事情

 

1、理解評分過程是非常重要的,這樣就可以根據具體的業務對評分結果進行調試、調節、減弱和定製。

2、本文介紹的4種相關度控制方案,建議結合實踐,根據自己的業務需求,多動手調試練習。

3、最相關 這個概念是一個難以觸及的模糊目標,通常不同人對文檔排序又有着不同的想法,這很容易使人陷入持續反覆調整而沒有明顯進展的怪圈。強烈建議不要去追求最相關,而要監控測量搜索結果。

4、評價搜索結果與用戶之間相關程度的指標。如果查詢能返回高相關的文檔,用戶會選擇前五中的一個,得到想要的結果,然後離開。不相關的結果會讓用戶來回點擊並嘗試新的搜索條件。

5、要想物盡其用並將搜索結果提高到 極高的 水平,唯一途徑就是需要具備能評價度量用戶行爲的強大能力。

 

最後,如果你有更好的相關度控制方式,或者在es的學習過程中有疑問,歡迎加入es交流羣,和大家一起交流學習。

文章來自:方纔編程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章