基於Lucene查詢原理分析Elasticsearch的效能
前言
Elasticsearch是一個很火的分散式搜尋系統,提供了非常強大而且易用的查詢和分析能力,包括全文索引、模糊查詢、多條件組合查詢、地理位置查詢等等,而且具有一定的分析聚合能力。因為其查詢場景非常豐富,所以如果泛泛的分析其查詢效能是一個非常複雜的事情,而且除了場景之外,還有很多影響因素,包括機型、引數配置、叢集規模等等。本文主要是針對幾種主要的查詢場景,從查詢原理的角度分析這個場景下的查詢開銷,並給出一個大概的效能數字,供大家參考。
Lucene查詢原理
本節主要是一些Lucene的背景知識,瞭解這些知識的同學可以略過。
Lucene的資料結構和查詢原理
Elasticsearch的底層是Lucene,可以說Lucene的查詢效能就決定了Elasticsearch的查詢效能。關於Lucene的查詢原理大家可以參考以下這篇文章:
Lucene中最重要的就是它的幾種資料結構,這決定了資料是如何被檢索的,本文再簡單描述一下幾種資料結構:
-
FST:儲存term字典,可以在FST上實現單Term、Term範圍、Term字首和萬用字元查詢等。
-
倒排鏈:儲存了每個term對應的docId的列表,採用skipList的結構儲存,用於快速跳躍。
-
BKD-Tree:BKD-Tree是一種儲存多維空間點的資料結構,用於數值型別(包括空間點)的快速查詢。
-
DocValues:基於docId的列式儲存,由於列式儲存的特點,可以有效提升排序聚合的效能。
組合條件的結果合併
瞭解了Lucene的資料結構和基本查詢原理,我們知道:
-
對單個詞條進行查詢,Lucene會讀取該詞條的倒排鏈,倒排鏈中是一個有序的docId列表。
-
對字串範圍/字首/萬用字元查詢,Lucene會從FST中獲取到符合條件的所有Term,然後就可以根據這些Term再查詢倒排鏈,找到符合條件的doc。
-
對數字型別進行範圍查詢,Lucene會透過BKD-Tree找到符合條件的docId集合,但這個集合中的docId並非有序的。
現在的問題是,如果給一個組合查詢條件,Lucene怎麼對各個單條件的結果進行組合,得到最終結果。簡化的問題就是如何求兩個集合的交集和並集。
1. 對N個倒排鏈求交集
上面Lucene原理分析的文章中講過,N個倒排鏈求交集,可以採用skipList,有效的跳過無效的doc。
2. 對N個倒排鏈求並集
處理方式一:仍然保留多個有序列表,多個有序列表的隊首構成一個優先佇列(最小堆),這樣後續可以對整個並集進行iterator(堆頂的隊首出堆,佇列裡下一個docID入堆),也可以透過skipList的方式向後跳躍(各個子列表分別透過skipList跳)。這種方式適合倒排鏈數量比較少(N比較小)的場景。
處理方式二:倒排鏈如果比較多(N比較大),採用方式一就不夠划算,這時候可以直接把結果合併成一個有序的docID陣列。
處理方式三:方式二中,直接儲存原始的docID,如果docID非常多,很消耗記憶體,所以當doc數量超過一定值時(32位docID在BitSet中只需要一個bit,BitSet的大小取決於segments裡的doc總數,所以可以根據doc總數和當前doc數估算是否BitSet更加划算),會採用構造BitSet的方式,非常節約記憶體,而且BitSet可以非常高效的取交/並集。
3. BKD-Tree的結果怎麼跟其他結果合併
透過BKD-Tree查詢到的docID是無序的,所以要麼先轉成有序的docID陣列,或者構造BitSet,然後再與其他結果合併。
查詢順序最佳化
如果採用多個條件進行查詢,那麼先查詢代價比較小的,再從小結果集上進行迭代,會更優一些。Lucene中做了很多這方面的最佳化,在查詢前會先估算每個查詢的代價,再決定查詢順序。
結果排序
預設情況下,Lucene會按照Score排序,即算分後的分數值,如果指定了其他的Sort欄位,就會按照指定的欄位排序。那麼,排序會非常影響效能嗎?首先,排序並不會對所有命中的doc進行排序,而是構造一個堆,保證前(Offset+Size)個數的doc是有序的,所以排序的效能取決於(Size+Offset)和命中的文件數,另外就是讀取docValues的開銷。因為(Size+Offset)並不會太大,而且docValues的讀取效能很高,所以排序並不會非常的影響效能。
各場景查詢效能分析
上一節講了一些查詢相關的理論知識,那麼本節就是理論結合實踐,透過具體的一些測試數字來分析一下各個場景的效能。測試採用單機單Shard、64核機器、SSD磁碟,主要分析各個場景的計算開銷,不考慮作業系統Cache的影響,測試結果僅供參考。
單Term查詢
ES中建立一個Index,一個shard,無replica。有1000萬行資料,每行只有幾個標籤和一個唯一ID,現在將這些資料寫入這個Index中。其中Tag1這個標籤只有a和b兩個值,現在要從1000萬行中找到一條Tag1=a的資料(約500萬)。給出以下查詢,那麼它耗時如何呢: 請求: { "query": { "constant_score": { "filter": { "term": { "Tag1": "a" } } } }, "size": 1}' 響應: {"took":233,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5184867,"max_score":1.0,"hits":...}
這個請求耗費了233ms,並且返回了符合條件的資料總數:5184867條。
對於Tag1="a"這個查詢條件,我們知道是查詢Tag1="a"的倒排鏈,這個倒排鏈的長度是5184867,是非常長的,主要時間就花在掃描這個倒排鏈上。其實對這個例子來說,掃描倒排鏈帶來的收益就是拿到了符合條件的記錄總數,因為條件中設定了constant_score,所以不需要算分,隨便返回一條符合條件的記錄即可。對於要算分的場景,Lucene會根據詞條在doc中出現的頻率來計算分值,並取分值排序返回。
目前我們得到一個結論,233ms時間至少可以掃描500萬的倒排鏈,另外考慮到單個請求是單執行緒執行的,可以粗略估算,一個CPU核在一秒內掃描倒排鏈內doc的速度是千萬級的。
我們再換一個小一點的倒排鏈,長度為1萬,總共耗時3ms。
{"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":10478,"max_score":1.0,"hits":...}
Term組合查詢
首先考慮兩個Term查詢求交集:
對於一個Term的組合查詢,兩個倒排鏈分別為1萬和500萬,合併後符合條件的資料為5000,查詢效能如何呢? 請求: { "size": 1, "query": { "constant_score": { "filter": { "bool": { "must": [ { "term": { "Tag1": "a" // 倒排鏈長度500萬 } }, { "term": { "Tag2": "0" // 倒排鏈長度1萬 } } ] } } } } } 響應: {"took":21,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5266,"max_score":2.0,"hits":...}
這個請求耗時21ms,主要是做兩個倒排鏈的求交操作,因此我們主要分析skipList的效能。
這個例子中,倒排鏈長度是1萬、500萬,合併後仍有5000多個doc符合條件。對於1萬的倒排鏈,基本上不進行skip,因為一半的doc都是符合條件的,對於500萬的倒排鏈,平均每次skip1000個doc。因為倒排鏈在儲存時最小的單位是BLOCK,一個BLOCK一般是128個docID,BLOCK內不會進行skip操作。所以即使能夠skip到某個BLOCK,BLOCK內的docID還是要順序掃描的。所以這個例子中,實際掃描的docID數粗略估計也有幾十萬,所以總時間花費了20多ms也符合預期。
對於Term查詢求並集呢,將上面的bool查詢的must改成should,查詢結果為:
{"took":393,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5190079,"max_score":1.0,"hits":...}
花費時間393ms,所以求並集的時間是多於其中單個條件查詢的時間。
字串範圍查詢
RecordID是一個UUID,1000萬條資料,每個doc都有一個唯一的uuid,從中查詢0~7開頭的uuid,大概結果有500多萬個,效能如何呢? 請求: { "query": { "constant_score": { "filter": { "range": { "RecordID": { "gte": "0", "lte": "8" } } } } }, "size": 1 } 響應: {"took":3001,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5185663,"max_score":1.0,"hits":...} 查詢a開頭的uuid,結果大概有60多萬,效能如何呢? 請求: { "query": { "constant_score": { "filter": { "range": { "RecordID": { "gte": "a", "lte": "b" } } } } }, "size": 1 } 響應: {"took":379,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":648556,"max_score":1.0,"hits":...}
這個查詢我們主要分析FST的查詢效能,從上面的結果中我們可以看到,FST的查詢效能相比掃描倒排鏈要差許多,同樣掃描500萬的資料,倒排鏈掃描只需要不到300ms,而FST上的掃描花費了3秒,基本上是慢十倍的。對於UUID長度的字串來說,FST範圍掃描的效能大概是每秒百萬級。
字串範圍查詢加Term查詢
字串範圍查詢(符合條件500萬),加上兩個Term查詢(符合條件5000),最終符合條件數目2600,效能如何? 請求: { "query": { "constant_score": { "filter": { "bool": { "must": [ { "range": { "RecordID": { "gte": "0", "lte": "8" } } }, { "term": { "Tag1": "a" } }, { "term": { "Tag2": "0" } } ] } } } }, "size": 1 } 結果: {"took":2849,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":2638,"max_score":1.0,"hits":...}
這個例子中,查詢消耗時間的大頭還是在掃描FST的部分,透過FST掃描出符合條件的Term,然後讀取每個Term對應的docID列表,構造一個BitSet,再與兩個TermQuery的倒排鏈求交集。
數字Range查詢
對於數字型別,我們同樣從1000萬資料中查詢500萬呢? 請求: { "query": { "constant_score": { "filter": { "range": { "Number": { "gte": 100000000, "lte": 150000000 } } } } }, "size": 1 } 響應: {"took":567,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5183183,"max_score":1.0,"hits":...}
這個場景我們主要測試BKD-Tree的效能,可以看到BKD-Tree查詢的效能還是不錯的,查詢500萬個doc花費了500多ms,只比掃描倒排鏈差一倍,相比FST的效能有了很大的提升。地理位置相關的查詢也是透過BKD-Tree實現的,效能很高。
數字Range查詢加Term查詢
這裡我們構造一個複雜的查詢場景,數字Range範圍資料500萬,再加兩個Term條件,最終符合條件資料2600多條,效能如何? 請求: { "query": { "constant_score": { "filter": { "bool": { "must": [ { "range": { "Number": { "gte": 100000000, "lte": 150000000 } } }, { "term": { "Tag1": "a" } }, { "term": { "Tag2": "0" } } ] } } } }, "size": 1 } 響應: {"took":27,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":2638,"max_score":1.0,"hits":...}
這個結果出乎我們的意料,竟然只需要27ms!因為在上一個例子中,數字Range查詢耗時500多ms,而我們增加兩個Term條件後,時間竟然變為27ms,這是為何呢?
實際上,Lucene在這裡做了一個最佳化,底層有一個查詢叫做IndexOrDocValuesQuery,會自動判斷是查詢Index(BKD-Tree)還是DocValues。在這個例子中,查詢順序是先對兩個TermQuery求交集,得到5000多個docID,然後讀取這個5000多個docID對應的docValues,從中篩選符合數字Range條件的資料。因為只需要讀5000多個doc的docValues,所以花費時間很少。
簡單結論
-
總體上講,掃描的doc數量越多,效能肯定越差。
-
單個倒排鏈掃描的效能在每秒千萬級,這個效能非常高,如果對數字型別要進行Term查詢,也推薦建成字串型別。
-
透過skipList進行倒排鏈合併時,效能取決於最短鏈的掃描次數和每次skip的開銷,skip的開銷比如BLOCK內的順序掃描等。
-
FST相關的字串查詢要比倒排鏈查詢慢很多(萬用字元查詢更是效能殺手,本文未做分析)。
-
基於BKD-Tree的數字範圍查詢效能很好,但是由於BKD-Tree內的docID不是有序的,不能採用類似skipList的向後跳的方式,如果跟其他查詢做交集,必須先構造BitSet,這一步可能非常耗時。Lucene中透過IndexOrDocValuesQuery對一些場景做了最佳化。
最後結尾再放一個彩蛋,既然掃描資料越多,效能越差,那麼能否獲取到足夠資料就提前終止呢,下一篇文章我會介紹一種這方面的技術,可以極大的提高很多場景下的查詢效能。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31551794/viewspace-2218073/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 從根上理解elasticsearch(lucene)查詢原理(2)-lucene常見查詢型別原理分析Elasticsearch型別
- Lucene查詢原理
- 從根上理解elasticsearch(lucene)查詢原理(1)-lucece查詢邏輯介紹Elasticsearch
- Elasticsearch系列---聚合查詢原理Elasticsearch
- elasticsearch查詢之大資料集分頁效能分析Elasticsearch大資料
- Lucene的分頁查詢
- ES 20 - 查詢Elasticsearch中的資料 (基於DSL查詢, 包括查詢校驗match + bool + term)Elasticsearch
- SQL查詢效能分析SQL
- 從查詢重寫角度理解elasticsearch的高亮原理Elasticsearch
- ElasticSearch基礎及查詢語法Elasticsearch
- Elasticsearch查詢Elasticsearch
- ElasticSearch的查詢(二)Elasticsearch
- elasticsearch的模糊查詢Elasticsearch
- Elasticsearch Lucene 資料寫入原理 | ES 核心篇Elasticsearch
- lucene 多欄位查詢-MultiFieldQueryParser
- ElasticSearch效能原理拆解Elasticsearch
- 求助~怎麼指定lucene查詢的field?
- Elasticsearch中的Term查詢和全文查詢Elasticsearch
- ElasticSearch DSL 查詢Elasticsearch
- Elasticsearch 高亮查詢Elasticsearch
- MySQL 查詢效能分析之 ExplainMySqlAI
- elasticsearch查詢之三種fetch id的方案分析Elasticsearch
- Elasticsearch複合查詢——boosting查詢Elasticsearch
- 基於Python的效能分析Python
- 剖析Elasticsearch的IndexSorting:一種查詢效能優化利器ElasticsearchIndex優化
- 基於聯合查詢的注入
- elasticsearch之多索引查詢Elasticsearch索引
- Elasticsearch 分頁查詢Elasticsearch
- Elasticsearch 或並查詢Elasticsearch
- Elasticsearch(三):索引查詢Elasticsearch索引
- elasticsearch之exists查詢Elasticsearch
- ElasticSearch中的簡單查詢Elasticsearch
- Lucene多欄位查詢&高亮顯示
- Lucene學習總結之八:Lucene的查詢語法,JavaCC及QueryParser(1)Java
- 一次elasticsearch 查詢瞬間超時案例分析Elasticsearch
- 基於Lucene的全文檢索實踐
- Lucene : 基於Java的全文搜尋引擎Java
- Elasticsearch——定位不合法的查詢Elasticsearch