Elasticsearch 實現快速索引的原理


基本概念

Elasticsearch的文件存儲,es是面向文檔型數據庫,一條數據在這里就是一個文檔,用json作為序列化的格式,比如下面這條用戶數據:

{
    "name":"John",
    "sex":"Male",
    "age":25,
    "birthDate":"1990/05/01",
    "about":"I love to go rock climbing",
    "interests":["sports","music"]
}

用Mysql這樣的數據庫存儲就會容易想到建立一張User表,有各種字段,在es里這是一個文檔,當然這個文檔會屬於一個User的類型,各種各樣的類型存在於一個索引當中。es和關系型數據庫對照:

關系型數據庫:數據庫 => 表 => 行 => 列

Elasticsearch:索引 => 類型 => 文檔 => 字段

一個 Elasticsearch 集群可以包含多個索引(數據庫),也就是說其中包含了很多類型(表)。這些類型中包含了很多的文檔(行),然后每個文檔中又包含了很多的字段(列)。

es的交互,可以使用Java API,也可以直接使用HTTP的Restful API方式。

索引

es索引的精髓:

一切設計都是為了提高搜索的性能,即為了提高搜索的性能,會犧牲某些其他方面,如插入/更新。如es在插入一條記錄時,其實就是PUT一個json對象,這個對象中有多個fields,在插入這些數據到es的同時,es會為這些字段建立索引-倒排索引。

Elasticsearch是如何做到快速索引的呢?

什么是B-Tree索引?

二叉樹查找效率是logN,同時插入新的節點不必移動全部節點,所以用樹型結構存儲索引,能同時兼顧插入和查詢的性能。因此在這個基礎上,再結合磁盤的讀取特性(順序讀/隨機讀),傳統關系型數據庫采用了B-Tree/B+Tree這樣的數據結構:

 

為了提高查詢的效率,減少磁盤尋道次數,將多個值作為一個數組通過連續區間存放,一次尋道讀取多個數據,同時也降低樹的高度。

什么是倒排索引?

假設有一下幾條數據:

ID    Name    Age    Sex
1     Kate    24     Female
2     John    24     Male
3     Bill    29     Male

ID是Elasticsearch自建的文檔id,那么Elasticsearch建立的索引如下:

Name:
Term Posting List
Kate 1
John 2
Bill 3

Age:
Term Posting List
24 [1,2]
29 3

Sex:
Term Posting List
Female 1
Male [2,3]

Elasticsearch分別為每個field都建立了一個倒排索引,Kate, John, 24, Female這些叫term,而[1,2]就是Posting List。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。通過posting list這種索引方式似乎可以很快進行查找,比如要找age=24的同學。

Term Dictionary

Elasticsearch為了能快速找到某個term,將所有的term排個序,二分法查找term,logN的查找效率,就像通過字典查找一樣,這就是Term Dictionary。現在再看起來,似乎和傳統數據庫通過B-Tree的方式類似啊,那么為什么說比B-Tree的查詢快呢?

Term Index

B-Tree通過減少磁盤尋道次數來提高查詢性能,Elasticsearch也是采用同樣的思路,直接通過內存查找term,不讀磁盤,但是如果term太多,term dictionary也會很大,放內存不現實,於是有了 Term Index,就像字典里的索引頁一樣,A開頭的有哪些term,分別在哪頁,可以理解term index是一顆樹:

 

這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然后從這個位置再往后順序查找。

所以term index不需要存下所有的term,而僅僅是他們的一些前綴與Term Dictionary的block之間的映射關系,再結合FST(Finite State Transducers)的壓縮技術,可以使term index緩存到內存中。從term index查到對應的term dictionary的block位置之后,再去磁盤上找term,大大減少了磁盤隨機讀的次數。

假設我們現在要將mop, moth, pop, star, stop and top(term index里的term前綴)映射到序號:0,1,2,3,4,5(term dictionary的block位置)。最簡單的做法就是定義個Map,大家找到自己的位置對應入座就好了,但從內存占用少的角度想想,有沒有更優的辦法呢?答案就是:FST。

 

