Elasticsearch查詢原理淺析
由於最近參與的項目中用到了Elasticsearch,所以學習了解了一下,這里做一個簡單對ES的查詢原理做一個學習總結,限於作者水平,如有錯誤,歡迎批評指正。
一、概述
Elasticsearch作為一個開源的高擴展的分布式全文搜索引擎。最為人稱道就是它對於海量數據近乎實時的強大搜索能力了。這里我們從索引的角度來分析一下,為什么Elasticsearch能夠實現快速的檢索。
二、索引結構
Elasticsearch索引的精髓:
一切設計都是為了提高搜索的性能
為了提高搜索的性能,難免會犧牲某些其他方面的性能,比如插入、更新。假如我們插入一個JSON的對象,Elasticsearch會為每一個Field建立索引。Elasticsearch底層使用的Lucene的倒排索引技術來實現比關系型數據庫更快的過濾的。所以要想了解Lucene的倒排索引,需要先介紹一下Lucene的數據模型。
2.1 Lucene的數據模型
Lucene中包含四種基本數據類型,分別是:
- Index: 索引,由很多Document組成。相當與MySQL中數據庫的概念。
- Document:由很多Field組成。相當與MySQL中Row的概念。也是查詢結果的最小單位。
- Field: 由很多Term組成,包括Field Name和Field Value.
- Term: 由很多字節組成。常譯為單詞,一般將Text類型中的Field Value分詞之后的最小單元叫做Term,如果是數值類型或者布爾型這種不可分的類型,那Field Value直接就是Term。
2.2 倒排索引
倒排索引(Inverted Index)又稱反向索引,是一種索引方法,被用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射。由於不是由記錄來確定屬性值,而是由屬性值來確定記錄的位置,因而稱為倒排索引。
Lucene的倒排索引結構如下;
下面通過具體的例子來介紹一下,這三個結構的具體概念。
假設有如下數據:
docid | name | age | sex |
---|---|---|---|
1 | Kate | 18 | Female |
2 | John | 18 | Male |
3 | Bill | 20 | Male |
這里每一個行都是一個document, 每個document都是一個文檔編號docid,由Lucene自建的文檔唯一標識。
那么,Elasticsearch建立的索引如下:
name:
Term | PostingList |
---|---|
Kate | 1 |
John | 2 |
Bill | 3 |
age:
Term | PostingList |
---|---|
18 | 1, 2 |
20 | 3 |
sex:
Term | PostingList |
---|---|
Female | 1 |
Male | 2, 3 |
2.2.1 Posting List
從上面例子可以看出,Elastictsearch為每個Field都建立了一個倒排索引。
Posting List就是一個int的數組,存儲了所有符合某個Term的文檔id。
優點:
- 通過這種方式,我們已經可以快速地通過屬性值,查找到相應的文檔,而不用遍歷所有的文檔來查詢。從而大大提升了查詢效率。
問題:
- 如果Term的候選值很多,成千上百萬,那該如何查詢?
2.2.2 Term Dictionary
Term Dictionary是所有單詞(Term)的不重復有序列表。
為了解決上面的這個問題,提升查找效率,Elasticsearch需要預先將Term Dictionary排序,這樣才能二分查找,實現logN的查詢效率。
2.2.3 Term Index
B樹/B+樹索引,為了提升查詢性能,減少磁盤尋道次數,會將索引樹頻繁使用的上幾層換入內存。
而ES中,一般來講Term Dictionary會很大,放內存不現實,而且也不是樹形結構,沒辦法像B樹/B+樹那樣部分換入內存,提高性能。
所以,Lucene又單獨為Term Dictionary創建了一個索引——Term Index。用來記錄以不同前綴開頭的Term分別在Term Dictionary的起始位置。
這個Term Index可以用HashMap來實現,當需要查找一個Term時,可以通過Hash前綴找到目標在Term Dictionary的起止點,然后二分,直到命中,得到Posting List。
但是,從Lucene4開始,為了實現范圍查詢、前綴、后綴等復雜的查詢語句,以及減少內存使用,Lucene采用了FST(Finite State Transducer)來存儲Term Index。
2.2.4 FST
FST(Finite State Transducer)直譯為有限狀態傳感器。下面通過Alice和Alan這兩個單詞,來看下FST的構建過程。
插入“Alan”
插入“Alice”
這樣就得到了一個加權有向無環圖,每條路徑上都有權重,權重相加,即得前綴在Term Dictionary上的起始位置。
FST通過前綴實現了對狀態的壓縮。實際的FST還可以開啟后綴壓縮,從而實現更快的后綴搜索。
FST在單個Term的查詢相比HashMap沒有明顯優勢,甚至會慢一些,但是在范圍、前綴搜索以及壓縮率方面優勢明顯。
2.2.5 整體結構
有了FST之后,Lucene的整體倒排結構如下所示:
三、壓縮技巧
對於TermIndex,FST其實已經是一種壓縮技巧了,它使得Term Index足夠小,以至於可以直接放入內存。
對於TermDictionary,有了Term Index之后,可以把公共前綴去掉,從而使一個磁盤塊能夠存儲更多Term,這樣磁盤一次隨機讀(Random Access)能夠將更多的Term加載內存,從而減少了磁盤尋道時間,且提高了磁盤利用率。
對於Posting List,Elasticsearch也提供了數據壓縮的方法。也是本節要重點介紹的,因為Elasticsearch中存儲了海量的數據,所以每個Posting List都可能很大。
舉個極端例子,如果Elasticsearch需要對性別這個Field進行索引,會出現什么情況呢,每個Posting List將幾乎是文檔總數的一半。
3.1 Frame of Reference
增量壓縮編碼,將大數變成小數,按字節存儲
首先,Elasticsearch要求Posting List是有序的,這樣的一個好處是方便壓縮。一下是官網給出的一個例子:
從上圖可以看出:
-
原本占24字節的6個數,壓縮之后只占7個字節
-
每個數的位置只存儲相對前一個數的增量值
-
存儲方式按字節分組存儲,而不是一個很小的數也用32位去存儲。
- 圖中,第一組每個數用8bit存,第二組每個數用5bit存
3.2 Roaring Bitmap
Bitmap是大數據中常用的一種數據結構,由於使用bit作為單位來存儲數據,因此可以大大節省存儲空間。
假設某個Posting List是:[1, 3, 4, 7, 10],占用空間位5 * 32 = 160 bit.
那其對應是一個長度為10(也可能是11,看下標從0還是1開始)的bitmap就是:[1, 0, 1, 1, 0, 0, 1, 0, 0, 1],占空間為 10 bit.
但是Elasticsearch中的文檔達到億級時,一個長度為一億的bitmap占內存125MB,這個消耗仍然是很奢侈的。
所以Lucene采用Roaring Bitmap這個數據結構。如下圖所示:
步驟如下:
- 第一步,將原數對65536求商,求余,記錄成(商,余數)
- short能表示范圍0-65535
- 第二步,通過的商,將余數分組(block)
- 第三步,如果一個block中的元素個數大於4096, 則用bitset存儲;否則用short數組存儲。
為什么是是4096作為閾值呢?
- 僅僅因為在一個塊中超過此數量的文檔,位圖就比數組更節省內存。
四、 聯合查詢
如果是多個Field上的聯合查詢,比如:age = 18 and sex = Female
。對於MySQL,如果在兩個字段上都建立索引,查詢僅能使用其中過濾性最好的一個,然后另個條件在內存中遍歷過濾掉。而對於Lucene,直觀的想法是分別查詢出兩個條件的Postint List, 將兩個Postint List的合並做“與”操作,即求交集。
但是這個求交集的操作並不容易。因為Posting List可能會很大,好在我們通過上面的壓縮方法,已經對Posting List進行了壓縮,
對應上面提到的Posting List的兩種壓縮方法,即存儲方式,這里分別介紹相應的合並方式。
4.1 利用SkipList合並
SkipList的結構如下:
假設查詢docid = 12,原先可能需要從頭掃描原始鏈表,現在的過程是,首先訪問第一層發現15大於12,然后進入第二層訪問3,8,發現15大於12,進入原鏈表訪問10,然后命中12.
引入SkipList之后 ,合並多個Posting List的過程如下:
- 以最短的Posting List 為基准,遍歷。
- 在其他Posting List中,利用跳表,快速查找是否含有當前遍歷值。
引入SkipList的優點:
- 能夠快速跳過不需要比較的值,加快多個Posting List交集的操作
- 由於采用Frame of Reference壓縮編碼,有一定解壓縮成本,跳過不需要比較的值,這些值不需要解壓縮,也能節省cpu。
4.2 利用BitSet按位與
這種就沒什么好說的了,直接將多個Posting List的Roaring Bitmap做位與操作即可,當然里面用short數組存儲的部分還是要遍歷的求交集的。
4.3 合並的效率對比
Elasticsearch對原始int數組、bitmap、Roaring Bitmap、FOR(Frame of Reference)四種方式的性能做了較為詳細的對比。
結論如下:
- 首先沒有一種特定的方法在各個指標上都比其他方法更好
- int數組雖然很快,但是在密集集上內存占用最大,被淘汰
- bitmap由於在稀疏集上的性能和內存使用上都很差,被淘汰
- Roaring Bitmap在各個指標上雖然不是最好的,但也不是最差的,較為均衡
- Frame of Reference因為編碼高和跳表的結構,雖然Posting List存儲在磁盤,而不是內存中,但是也保持了很快的速度。
五、總結
Elasticsearch的索引思路:
- 通過多級索引的方式減小最前端索引占用的空間,感覺與操作系統防止頁表太大,而分為多級頁表的思想類似
- 將索引放入內存,盡可能減少磁盤隨機讀,提高索引查找效率
- 在索引的各個結構Term Index、Term Dictionary、Postintg List上幾乎都采用了一定的壓縮技巧,一方面能夠減少內存占用;另一方面一次磁盤IO也能讀入更多的數據,從而側面減少磁盤隨機讀。