深入理解ElasticSearch(六)排序與相關性
排序與相關性
預設情況下,返回的結果是按照 相關性 進行排序的——最相關的文件排在最前。 在本章的後面部分,我們會解釋 相關性 意味著什麼以及它是如何計算的, 不過讓我們首先看看 sort 引數以及如何使用它。
1、排序
為了按照相關性來排序,需要將相關性表示為一個數值。在 Elasticsearch 中, 相關性得分 由一個浮點數進行表示,並在搜尋結果中通過 _score 引數返回, 預設排序是 _score 降序。
有時,相關性評分對你來說並沒有意義。例如,下面的查詢返回所有 user_id 欄位包含 1 的結果:
GET /_search { "query" : { "bool" : { "filter" : { "term" : { "user_id" : 1 } } } } }
這裡沒有一個有意義的分數:因為我們使用的是 filter (過濾),這表明我們只希望獲取匹配 user_id: 1 的文件,並沒有試圖確定這些文件的相關性。 實際上文件將按照隨機順序返回,並且每個文件都會評為零分。
1.1、按照欄位的值排序
在這個案例中,通過時間來對 tweets 進行排序是有意義的,最新的 tweets 排在最前。 我們可以使用 sort 引數進行實現:
GET /_search { "query" : { "bool" : { "filter" : { "term" : { "user_id" : 1 }} } }, "sort": { "date": { "order": "desc" }} }
你會注意到結果中的兩個不同點:
"hits" : { "total" : 6, "max_score" : null, "hits" : [ { "_index" : "us", "_type" : "tweet", "_id" : "14", "_score" : null, "_source" : { "date": "2014-09-24", ... }, "sort" : [ 1411516800000 ] }, ... }
_score 不被計算, 因為它並沒有用於排序。
date 欄位的值表示為自 epoch (January 1, 1970 00:00:00 UTC)以來的毫秒數,通過 sort 欄位的值進行返回。
首先我們在每個結果中有一個新的名為 sort 的元素,它包含了我們用於排序的值。 在這個案例中,我們按照 date 進行排序,在內部被索引為 自 epoch 以來的毫秒數 。 long 型別數 1411516800000 等價於日期字串 2014-09-24 00:00:00 UTC 。
其次 _score 和 max_score 欄位都是 null 。 計算 _score 的花銷巨大,通常僅用於排序; 我們並不根據相關性排序,所以記錄 _score 是沒有意義的。如果無論如何你都要計算 _score , 你可以將 track_scores 引數設定為 true 。
1.2、多級排序
假定我們想要結合使用 date 和 _score 進行查詢,並且匹配的結果首先按照日期排序,然後按照相關性排序:
GET /_search
{
"query" : {
"bool" : {
"must": { "match": { "tweet": "manage text search" }},
"filter" : { "term" : { "user_id" : 2 }}
}
},
"sort": [
{ "date": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
}
排序條件的順序是很重要的。結果首先按第一個條件排序,僅當結果集的第一個 sort 值完全相同時才會按照第二個條件進行排序,以此類推。
多級排序並不一定包含 _score 。你可以根據一些不同的欄位進行排序, 如地理距離或是指令碼計算的特定值。
1.3、欄位多值的排序
一種情形是欄位有多個值的排序, 需要記住這些值並沒有固有的順序;一個多值的欄位僅僅是多個值的包裝,這時應該選擇哪個進行排序呢?
對於數字或日期,你可以將多值欄位減為單值,這可以通過使用 min 、 max 、 avg 或是 sum 排序模式 。 例如你可以按照每個 date 欄位中的最早日期進行排序,通過以下方法:
"sort": {
"dates": {
"order": "asc",
"mode": "min"
}
}
2、字串排序與多欄位
被解析的字串欄位也是多值欄位, 但是很少會按照你想要的方式進行排序。如果你想分析一個字串,如 fine old art , 這包含 3 項。我們很可能想要按第一項的字母排序,然後按第二項的字母排序,諸如此類,但是 Elasticsearch 在排序過程中沒有這樣的資訊。
你可以使用 min 和 max 排序模式(預設是 min ),但是這會導致排序以 art 或是 old ,任何一個都不是所希望的。
為了以字串欄位進行排序,這個欄位應僅包含一項: 整個 not_analyzed 字串。 但是我們仍需要 analyzed 欄位,這樣才能以全文進行查詢
一個簡單的方法是用兩種方式對同一個字串進行索引,這將在文件中包括兩個欄位: analyzed 用於搜尋, not_analyzed 用於排序
但是儲存相同的字串兩次在 _source 欄位是浪費空間的。 我們真正想要做的是傳遞一個 單欄位 但是卻用兩種方式索引它。所有的 _core_field 型別 (strings, numbers, Booleans, dates) 接收一個 fields 引數
該引數允許你轉化一個簡單的對映如:
"tweet": {
"type": "string",
"analyzer": "english"
}
為一個多欄位對映如:
"tweet": {
"type": "string",
"analyzer": "english",
"fields": {
"raw": {
"type": "string",
"index": "not_analyzed"
}
}
}
tweet 主欄位與之前的一樣: 是一個 analyzed 全文欄位。
新的 tweet.raw 子欄位是 not_analyzed.
現在,至少只要我們重新索引了我們的資料,使用 tweet 欄位用於搜尋,tweet.raw 欄位用於排序:
GET /_search
{
"query": {
"match": {
"tweet": "elasticsearch"
}
},
"sort": "tweet.raw"
}
3、什麼是相關性?
我們曾經講過,預設情況下,返回結果是按相關性倒序排列的。 但是什麼是相關性? 相關性如何計算?
每個文件都有相關性評分,用一個正浮點數字段 _score 來表示 。 _score 的評分越高,相關性越高。
查詢語句會為每個文件生成一個 _score 欄位。評分的計算方式取決於查詢型別 不同的查詢語句用於不同的目的: fuzzy 查詢會計算與關鍵詞的拼寫相似程度,terms 查詢會計算 找到的內容與關鍵片語成部分匹配的百分比,但是通常我們說的 relevance 是我們用來計算全文字欄位的值相對於全文字檢索詞相似程度的演算法。
Elasticsearch 的相似度演算法 被定義為檢索詞頻率/反向文件頻率, TF/IDF ,包括以下內容:
- 檢索詞頻率 檢索詞在該欄位出現的頻率?出現頻率越高,相關性也越高。 欄位中出現過 5 次要比只出現過 1 次的相關性高。
- 反向文件頻率 每個檢索詞在索引中出現的頻率?頻率越高,相關性越低。檢索詞出現在多數文件中會比出現在少數文件中的權重更低。
- 欄位長度準則 欄位的長度是多少?長度越長,相關性越低。 檢索詞出現在一個短的 title 要比同樣的詞出現在一個長的 content 欄位權重更大。
單個查詢可以聯合使用 TF/IDF 和其他方式,比如短語查詢中檢索詞的距離或模糊查詢裡的檢索詞相似度。
相關性並不只是全文字檢索的專利。也適用於 yes|no 的子句,匹配的子句越多,相關性評分越高。
如果多條查詢子句被合併為一條複合查詢語句 ,比如 bool 查詢,則每個查詢子句計算得出的評分會被合併到總的相關性評分中。
3.1、理解評分標準
當除錯一條複雜的查詢語句時, 想要理解 _score 究竟是如何計算是比較困難的。Elasticsearch 在 每個查詢語句中都有一個 explain 引數,將 explain 設為 true 就可以得到更詳細的資訊。
GET /_search?explain
{
"query" : { "match" : { "tweet" : "honeymoon" }}
}
explain 引數可以讓返回結果新增一個 _score 評分的得來依據。
首先,我們看一下普通查詢返回的元資料:
{
"_index" : "us",
"_type" : "tweet",
"_id" : "12",
"_score" : 0.076713204,
"_source" : { ... trimmed ... },
這裡加入了該文件來自於哪個節點哪個分片上的資訊,這對我們是比較有幫助的,因為詞頻率和 文件頻率是在每個分片中計算出來的,而不是每個索引中:
"_shard" : 1,
"_node" : "mzIVYCsqSWCG_M_ZffSs9Q",
然後它提供了 _explanation 。每個 入口都包含一個 description 、 value 、 details 欄位,它分別告訴你計算的型別、計算結果和任何我們需要的計算細節。
"_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,
}
]
}
]
}
第一部分是關於計算的總結。告訴了我們 honeymoon 在 tweet 欄位中的檢索詞頻率/反向文件頻率或 TF/IDF, (這裡的文件 0 是一個內部的 ID,跟我們沒有關係,可以忽略。)
然後它提供了權重是如何計算的細節:
檢索詞頻率:
檢索詞
honeymoon
在這個文件的tweet
欄位中的出現次數。
反向文件頻率:
檢索詞
honeymoon
在索引上所有文件的tweet
欄位中出現的次數。
欄位長度準則:
在這個文件中,
tweet
欄位內容的長度 – 內容越長,值越小。
複雜的查詢語句解釋也非常複雜,但是包含的內容與上面例子大致相同。 通過這段資訊我們可以瞭解搜尋結果是如何產生的。
3.2、理解文件是如何被匹配到的
當 explain 選項加到某一文件上時, explain api 會幫助你理解為何這個文件會被匹配,更重要的是,一個文件為何沒有被匹配。
請求路徑為 /index/type/id/_explain ,如下所示:
GET /us/tweet/12/_explain
{
"query" : {
"bool" : {
"filter" : { "term" : { "user_id" : 2 }},
"must" : { "match" : { "tweet" : "honeymoon" }}
}
}
}
不只是我們之前看到的充分解釋 ,我們現在有了一個 description 元素,它將告訴我們:
"failure to match filter: cache(user_id:[2 TO 2])"
也就是說我們的 user_id 過濾子句使該文件不能匹配到。
4、Doc Values 介紹
本章的最後一個話題是關於 Elasticsearch 內部的一些執行情況。在這裡我們先不介紹新的知識點,所以我們應該意識到,Doc Values 是我們需要反覆提到的一個重要話題。
當你對一個欄位進行排序時,Elasticsearch 需要訪問每個匹配到的文件得到相關的值。倒排索引的檢索效能是非常快的,但是在欄位值排序時卻不是理想的結構。
- 在搜尋的時候,我們能通過搜尋關鍵詞快速得到結果集。
- 當排序的時候,我們需要倒排索引裡面某個欄位值的集合。換句話說,我們需要
倒置
倒排索引。
倒置
結構在其他系統中經常被稱作 列儲存
。實質上,它將所有單欄位的值儲存在單資料列中,這使得對其進行操作是十分高效的,例如排序。
在 Elasticsearch 中,doc values 就是一種列式儲存結構,預設情況下每個欄位的 doc values 都是啟用的,doc values 是在索引時建立的,當欄位索引時,Elasticsearch 為了能夠快速檢索,會把欄位的值加入倒排索引中,同時它也會儲存該欄位的 doc values。
Elasticsearch 中的 doc vaules 常被應用到以下場景:
- 對一個欄位進行排序
- 對一個欄位進行聚合
- 某些過濾,比如地理位置過濾
- 某些與欄位相關的指令碼計算
因為文件值被序列化到磁碟,我們可以依靠作業系統的幫助來快速訪問。當 working set 遠小於節點的可用記憶體,系統會自動將所有的文件值儲存在記憶體中,使得其讀寫十分高速; 當其遠大於可用記憶體,作業系統會自動把 doc values 載入到系統的頁快取中,從而避免了 jvm 堆記憶體溢位異常。