基本概念
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

這棵樹不會包含所有的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]。

”為什么是以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按位“與”。先看看跳表的數據結構:

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