深入理解ElasticSearch(六)排序與相關(guān)性
排序與相關(guān)性
默認(rèn)情況下,返回的結(jié)果是按照 相關(guān)性 進(jìn)行排序的——最相關(guān)的文檔排在最前。在本章的后面部分,我們會(huì)解釋 相關(guān)性 意味著什么以及它是如何計(jì)算的, 不過讓我們首先看看 sort 參數(shù)以及如何使用它。
1、排序
為了按照相關(guān)性來排序,需要將相關(guān)性表示為一個(gè)數(shù)值。在 Elasticsearch 中, 相關(guān)性得分 由一個(gè)浮點(diǎn)數(shù)進(jìn)行表示,并在搜索結(jié)果中通過 _score 參數(shù)返回, 默認(rèn)排序是 _score 降序。
有時(shí),相關(guān)性評(píng)分對(duì)你來說并沒有意義。例如,下面的查詢返回所有 user_id 字段包含 1 的結(jié)果:
GET /_search
{
"query" : {
"bool" : {
"filter" : {
"term" : {
"user_id" : 1
}
}
}
}
}
這里沒有一個(gè)有意義的分?jǐn)?shù):因?yàn)槲覀兪褂玫氖?filter (過濾),這表明我們只希望獲取匹配 user_id: 1 的文檔,并沒有試圖確定這些文檔的相關(guān)性。實(shí)際上文檔將按照隨機(jī)順序返回,并且每個(gè)文檔都會(huì)評(píng)為零分。
1.1、按照字段的值排序
在這個(gè)案例中,通過時(shí)間來對(duì) tweets 進(jìn)行排序是有意義的,最新的 tweets 排在最前。我們可以使用 sort 參數(shù)進(jìn)行實(shí)現(xiàn):
GET /_search
{
"query" : {
"bool" : {
"filter" : { "term" : { "user_id" : 1 }}
}
},
"sort": { "date": { "order": "desc" }}
}
你會(huì)注意到結(jié)果中的兩個(gè)不同點(diǎn):
"hits" : {
"total" : 6,
"max_score" : null,
"hits" : [ {
"_index" : "us",
"_type" : "tweet",
"_id" : "14",
"_score" : null,
"_source" : {
"date": "2014-09-24",
...
},
"sort" : [ 1411516800000 ]
},
...
}
_score 不被計(jì)算, 因?yàn)樗]有用于排序。
date 字段的值表示為自 epoch (January 1, 1970 00:00:00 UTC)以來的毫秒數(shù),通過 sort 字段的值進(jìn)行返回。
首先我們?cè)诿總€(gè)結(jié)果中有一個(gè)新的名為 sort 的元素,它包含了我們用于排序的值。在這個(gè)案例中,我們按照 date 進(jìn)行排序,在內(nèi)部被索引為 自 epoch 以來的毫秒數(shù) 。long 類型數(shù) 1411516800000 等價(jià)于日期字符串 2014-09-24 00:00:00 UTC 。
其次 _score 和 max_score 字段都是 null 。計(jì)算 _score 的花銷巨大,通常僅用于排序;我們并不根據(jù)相關(guān)性排序,所以記錄 _score 是沒有意義的。如果無論如何你都要計(jì)算 _score , 你可以將 track_scores 參數(shù)設(shè)置為 true 。
1.2、多級(jí)排序
假定我們想要結(jié)合使用 date 和 _score 進(jìn)行查詢,并且匹配的結(jié)果首先按照日期排序,然后按照相關(guān)性排序:
GET /_search
{
"query" : {
"bool" : {
"must": { "match": { "tweet": "manage text search" }},
"filter" : { "term" : { "user_id" : 2 }}
}
},
"sort": [
{ "date": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
}
排序條件的順序是很重要的。結(jié)果首先按第一個(gè)條件排序,僅當(dāng)結(jié)果集的第一個(gè) sort 值完全相同時(shí)才會(huì)按照第二個(gè)條件進(jìn)行排序,以此類推。
多級(jí)排序并不一定包含 _score 。你可以根據(jù)一些不同的字段進(jìn)行排序, 如地理距離或是腳本計(jì)算的特定值。
1.3、字段多值的排序
一種情形是字段有多個(gè)值的排序, 需要記住這些值并沒有固有的順序;一個(gè)多值的字段僅僅是多個(gè)值的包裝,這時(shí)應(yīng)該選擇哪個(gè)進(jìn)行排序呢?
對(duì)于數(shù)字或日期,你可以將多值字段減為單值,這可以通過使用 min 、 max 、 avg 或是 sum 排序模式 。例如你可以按照每個(gè) date 字段中的最早日期進(jìn)行排序,通過以下方法:
"sort": {
"dates": {
"order": "asc",
"mode": "min"
}
}
2、字符串排序與多字段
被解析的字符串字段也是多值字段, 但是很少會(huì)按照你想要的方式進(jìn)行排序。如果你想分析一個(gè)字符串,如 fine old art , 這包含 3 項(xiàng)。我們很可能想要按第一項(xiàng)的字母排序,然后按第二項(xiàng)的字母排序,諸如此類,但是 Elasticsearch 在排序過程中沒有這樣的信息。
你可以使用 min 和 max 排序模式(默認(rèn)是 min ),但是這會(huì)導(dǎo)致排序以 art 或是 old ,任何一個(gè)都不是所希望的。
為了以字符串字段進(jìn)行排序,這個(gè)字段應(yīng)僅包含一項(xiàng):整個(gè) not_analyzed 字符串。但是我們?nèi)孕枰?analyzed 字段,這樣才能以全文進(jìn)行查詢
一個(gè)簡(jiǎn)單的方法是用兩種方式對(duì)同一個(gè)字符串進(jìn)行索引,這將在文檔中包括兩個(gè)字段:analyzed 用于搜索, not_analyzed 用于排序
但是保存相同的字符串兩次在 _source 字段是浪費(fèi)空間的。我們真正想要做的是傳遞一個(gè) 單字段 但是卻用兩種方式索引它。所有的 _core_field 類型 (strings, numbers, Booleans, dates) 接收一個(gè) fields 參數(shù)
該參數(shù)允許你轉(zhuǎn)化一個(gè)簡(jiǎn)單的映射如:
"tweet": {
"type": "string",
"analyzer": "english"
}
為一個(gè)多字段映射如:
"tweet": {
"type": "string",
"analyzer": "english",
"fields": {
"raw": {
"type": "string",
"index": "not_analyzed"
}
}
}
tweet 主字段與之前的一樣: 是一個(gè) analyzed 全文字段。
新的 tweet.raw 子字段是 not_analyzed.
現(xiàn)在,至少只要我們重新索引了我們的數(shù)據(jù),使用 tweet 字段用于搜索,tweet.raw 字段用于排序:
GET /_search
{
"query": {
"match": {
"tweet": "elasticsearch"
}
},
"sort": "tweet.raw"
}
3、什么是相關(guān)性?
我們?cè)?jīng)講過,默認(rèn)情況下,返回結(jié)果是按相關(guān)性倒序排列的。但是什么是相關(guān)性?相關(guān)性如何計(jì)算?
每個(gè)文檔都有相關(guān)性評(píng)分,用一個(gè)正浮點(diǎn)數(shù)字段 _score 來表示 。_score 的評(píng)分越高,相關(guān)性越高。
查詢語句會(huì)為每個(gè)文檔生成一個(gè) _score 字段。評(píng)分的計(jì)算方式取決于查詢類型 不同的查詢語句用于不同的目的:fuzzy 查詢會(huì)計(jì)算與關(guān)鍵詞的拼寫相似程度,terms 查詢會(huì)計(jì)算 找到的內(nèi)容與關(guān)鍵詞組成部分匹配的百分比,但是通常我們說的 relevance 是我們用來計(jì)算全文本字段的值相對(duì)于全文本檢索詞相似程度的算法。
Elasticsearch 的相似度算法 被定義為檢索詞頻率/反向文檔頻率, TF/IDF ,包括以下內(nèi)容:
檢索詞頻率
檢索詞在該字段出現(xiàn)的頻率?出現(xiàn)頻率越高,相關(guān)性也越高。字段中出現(xiàn)過 5 次要比只出現(xiàn)過 1 次的相關(guān)性高。反向文檔頻率
每個(gè)檢索詞在索引中出現(xiàn)的頻率?頻率越高,相關(guān)性越低。檢索詞出現(xiàn)在多數(shù)文檔中會(huì)比出現(xiàn)在少數(shù)文檔中的權(quán)重更低。字段長(zhǎng)度準(zhǔn)則
字段的長(zhǎng)度是多少?長(zhǎng)度越長(zhǎng),相關(guān)性越低。檢索詞出現(xiàn)在一個(gè)短的 title 要比同樣的詞出現(xiàn)在一個(gè)長(zhǎng)的 content 字段權(quán)重更大。
單個(gè)查詢可以聯(lián)合使用 TF/IDF 和其他方式,比如短語查詢中檢索詞的距離或模糊查詢里的檢索詞相似度。
相關(guān)性并不只是全文本檢索的專利。也適用于 yes|no 的子句,匹配的子句越多,相關(guān)性評(píng)分越高。
如果多條查詢子句被合并為一條復(fù)合查詢語句 ,比如 bool 查詢,則每個(gè)查詢子句計(jì)算得出的評(píng)分會(huì)被合并到總的相關(guān)性評(píng)分中。
3.1、理解評(píng)分標(biāo)準(zhǔn)
當(dāng)調(diào)試一條復(fù)雜的查詢語句時(shí), 想要理解 _score 究竟是如何計(jì)算是比較困難的。Elasticsearch 在 每個(gè)查詢語句中都有一個(gè) explain 參數(shù),將 explain 設(shè)為 true 就可以得到更詳細(xì)的信息。
GET /_search?explain
{
"query" : { "match" : { "tweet" : "honeymoon" }}
}
explain 參數(shù)可以讓返回結(jié)果添加一個(gè) _score 評(píng)分的得來依據(jù)。
首先,我們看一下普通查詢返回的元數(shù)據(jù):
{
"_index" : "us",
"_type" : "tweet",
"_id" : "12",
"_score" : 0.076713204,
"_source" : { ... trimmed ... },
這里加入了該文檔來自于哪個(gè)節(jié)點(diǎn)哪個(gè)分片上的信息,這對(duì)我們是比較有幫助的,因?yàn)樵~頻率和 文檔頻率是在每個(gè)分片中計(jì)算出來的,而不是每個(gè)索引中:
"_shard" : 1,
"_node" : "mzIVYCsqSWCG_M_ZffSs9Q",
然后它提供了 _explanation 。每個(gè) 入口都包含一個(gè) description 、 value 、 details 字段,它分別告訴你計(jì)算的類型、計(jì)算結(jié)果和任何我們需要的計(jì)算細(xì)節(jié)。
"_explanation": {
"description": "weight(tweet:honeymoon in 0)
[PerFieldSimilarity], result of:",
"value": 0.076713204,
"details": [
{
"description": "fieldWeight in 0, product of:",
"value": 0.076713204,
"details": [
{
"description": "tf(freq=1.0), with freq of:",
"value": 1,
"details": [
{
"description": "termFreq=1.0",
"value": 1
}
]
},
{
"description": "idf(docFreq=1, maxDocs=1)",
"value": 0.30685282
},
{
"description": "fieldNorm(doc=0)",
"value": 0.25,
}
]
}
]
}
第一部分是關(guān)于計(jì)算的總結(jié)。告訴了我們 honeymoon 在 tweet 字段中的檢索詞頻率/反向文檔頻率或 TF/IDF, (這里的文檔 0 是一個(gè)內(nèi)部的 ID,跟我們沒有關(guān)系,可以忽略。)
然后它提供了權(quán)重是如何計(jì)算的細(xì)節(jié):
檢索詞頻率:
檢索詞
honeymoon在這個(gè)文檔的tweet字段中的出現(xiàn)次數(shù)。
反向文檔頻率:
檢索詞
honeymoon在索引上所有文檔的tweet字段中出現(xiàn)的次數(shù)。
字段長(zhǎng)度準(zhǔn)則:
在這個(gè)文檔中,
tweet字段內(nèi)容的長(zhǎng)度 – 內(nèi)容越長(zhǎng),值越小。
復(fù)雜的查詢語句解釋也非常復(fù)雜,但是包含的內(nèi)容與上面例子大致相同。通過這段信息我們可以了解搜索結(jié)果是如何產(chǎn)生的。
3.2、理解文檔是如何被匹配到的
當(dāng) explain 選項(xiàng)加到某一文檔上時(shí), explain api 會(huì)幫助你理解為何這個(gè)文檔會(huì)被匹配,更重要的是,一個(gè)文檔為何沒有被匹配。
請(qǐng)求路徑為 /index/type/id/_explain ,如下所示:
GET /us/tweet/12/_explain
{
"query" : {
"bool" : {
"filter" : { "term" : { "user_id" : 2 }},
"must" : { "match" : { "tweet" : "honeymoon" }}
}
}
}
不只是我們之前看到的充分解釋 ,我們現(xiàn)在有了一個(gè) description 元素,它將告訴我們:
"failure to match filter: cache(user_id:[2 TO 2])"也就是說我們的 user_id 過濾子句使該文檔不能匹配到。
4、Doc Values 介紹
本章的最后一個(gè)話題是關(guān)于 Elasticsearch 內(nèi)部的一些運(yùn)行情況。在這里我們先不介紹新的知識(shí)點(diǎn),所以我們應(yīng)該意識(shí)到,Doc Values 是我們需要反復(fù)提到的一個(gè)重要話題。
當(dāng)你對(duì)一個(gè)字段進(jìn)行排序時(shí),Elasticsearch 需要訪問每個(gè)匹配到的文檔得到相關(guān)的值。倒排索引的檢索性能是非??斓模窃谧侄沃蹬判驎r(shí)卻不是理想的結(jié)構(gòu)。
在搜索的時(shí)候,我們能通過搜索關(guān)鍵詞快速得到結(jié)果集。
當(dāng)排序的時(shí)候,我們需要倒排索引里面某個(gè)字段值的集合。換句話說,我們需要
倒置倒排索引。
倒置 結(jié)構(gòu)在其他系統(tǒng)中經(jīng)常被稱作 列存儲(chǔ) 。實(shí)質(zhì)上,它將所有單字段的值存儲(chǔ)在單數(shù)據(jù)列中,這使得對(duì)其進(jìn)行操作是十分高效的,例如排序。
在 Elasticsearch 中,doc values 就是一種列式存儲(chǔ)結(jié)構(gòu),默認(rèn)情況下每個(gè)字段的 doc values 都是激活的,doc values 是在索引時(shí)創(chuàng)建的,當(dāng)字段索引時(shí),Elasticsearch 為了能夠快速檢索,會(huì)把字段的值加入倒排索引中,同時(shí)它也會(huì)存儲(chǔ)該字段的 doc values。
Elasticsearch 中的 doc vaules 常被應(yīng)用到以下場(chǎng)景:
對(duì)一個(gè)字段進(jìn)行排序
對(duì)一個(gè)字段進(jìn)行聚合
某些過濾,比如地理位置過濾
某些與字段相關(guān)的腳本計(jì)算
因?yàn)槲臋n值被序列化到磁盤,我們可以依靠操作系統(tǒng)的幫助來快速訪問。當(dāng) working set 遠(yuǎn)小于節(jié)點(diǎn)的可用內(nèi)存,系統(tǒng)會(huì)自動(dòng)將所有的文檔值保存在內(nèi)存中,使得其讀寫十分高速;當(dāng)其遠(yuǎn)大于可用內(nèi)存,操作系統(tǒng)會(huì)自動(dòng)把 doc values 加載到系統(tǒng)的頁緩存中,從而避免了 jvm 堆內(nèi)存溢出異常。
