相關度評分背後的理論
Lucene(或 Elasticsearch)使用 布爾模型(Boolean model) 查找匹配文檔,並用一個名爲 實用評分函數(practical scoring function) 的公式來計算相關度。這個公式借鑑了 詞頻/逆向文檔頻率(term frequency/inverse document frequency) 和 向量空間模型(vector space model),同時也加入了一些現代的新特性,如協調因子(coordination factor),字段長度歸一化(field length normalization),以及詞或查詢語句權重提升。
不要緊張!這些概念並沒有像它們字面看起來那麼複雜,儘管本小節提到了算法、公式和數學模型,但內容還是讓人容易理解的,與理解算法本身相比,瞭解這些因素如何影響結果更爲重要。
布爾模型
布爾模型(Boolean Model) 只是在查詢中使用 AND 、 OR 和 NOT (與、或和非)這樣的條件來查找匹配的文檔,以下查詢:
full AND text AND search AND (elasticsearch OR lucene)
會將所有包括詞 full 、 text 和 search ,以及 elasticsearch 或 lucene 的文檔作爲結果集。
這個過程簡單且快速,它將所有可能不匹配的文檔排除在外。
詞頻/逆向文檔頻率(TF/IDF)
當匹配到一組文檔後,需要根據相關度排序這些文檔,不是所有的文檔都包含所有詞,有些詞比其他的詞更重要。一個文檔的相關度評分部分取決於每個查詢詞在文檔中的 權重 。
詞的權重由三個因素決定,在 什麼是相關 中已經有所介紹,有興趣可以瞭解下面的公式,但並不要求記住。
詞頻
詞在文檔中出現的頻度是多少?頻度越高,權重 越高 。 5 次提到同一詞的字段比只提到 1 次的更相關。詞頻的計算方式如下:
tf(t in d) = √frequency
詞 t 在文檔 d 的詞頻( tf )是該詞在文檔中出現次數的平方根。
如果不在意詞在某個字段中出現的頻次,而只在意是否出現過,則可以在字段映射中禁用詞頻統計:
PUT /my_index { "mappings": { "doc": { "properties": { "text": { "type": "string", "index_options": "docs" } } } } }
將參數 index_options 設置爲 docs 可以禁用詞頻統計及詞頻位置,這個映射的字段不會計算詞的出現次數,對於短語或近似查詢也不可用。要求精確查詢的 not_analyzed 字符串字段會默認使用該設置。
逆向文檔頻率
詞在集合所有文檔裏出現的頻率是多少?頻次越高,權重 越低 。常用詞如 and 或 the 對相關度貢獻很少,因爲它們在多數文檔中都會出現,一些不常見詞如 elastic 或 hippopotamus 可以幫助我們快速縮小範圍找到感興趣的文檔。逆向文檔頻率的計算公式如下:
idf(t) = 1 + log ( numDocs / (docFreq + 1))
詞 t 的逆向文檔頻率( idf )是:索引中文檔數量除以所有包含該詞的文檔數,然後求其對數。
字段長度歸一值
字段的長度是多少?字段越短,字段的權重 越高 。如果詞出現在類似標題 title 這樣的字段,要比它出現在內容 body 這樣的字段中的相關度更高。字段長度的歸一值公式如下:
norm(d) = 1 / √numTerms
字段長度歸一值( norm )是字段中詞數平方根的倒數。
字段長度的歸一值對全文搜索非常重要,許多其他字段不需要有歸一值。無論文檔是否包括這個字段,索引中每個文檔的每個 string 字段都大約佔用 1 個 byte 的空間。對於 not_analyzed 字符串字段的歸一值默認是禁用的,而對於 analyzed 字段也可以通過修改字段映射禁用歸一值:
PUT /my_index { "mappings": { "doc": { "properties": { "text": { "type": "string", "norms": { "enabled": false } } } } } }
這個字段不會將字段長度歸一值考慮在內,長字段和短字段會以相同長度計算評分。
對於有些應用場景如日誌,歸一值不是很有用,要關心的只是字段是否包含特殊的錯誤碼或者特定的瀏覽器唯一標識符。字段的長度對結果沒有影響,禁用歸一值可以節省大量內存空間。
結合使用
以下三個因素——詞頻(term frequency)、逆向文檔頻率(inverse document frequency)和字段長度歸一值(field-length norm)——是在索引時計算並存儲的。最後將它們結合在一起計算單個詞在特定文檔中的 權重 。
前面公式中提到的 文檔 實際上是指文檔裏的某個字段,每個字段都有它自己的倒排索引,因此字段的 TF/IDF 值就是文檔的 TF/IDF 值。
當用 explain 查看一個簡單的 term 查詢時(參見 explain ),可以發現與計算相關度評分的因子就是前面章節介紹的這些:
PUT /my_index/doc/1 { "text" : "quick brown fox" } GET /my_index/doc/_search?explain { "query": { "term": { "text": "fox" } } }
以上請求(簡化)的 explanation 解釋如下:
weight(text:fox in 0) [PerFieldSimilarity]: 0.15342641
result of:
fieldWeight in 0 0.15342641
product of:
tf(freq=1.0), with freq of 1: 1.0
idf(docFreq=1, maxDocs=1): 0.30685282
fieldNorm(doc=0): 0.5
- 詞 fox 在文檔的內部 Lucene doc ID 爲 0 ,字段是 text 裏的最終評分。
- 詞 fox 在該文檔 text 字段中只出現了一次。
- fox 在所有文檔 text 字段索引的逆向文檔頻率。
- 該字段的字段長度歸一值。
當然,查詢通常不止一個詞,所以需要一種合併多詞權重的方式——向量空間模型(vector space model)。
向量空間模型
向量空間模型(vector space model) 提供一種比較多詞查詢的方式,單個評分代表文檔與查詢的匹配程度,爲了做到這點,這個模型將文檔和查詢都以 向量(vectors) 的形式表示:
向量實際上就是包含多個數的一維數組,例如:
[1,2,5,22,3,8]
在向量空間模型裏,向量空間模型裏的每個數字都代表一個詞的 權重 ,與 詞頻/逆向文檔頻率(term frequency/inverse document frequency) 計算方式類似。
儘管 TF/IDF 是向量空間模型計算詞權重的默認方式,但不是唯一方式。Elasticsearch 還有其他模型如 Okapi-BM25 。TF/IDF 是默認的因爲它是個經檢驗過的簡單又高效的算法,可以提供高質量的搜索結果。
設想如果查詢 “happy hippopotamus” ,常見詞 happy 的權重較低,不常見詞 hippopotamus 權重較高,假設 happy 的權重是 2 , hippopotamus 的權重是 5 ,可以將這個二維向量—— [2,5] ——在座標系下作條直線,線的起點是 (0,0) 終點是 (2,5) ,如圖 Figure 27, “表示 “happy hippopotamus” 的二維查詢向量” 。
查詢向量繪點圖
Figure 27. 表示 “happy hippopotamus” 的二維查詢向量
現在,設想我們有三個文檔:
- I am happy in summer 。
- After Christmas I’m a hippopotamus 。
- The happy hippopotamus helped Harry 。
可以爲每個文檔都創建包括每個查詢詞—— happy 和 hippopotamus ——權重的向量,然後將這些向量置入同一個座標系中,如圖 Figure 28, ““happy hippopotamus” 查詢及文檔向量” :
- 文檔 1: (happy,____________) —— [2,0]
- 文檔 2: ( ___ ,hippopotamus) —— [0,5]
- 文檔 3: (happy,hippopotamus) —— [2,5]
查詢及文檔向量繪點圖
Figure 28. “happy hippopotamus” 查詢及文檔向量
向量之間是可以比較的,只要測量查詢向量和文檔向量之間的角度就可以得到每個文檔的相關度,文檔 1 與查詢之間的角度最大,所以相關度低;文檔 2 與查詢間的角度較小,所以更相關;文檔 3 與查詢的角度正好吻合,完全匹配。
在實際中,只有二維向量(兩個詞的查詢)可以在平面上表示,幸運的是, 線性代數 ——作爲數學中處理向量的一個分支——爲我們提供了計算兩個多維向量間角度工具,這意味着可以使用如上同樣的方式來解釋多個詞的查詢。
關於比較兩個向量的更多信息可以參考 餘弦近似度(cosine similarity)。
現在已經講完評分計算的基本理論,我們可以繼續瞭解 Lucene 是如何實現評分計算的。
權重提升
在 語句優先級(Prioritizing Clauses) 中,我們解釋過如何在搜索時使用 boost 參數讓一個查詢語句比其他語句更重要。例如:
GET /_search { "query": { "bool": { "should": [ { "match": { "title": { "query": "quick brown fox", "boost": 2 } } }, { "match": { "content": "quick brown fox" } } ] } } }
title 查詢語句的重要性是 content 查詢的 2 倍,因爲它的權重提升值爲 2 。
沒有設置 boost 的查詢語句的值爲 1 。
查詢時的權重提升 是可以用來影響相關度的主要工具,任意類型的查詢都能接受 boost 參數。將 boost 設置爲 2 ,並不代表最終的評分 _score 是原值的兩倍;實際的權重值會經過歸一化和一些其他內部優化過程。儘管如此,它確實想要表明一個提升值爲 2 的句子的重要性是提升值爲 1 語句的兩倍。
在實際應用中,無法通過簡單的公式得出某個特定查詢語句的 “正確” 權重提升值,只能通過不斷嘗試獲得。需要記住的是 boost 只是影響相關度評分的其中一個因子;它還需要與其他因子相互競爭。在前例中, title 字段相對 content 字段可能已經有一個 “缺省的” 權重提升值,這因爲在 字段長度歸一值 中,標題往往比相關內容要短,所以不要想當然的去盲目提升一些字段的權重。選擇權重,檢查結果,如此反覆。
改變嵌套結構修改相關度
Elasticsearch 的查詢表達式相當靈活,可以通過調整查詢結構中查詢語句的所處層次,從而或多或少改變其重要性,比如,設想下面這個查詢:
quick OR brown OR red OR fox
可以將所有詞都放在 bool 查詢的同一層中:
GET /_search { "query": { "bool": { "should": [ { "term": { "text": "quick" }}, { "term": { "text": "brown" }}, { "term": { "text": "red" }}, { "term": { "text": "fox" }} ] } } }
這個查詢可能最終給包含 quick 、 red 和 brown 的文檔評分與包含 quick 、 red 、 fox 文檔的評分相同,這裏 Red 和 brown 是同義詞,可能只需要保留其中一個,而我們真正要表達的意思是想做以下查詢:
quick OR (brown OR red) OR fox
根據標準的布爾邏輯,這與原始的查詢是完全一樣的,但是我們已經在 組合查詢(Combining Queries) 中看到, bool 查詢不關心文檔匹配的 程度 ,只關心是否能匹配。
上述查詢有個更好的方式:
GET /_search { "query": { "bool": { "should": [ { "term": { "text": "quick" }}, { "term": { "text": "fox" }}, { "bool": { "should": [ { "term": { "text": "brown" }}, { "term": { "text": "red" }} ] } } ] } } }
現在, red 和 brown 處於相互競爭的層次, quick 、 fox 以及 red OR brown 則是處於頂層且相互競爭的詞。
Not Quite Not (不完全的NOT)
在互聯網上搜索 “Apple”,返回的結果很可能是一個公司、水果和各種食譜。我們可以在 bool 查詢中用 must_not 語句來排除像 pie 、 tart 、 crumble 和 tree 這樣的詞,從而將查詢結果的範圍縮小至只返回與 “Apple” (蘋果)公司相關的結果:
GET /_search { "query": { "bool": { "must": { "match": { "text": "apple" } }, "must_not": { "match": { "text": "pie tart fruit crumble tree" } } } } }
但誰又敢保證在排除 tree 或 crumble 這種詞後,不會錯失一個與蘋果公司特別相關的文檔呢?有時, must_not 條件會過於嚴格。
權重提升查詢
boosting 查詢 恰恰能解決這個問題。它仍然允許我們將關於水果或甜點的結果包括到結果中,但是使它們降級——即降低它們原來可能應有的排名:
GET /_search { "query": { "boosting": { "positive": { "match": { "text": "apple" } }, "negative": { "match": { "text": "pie tart fruit crumble tree" } }, "negative_boost": 0.5 } } }
它接受 positive 和 negative 查詢。只有那些匹配 positive 查詢的文檔羅列出來,對於那些同時還匹配 negative 查詢的文檔將通過文檔的原始 _score 與 negative_boost 相乘的方式降級後的結果。
爲了達到效果, negative_boost 的值必須小於 1.0 。在這個示例中,所有包含負向詞的文檔評分 _score 都會減半。
忽略 TF/IDF
有時候我們根本不關心 TF/IDF ,只想知道一個詞是否在某個字段中出現過。可能搜索一個度假屋並希望它能儘可能有以下設施:
- WiFi
- Garden(花園)
- Pool(游泳池)
這個度假屋的文檔如下:
{ "description": "A delightful four-bedroomed house with ... " }
可以用簡單的 match 查詢進行匹配:
GET /_search { "query": { "match": { "description": "wifi garden pool" } } }
但這並不是真正的 全文搜索 ,此種情況下,TF/IDF 並無用處。我們既不關心 wifi 是否爲一個普通詞,也不關心它在文檔中出現是否頻繁,關心的只是它是否曾出現過。實際上,我們希望根據房屋不同設施的數量對其排名——設施越多越好。如果設施出現,則記 1 分,不出現記 0 分。
constant_score 查詢
在 constant_score 查詢中,它可以包含查詢或過濾,爲任意一個匹配的文檔指定評分 1 ,忽略 TF/IDF 信息:
GET /_search { "query": { "bool": { "should": [ { "constant_score": { "query": { "match": { "description": "wifi" }} }}, { "constant_score": { "query": { "match": { "description": "garden" }} }}, { "constant_score": { "query": { "match": { "description": "pool" }} }} ] } } }
或許不是所有的設施都同等重要——對某些用戶來說有些設施更有價值。如果最重要的設施是游泳池,那我們可以爲更重要的設施增加權重:
GET /_search { "query": { "bool": { "should": [ { "constant_score": { "query": { "match": { "description": "wifi" }} }}, { "constant_score": { "query": { "match": { "description": "garden" }} }}, { "constant_score": { "boost": 2 "query": { "match": { "description": "pool" }} }} ] } } }
pool 語句的權重提升值爲 2 ,而其他的語句爲 1 。
按受歡迎度提升權重
設想有個網站供用戶發佈博客並且可以讓他們爲自己喜歡的博客點贊,我們希望將更受歡迎的博客放在搜索結果列表中相對較上的位置,同時全文搜索的評分仍然作爲相關度的主要排序依據,可以簡單的通過存儲每個博客的點贊數來實現它:
PUT /blogposts/_doc/1 { "title": "About popularity", "content": "In this post we will talk about...", "votes": 6 }
在搜索時,可以將 function_score 查詢與 field_value_factor 結合使用,即將點贊數與全文相關度評分結合:
GET /blogposts/_search { "query": { "function_score": { "query": { "multi_match": { "query": "popularity", "fields": [ "title", "content" ] } }, "field_value_factor": { "field": "votes" } } } }
- function_score 查詢將主查詢和函數包括在內。
- 主查詢優先執行。
- field_value_factor 函數會被應用到每個與主 query 匹配的文檔。
- 每個文檔的 votes 字段都 必須 有值供 function_score 計算。如果 沒有 文檔的 votes 字段有值,那麼就 必須 使用 missing 屬性 提供的默認值來進行評分計算。
在前面示例中,每個文檔的最終評分 _score 都做了如下修改:
new_score = old_score * number_of_votes
然而這並不會帶來出人意料的好結果,全文評分 _score 通常處於 0 到 10 之間,如下圖 Figure 29, “受歡迎度的線性關係基於 _score 的原始值 2.0” 中,有 10 個讚的博客會掩蓋掉全文評分,而 0 個讚的博客的評分會被置爲 0 。
Linear popularity based on an original `_score` of `2.0`
Figure 29. 受歡迎度的線性關係基於 _score 的原始值 2.0
modifier
一種融入受歡迎度更好方式是用 modifier 平滑 votes 的值。換句話說,我們希望最開始的一些贊更重要,但是其重要性會隨着數字的增加而降低。 0 個贊與 1 個讚的區別應該比 10 個贊與 11 個讚的區別大很多。
對於上述情況,典型的 modifier 應用是使用 log1p 參數值,公式如下:
new_score = old_score * log(1 + number_of_votes)
log 對數函數使 votes 贊字段的評分曲線更平滑,如圖 Figure 30, “受歡迎度的對數關係基於 _score 的原始值 2.0” :
Logarithmic popularity based on an original `_score` of `2.0`
Figure 30. 受歡迎度的對數關係基於 _score 的原始值 2.0
帶 modifier 參數的請求如下:
GET /blogposts/post/_search { "query": { "function_score": { "query": { "multi_match": { "query": "popularity", "fields": [ "title", "content" ] } }, "field_value_factor": { "field": "votes", "modifier": "log1p" } } } }
modifier 爲 log1p 。
修飾語 modifier 的值可以爲: none (默認狀態)、 log 、 log1p 、 log2p 、 ln 、 ln1p 、 ln2p 、 square 、 sqrt 以及 reciprocal 。想要了解更多信息請參照: field_value_factor 文檔.
factor
可以通過將 votes 字段與 factor 的積來調節受歡迎程度效果的高低:
GET /blogposts/post/_search { "query": { "function_score": { "query": { "multi_match": { "query": "popularity", "fields": [ "title", "content" ] } }, "field_value_factor": { "field": "votes", "modifier": "log1p", "factor": 2 } } } }
雙倍效果。
添加了 factor 會使公式變成這樣:
new_score = old_score * log(1 + factor * number_of_votes)
factor 值大於 1 會提升效果, factor 值小於 1 會降低效果,
如圖 Figure 31, “受歡迎度的對數關係基於多個不同因子” 。
boost_mode
或許將全文評分與 field_value_factor 函數值乘積的效果仍然可能太大,我們可以通過參數 boost_mode 來控制函數與查詢評分 _score 合併後的結果,參數接受的值爲:
- multiply
- 評分 _score 與函數值的積(默認)
- sum
- 評分 _score 與函數值的和
- min
- 評分 _score 與函數值間的較小值
- max
- 評分 _score 與函數值間的較大值
- replace
- 函數值替代評分 _score
與使用乘積的方式相比,使用評分 _score 與函數值求和的方式可以弱化最終效果,特別是使用一個較小 factor 因子時:
GET /blogposts/post/_search { "query": { "function_score": { "query": { "multi_match": { "query": "popularity", "fields": [ "title", "content" ] } }, "field_value_factor": { "field": "votes", "modifier": "log1p", "factor": 0.1 }, "boost_mode": "sum" } } }
將函數計算結果值累加到評分 _score 。
之前請求的公式現在變成下面這樣(參見 Figure 32, “使用 sum 結合受歡迎程度” ):
new_score = old_score + log(1 + 0.1 * number_of_votes)
max_boost
最後,可以使用 max_boost 參數限制一個函數的最大效果:
GET /blogposts/post/_search { "query": { "function_score": { "query": { "multi_match": { "query": "popularity", "fields": [ "title", "content" ] } }, "field_value_factor": { "field": "votes", "modifier": "log1p", "factor": 0.1 }, "boost_mode": "sum", "max_boost": 1.5 } } }
無論 field_value_factor 函數的結果如何,最終結果都不會大於 1.5 。
越近越好
很多變量都可以影響用戶對於度假屋的選擇,也許用戶希望離市中心近點,但如果價格足夠便宜,也有可能選擇一個更遠的住處,也有可能反過來是正確的:願意爲最好的位置付更多的價錢。
如果我們添加過濾器排除所有市中心方圓 1 千米以外的度假屋,或排除所有每晚價格超過 £100 英鎊的,我們可能會將用戶願意考慮妥協的那些選擇排除在外。
function_score 查詢會提供一組 衰減函數(decay functions) ,讓我們有能力在兩個滑動標準,如地點和價格,之間權衡。
有三種衰減函數—— linear 、 exp 和 gauss (線性、指數和高斯函數),它們可以操作數值、時間以及經緯度地理座標點這樣的字段。所有三個函數都能接受以下參數:
- origin
- 中心點 或字段可能的最佳值,落在原點 origin 上的文檔評分 _score 爲滿分 1.0 。
- scale
- 衰減率,即一個文檔從原點 origin 下落時,評分 _score 改變的速度。(例如,每 £10 歐元或每 100 米)。
- decay
- 從原點 origin 衰減到 scale 所得的評分 _score ,默認值爲 0.5 。
- offset
- 以原點 origin 爲中心點,爲其設置一個非零的偏移量 offset 覆蓋一個範圍,而不只是單個原點。在範圍 -offset <= origin <= +offset 內的所有評分 _score 都是 1.0
- 這三個函數的唯一區別就是它們衰減曲線的形狀.
用圖來說明會更爲直觀(參見 Figure 33, “衰減函數曲線” )
圖 Figure 33, “衰減函數曲線” 中所有曲線的原點 origin (即中心點)的值都是 40 , offset 是 5 ,也就是在範圍 40 - 5 <= value <= 40 + 5 內的所有值都會被當作原點 origin 處理——所有這些點的評分都是滿分 1.0 。
在此範圍之外,評分開始衰減,衰減率由 scale 值(此例中的值爲 5 )和 衰減值 decay (此例中爲默認值 0.5 )共同決定。結果是所有三個曲線在 origin +/- (offset + scale) 處的評分都是 0.5 ,即點 30 和 50 處。
linear 、 exp 和 gauss (線性、指數和高斯)函數三者之間的區別在於範圍( origin +/- (offset + scale) )之外的曲線形狀:
- linear 線性函數是條直線,一旦直線與橫軸 0 相交,所有其他值的評分都是 0.0 。
- exp 指數函數是先劇烈衰減然後變緩。
- gauss 高斯函數是鐘形的——它的衰減速率是先緩慢,然後變快,最後又放緩。
選擇曲線的依據完全由期望評分 _score 的衰減速率來決定,即距原點 origin 的值。
回到我們的例子:用戶希望租一個離倫敦市中心近( { "lat": 51.50, "lon": 0.12} )且每晚不超過 £100 英鎊的度假屋,而且與距離相比,我們的用戶對價格更爲敏感,這樣查詢可以寫成:
GET /_search { "query": { "function_score": { "functions": [ { "gauss": { "location": { "origin": { "lat": 51.5, "lon": 0.12 }, "offset": "2km", "scale": "3km" } } }, { "gauss": { "price": { "origin": "50", "offset": "50", "scale": "20" } }, "weight": 2 } ] } } }
- location 字段以地理座標點 geo_point 映射。
- price 字段是數值。
- origin 爲什麼是 50 而不是 100 。
- price 語句是 location 語句權重的兩倍。
location 語句可以簡單理解爲:
- 以倫敦市中作爲原點 origin 。
- 所有距原點 origin 2km 範圍內的位置的評分是 1.0 。
- 距中心 5km ( offset + scale )的位置的評分是 0.5 。
price
語句使用了一個小技巧:用戶希望選擇 £100 英鎊以下的度假屋,但是例子中的原點被設置成 £50 英鎊,價格不能爲負,但肯定是越低越好,所以 £0 到 £100 英鎊內的所有價格都認爲是比較好的。如果我們將原點 origin
被設置成 £100 英鎊,那麼低於 £100 英鎊的度假屋的評分會變低,與其這樣不如將原點 origin
和偏移量 offset
同時設置成 £50 英鎊,這樣就能使只有在價格高於 £100 英鎊( origin + offset
)時評分纔會變低。
可插拔的相似度算法
在進一步討論相關度和評分之前,我們會以一個更高級的話題結束本章節的內容:可插拔的相似度算法(Pluggable Similarity Algorithms)。 Elasticsearch 將 實用評分算法 作爲默認相似度算法,它也能夠支持其他的一些算法,這些算法可以參考 相似度模塊 文檔。
Okapi BM25算法
能與 TF/IDF 和向量空間模型媲美的就是 Okapi BM25 ,它被認爲是 當今最先進的 排序函數。 BM25 源自 概率相關模型(probabilistic relevance model) ,而不是向量空間模型,但這個算法也和 Lucene 的實用評分函數有很多共通之處。
BM25 同樣使用詞頻、逆向文檔頻率以及字段長歸一化,但是每個因子的定義都有細微區別。與其詳細解釋 BM25 公式,倒不如將關注點放在 BM25 所能帶來的實際好處上。
詞頻飽和度
TF/IDF 和 BM25 同樣使用 逆向文檔頻率 來區分普通詞(不重要)和非普通詞(重要),同樣認爲(參見 詞頻 )文檔裏的某個詞出現次數越頻繁,文檔與這個詞就越相關。
不幸的是,普通詞隨處可見,實際上一個普通詞在同一個文檔中大量出現的作用會由於該詞在 所有 文檔中的大量出現而被抵消掉。
曾經有個時期,將 最 普通的詞(或 停用詞 ,參見 停用詞)從索引中移除被認爲是一種標準實踐,TF/IDF 正是在這種背景下誕生的。TF/IDF 沒有考慮詞頻上限的問題,因爲高頻停用詞已經被移除了。
Elasticsearch 的 standard 標準分析器( string 字段默認使用)不會移除停用詞,因爲儘管這些詞的重要性很低,但也不是毫無用處。這導致:在一個相當長的文檔中,像 the 和 and 這樣詞出現的數量會高得離譜,以致它們的權重被人爲放大。
另一方面,BM25 有一個上限,文檔裏出現 5 到 10 次的詞會比那些只出現一兩次的對相關度有着顯著影響。但是如圖 TF/IDF 與 BM25 的詞頻飽和度 所見,文檔中出現 20 次的詞幾乎與那些出現上千次的詞有着相同的影響。
這就是 非線性詞頻飽和度(nonlinear term-frequency saturation) 。
Figure 34. TF/IDF 與 BM25 的詞頻飽和度
字段長度歸一化(Field-length normalization)
在 字段長歸一化 中,我們提到過 Lucene 會認爲較短字段比較長字段更重要:字段某個詞的頻度所帶來的重要性會被這個字段長度抵消,但是實際的評分函數會將所有字段以同等方式對待。它認爲所有較短的 title 字段比所有較長的 body 字段更重要。
BM25 當然也認爲較短字段應該有更多的權重,但是它會分別考慮每個字段內容的平均長度,這樣就能區分短 title 字段和 長 title 字段。
在 查詢時權重提升 中,已經說過 title 字段因爲其長度比 body 字段 自然 有更高的權重提升值。由於字段長度的差異只能應用於單字段,這種自然的權重提升會在使用 BM25 時消失。