⭕️ 表示一種狀態

–> 表示狀態的變化過程,上面的字母/數字表示狀態變化和權重

將單詞分成單個字母通過⭕️和–>表示出來,0權重不顯示。如果⭕️后面出現分支,就標記權重,最后整條路徑上的權重加起來就是這個單詞對應的序號。

FST以字節的方式存儲所有的term,這種壓縮方式可以有效的縮減存儲空間,使得term index足以放進內存,但這種方式也會導致查找時需要更多的CPU資源。

壓縮技巧

Elasticsearch里除了上面說到用FST壓縮term index外,對posting list也有壓縮技巧。如果Elasticsearch需要對同學的性別進行索引會怎樣?如果有上千萬個同學,而世界上只有男/女這樣兩個性別,每個posting list都會有至少百萬個文檔id。Elasticsearch是如何有效的對這些文檔id壓縮的呢?

增量編碼壓縮,將大數變小數,按字節存儲

首先,Elasticsearch要求posting list是有序的(為了提高搜索的性能,再任性的要求也得滿足),這樣做的一個好處是方便壓縮,看下面這個圖例:

原理就是通過增量,將原來的大數變成小數僅存儲增量值,再精打細算按bit排好隊,最后通過字節存儲,而不是大大咧咧的盡管是2也是用int(4個字節)來存儲。

Roaring bitmaps

說到Roaring bitmaps,就必須先從bitmap說起。Bitmap是一種數據結構,假設有某個posting list:[1,3,4,7,10],那么對應的bitmap就是:[1,0,1,1,0,0,1,0,0,1]。

非常直觀,用0/1表示某個值是否存在,比如10這個值就對應第10位,對應的bit值是1,這樣用一個字節就可以代表8個文檔id,舊版本(5.0之前)的Lucene就是用這樣的方式來壓縮的,但這樣的壓縮方式仍然不夠高效,如果有1億個文檔,那么需要12.5MB的存儲空間,這僅僅是對應一個索引字段(我們往往會有很多個索引字段)。於是有人想出了Roaring bitmaps這樣更高效的數據結構。
Bitmap的缺點是存儲空間隨着文檔個數線性增長,Roaring bitmaps需要打破這個魔咒就一定要用到某些指數特性:
將posting list按照65535為界限分塊,比如第一塊所包含的文檔id范圍在0~65535之間,第二塊的id范圍是65536~131071,以此類推。再用<商,余數>的組合表示每一組id,這樣每組里的id范圍都在0~65535內了,剩下的就好辦了,既然每組id不會變得無限大,那么我們就可以通過最有效的方式對這里的id存儲。

”為什么是以65535為界限?”

程序員的世界里除了1024外,65535也是一個經典值,因為它=2^16-1,正好是用2個字節能表示的最大數,一個short的存儲單位,注意到上圖里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大塊,用節省點用bitset存,小塊就豪爽點,2個字節我也不計較了,用一個short[]存着方便。

那為什么用4096來區分采用數組還是bitmap的閥值呢?

這個是從內存大小考慮的,當block塊里元素超過4096后,用bitmap更剩空間: 采用bitmap需要的空間是恆定的: 65536/8 = 8192bytes 而如果采用short[],所需的空間是: 2*N(N為數組元素個數) 小明手指一掐N=4096剛好是邊界:

聯合索引

上面說了半天都是單field索引,如果多個field索引的聯合查詢,倒排索引如何滿足快速查詢的要求呢?

利用跳表(Skip list)的數據結構快速做“與”運算,或者利用上面提到的bitset按位“與”。先看看跳表的數據結構:

將一個有序鏈表level0,挑出其中幾個元素到level1及level2,每個level越往上,選出來的指針元素越少,查找時依次從高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉樹的效率相當,但也是用了一定的空間冗余來換取的。
假設有下面三個posting list需要聯合索引:

如果使用跳表,對最短的posting list中的每個id,逐個在另外兩個posting list中查找看是否存在,最后得到交集的結果。如果使用bitset,就很直觀了,直接按位與,得到的結果就是最后的交集。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM