前言
使用ES時,對於查詢出的文檔無疑會有文檔相似度之別。而理想的排序是和查詢條件相關性越高排序越靠前,而這個排序的依據就是_score
。本文就是詳解_score
有關的信息,希望能對排序評分的理解有所幫助。
正文
一個文檔對於搜索的評分一定是有據可依的,而接下來就要介紹根據哪些參數查找匹配的文檔以及評分的標準。
理論
此段轉自官方文檔—– 相關度評分背後的理論
詞頻/逆向文檔頻率(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 } } } } } }
對於有些應用場景如日誌,歸一值不是很有用,要關心的只是字段是否包含特殊的錯誤碼或者特定的瀏覽器唯一標識符。字段的長度對結果沒有影響,禁用歸一值可以節省大量內存空間。
文檔評分計算
轉自官方文檔 Lucene的使用評分函數
評分計算公式
score(q,d) =
queryNorm(q) //歸一化因子
· coord(q,d) //協調因子
· ∑ (
tf(t in d) //詞頻
· idf(t)² //逆向文檔頻率
· t.getBoost() //權重
· norm(t,d) //字段長度歸一值
) (t in q)
下面簡要介紹公式中新提及的三個參數,具體信息可以點擊上方官方文檔原文:
queryNorm
查詢歸化因子:會被應用到每個文檔,不能被更改,總而言之,可以被忽略。coord
協調因子: 可以爲那些查詢詞包含度高的文檔提供獎勵,文檔裏出現的查詢詞越多,它越有機會成爲好的匹配結果。協調因子將評分與文檔裏匹配詞的數量相乘,然後除以查詢裏所有詞的數量,如果使用協調因子,評分會變成:
- 文檔裏有
fox
→ 評分:1.5 * 1 / 3
= 0.5 - 文檔裏有
quick fox
→ 評分:3.0 * 2 / 3
= 2.0 - 文檔裏有
quick brown fox
→ 評分:4.5 * 3 / 3
= 4.5
協調因子能使包含所有三個詞的文檔比只包含兩個詞的文檔評分要高出很多。
- 文檔裏有
Boost
權重:在查詢中設置關鍵字的權重可以靈活的找到更匹配的文檔。
實例測試
// 準備
/PUT {{host}}:{{port}}/demo
{
"mappings":{
"article":{
"properties":{
"content":{
"type":"text"
}
}
}
}
}
//導入數據
[
{
"content": "測試語句1"
},
{
"content": "測試語句2"
},
{
"content": "測試語句3,字段長度不同"
}
]
查詢
/POST {{host}}:{{port}}/demo/article/_search
{
"query":{
"match":{
"content":"測"
}
}
}
測試結果:
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 0.2824934,
"hits": [
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ90700f4t28Wzjdj",
"_score": 0.2824934,
"_source": {
"content": "測試語句2"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ71f00f4t28WzjZT",
"_score": 0.21247853,
"_source": {
"content": "測試語句1"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIRAEw00f4t28Wzjkd",
"_score": 0.1293895,
"_source": {
"content": "測試語句3,字段長度不同"
}
}
]
}
}
奇怪的是,按照語句1和語句2的分數居然不同!因爲他們兩個文檔的關鍵參數,詞頻,字段長度,逆向文檔頻率均相同,爲什麼算出來的分不同呢?
原因主要是因爲 每個分片會根據 該分片內的所有文檔計算一個本地 IDF 。而文檔落在不同的分片就會導致逆向文檔頻率不同,算出來的分數也不同。
參見官網 被破壞的相關度
當文檔數量比較大,分片分佈均勻後,這個問題基本不會影響很大。那麼在我們這個demo中使用添加 ?search_type=dfs_query_then_fetch
來查詢所有的idf
。
/POST {{host}}:{{port}}/demo/article/_search?search_type=dfs_query_then_fetch
{
"query":{
"match":{
"content":"測"
}
}
}
測試結果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 0.14899126,
"hits": [
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ71f00f4t28WzjZT",
"_score": 0.14899126,
"_source": {
"content": "測試語句1"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ90700f4t28Wzjdj",
"_score": 0.14899126,
"_source": {
"content": "測試語句2"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIRAEw00f4t28Wzjkd",
"_score": 0.087505676,
"_source": {
"content": "測試語句3,字段長度不同"
}
}
]
}
}
可以看到,評分如我們所想得,文檔1和2分數相同,而文檔3因爲長度更長,導致分數更低。
繼續測試查詢時權重的影響
/POST {{host}}:{{port}}/demo/article/_search?search_type=dfs_query_then_fetch
{
"query": {
"bool": {
"should": [
{
"match": {
"content": {
"query": "1",
"boost": 2
}
}
},
{
"match": {
"content": "2"
}
}
]
}
}
}
測試結果:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 2.1887734,
"hits": [
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ71f00f4t28WzjZT",
"_score": 2.1887734,
"_source": {
"content": "測試語句1"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ90700f4t28Wzjdj",
"_score": 1.0943867,
"_source": {
"content": "測試語句2"
}
}
]
}
}
可以看到,由於給予搜索關鍵字1
更高的權重,因此文檔1的分數比文檔2分數要高,具體細節可以通過?explain
查看。
其他更改評分的方法
由於其他幾個方法官網介紹的比較詳盡,所以這裏就不多做介紹,直接貼上官網鏈接。而使用腳本評分
,官網介紹有些細節不夠完善,因此在此多加介紹:
按受歡迎度提升權重
過濾集提升權重
隨機評分
越近越好
腳本評分
腳本評分主要應用在提供的評分滿足不了需求,需要通過腳本自定義評分標準。比如雖然提供了前綴分詞,但是前綴分詞後,返回匹配的結果評分都是1,無法進一步區分。而我們可以通過腳本在使用tf/idf
得出分數後,再加上前綴匹配後的額外分值,達到搜索和前綴匹配的目的。
那麼在增加一組數據
{
"content":"語句測試4"
}
繼續之前的查詢條件:
/POST {{host}}:{{port}}/demo/article/_search?search_type=dfs_query_then_fetch
{
"query":{
"match":{
"content":"測"
}
}
}
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 4,
"max_score": 0.11455677,
"hits": [
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ71f00f4t28WzjZT",
"_score": 0.11455677,
"_source": {
"content": "測試語句1"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIQ90700f4t28Wzjdj",
"_score": 0.11455677,
"_source": {
"content": "測試語句2"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIaVP000f4t28W0AmE",
"_score": 0.11455677,
"_source": {
"content": "語句測試4"
}
},
{
"_index": "demo",
"_type": "article",
"_id": "AWEIRAEw00f4t28Wzjkd",
"_score": 0.065936774,
"_source": {
"content": "測試語句3,字段長度不同"
}
}
]
}
}
從測試結果中看到,雖然語句4順序不同,但是根據評分算法,依舊還是同分。
如果想突出前綴匹配的效果呢?
/POST {{host}}:{{port}}/demo/article/_search?search_type=dfs_query_then_fetch
{
"query": {
"function_score": {
"query": {
"match": {
"content": "測"
}
},
"script_score": {
"script": {
"lang": "painless", // 1
"source": "if(doc['content'].value.startsWith(params.keyword))return 1; return 0;",
"params":{ // 2
"keyword":"測"
}
}
},
"boost_mode": "sum" / 3
}
}
}
雖然和官網的實例代碼有所不同,但是這個代碼在我的ES 5.6.0上能正常工作。
painless
是一種新支持的腳本語言,語言格式和java
十分類似。可以參考以下文檔:腳本參數
score_mode
計算functions中的分數形式,加減乘除,boost_mode
計算最外層的分數形式,加減乘除。所以最後總分是tf/idf
分數加上腳本得分。但是運行結果爆出異常:
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [content] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
主要原因是如果一個可搜索的字段,默認是不能被腳本引用的。如果強行打開,對性能消耗很大,因此不建議這種做法。
參考官方文檔 fielddata
PUT my_index/_mapping/my_type { "properties": { "my_field": { "type": "text", "fielddata": true } } }
所以建議重新定義索引映射
PUT my_index { "mappings": { "my_type": { "properties": { "my_field": { "type": "text", "fields": { "keyword": { "type": "keyword" } } } } } } }
再重新輸入搜索
/POST {{host}}:{{port}}/demo/article/_search?search_type=dfs_query_then_fetch { "query": { "function_score": { "query": { "match": { "content": "測" } }, "script_score": { "script": { "lang": "painless", "source": "if(doc['content.keyword'].value.startsWith(params.keyword))return 1; return 0;", //此處更改爲content.keyword "params":{ "keyword":"測" } } }, "boost_mode": "sum" } } }
查詢結果:
{ "took": 16, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 4, "max_score": 1.1145568, "hits": [ { "_index": "demo", "_type": "article", "_id": "AWEIiwUM00f4t28W0cS6", "_score": 1.1145568, "_source": { "content": "測試語句1" } }, { "_index": "demo", "_type": "article", "_id": "AWEIiy7900f4t28W0cc0", "_score": 1.1145568, "_source": { "content": "測試語句2" } }, { "_index": "demo", "_type": "article", "_id": "AWEIi1Bq00f4t28W0cjD", "_score": 1.0659368, "_source": { "content": "測試語句3,字段長度不同" } }, { "_index": "demo", "_type": "article", "_id": "AWEIi2uV00f4t28W0cpI", "_score": 0.11455677, "_source": { "content": "語句測試4" } } ] } }
可以看到,給
測
開頭的語句加了1分,腳本運行成功。
總結
通過以上文字,介紹了筆者在研究ES評分的過程和遇到的一些問題,主要還是參考官方文檔。所以希望研究所得對本人對讀者有所幫助。