Elasticsearch索引原理

最近在參與一個基於Elasticsearch做爲底層數據框架提供大數據量(億級)的實時統計查詢的方案設計工做,花了些時間學習Elasticsearch的基礎理論知識,整理了一下,但願能對Elasticsearch感興趣/想了解的同窗有所幫助。 同時也但願有發現內容不正確或者有疑問的地方,望指明,一塊兒探討,學習,進步。html

介紹

Elasticsearch 是一個分佈式可擴展的實時搜索和分析引擎.java

Elasticsearch 是一個創建在全文搜索引擎 Apache Lucene(TM) 基礎上的搜索引擎. 固然 Elasticsearch 並不只僅是 Lucene 那麼簡單,它不只包括了全文搜索功能,還能夠進行如下工做:mysql

  • 分佈式實時文件存儲,並將每個字段都編入索引,使其能夠被搜索。
  • 實時分析的分佈式搜索引擎。
  • 能夠擴展到上百臺服務器,處理PB級別的結構化或非結構化數據。

    基本概念

先說Elasticsearch的文件存儲,Elasticsearch是面向文檔型數據庫,一條數據在這裏就是一個文檔,用JSON做爲文檔序列化的格式,好比下面這條用戶數據:程序員

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

MySQL這樣的數據庫存儲就會容易想到創建一張User表,有balabala的字段等,在Elasticsearch裏這就是一個文檔,固然這個文檔會屬於一個User的類型,各類各樣的類型存在於一個索引當中。這裏有一份簡易的將Elasticsearch和關係型數據術語對照表:算法

關係數據庫 ⇒ 數據庫 ⇒ 表 ⇒ 行 ⇒ 列(Columns)sql

Elasticsearch ⇒ 索引 ⇒ 類型 ⇒ 文檔 ⇒ 字段(Fields)數據庫

一個 Elasticsearch 集羣能夠包含多個索引(數據庫),也就是說其中包含了不少類型(表)。這些類型中包含了不少的文檔(行),而後每一個文檔中又包含了不少的字段(列)。json

Elasticsearch的交互,可使用Java API,也能夠直接使用HTTP的Restful API方式,好比咱們打算插入一條記錄,能夠簡單發送一個HTTP的請求:數組

PUT /megacorp/employee/1 { "name" : "John", "sex" : "Male", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }

更新,查詢也是相似這樣的操做,具體操做手冊能夠參見Elasticsearch權威指南緩存


索引

Elasticsearch最關鍵的就是提供強大的索引能力了,其實InfoQ的這篇時間序列數據庫的祕密(2)——索引寫的很是好,我這裏也是圍繞這篇結合本身的理解進一步梳理下,也但願能夠幫助你們更好的理解這篇文章。

Elasticsearch索引的精髓:

一切設計都是爲了提升搜索的性能

另外一層意思:爲了提升搜索的性能,不免會犧牲某些其餘方面,好比插入/更新,不然其餘數據庫不用混了:)

前面看到往Elasticsearch裏插入一條記錄,其實就是直接PUT一個json的對象,這個對象有多個fields,好比上面例子中的name, sex, age, about, interests,那麼在插入這些數據到Elasticsearch的同時,Elasticsearch還默默

的爲這些字段創建索引–倒排索引,由於Elasticsearch最核心功能是搜索。

Elasticsearch是如何作到快速索引的

InfoQ那篇文章裏說Elasticsearch使用的倒排索引比關係型數據庫的B-Tree索引快,爲何呢?

什麼是B-Tree索引?

上大學讀書時老師教過咱們,二叉樹查找效率是logN,同時插入新的節點沒必要移動所有節點,因此用樹型結構存儲索引,能同時兼顧插入和查詢的性能。

所以在這個基礎上,再結合磁盤的讀取特性(順序讀/隨機讀),傳統關係型數據庫採用了B-Tree/B+Tree這樣的數據結構

Alt text

爲了提升查詢的效率,減小磁盤尋道次數,將多個值做爲一個數組經過連續區間存放,一次尋道讀取多個數據,同時也下降樹的高度。

什麼是倒排索引?

Alt text

繼續上面的例子,假設有這麼幾條數據(爲了簡單,去掉about, interests這兩個field):

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]
Posting List

Elasticsearch分別爲每一個field都創建了一個倒排索引,Kate, John, 24, Female這些叫term,而[1,2]就是Posting List。Posting list就是一個int的數組,存儲了全部符合某個term的文檔id。

看到這裏,不要認爲就結束了,精彩的部分纔剛開始…

經過posting list這種索引方式彷佛能夠很快進行查找,好比要找age=24的同窗,愛回答問題的小明立刻就舉手回答:我知道,id是1,2的同窗。可是,若是這裏有上千萬的記錄呢?若是是想經過name來查找呢?

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是一顆樹:Alt text

這棵樹不會包含全部的term,它包含的是term的一些前綴。經過term index能夠快速地定位到term dictionary的某個offset,而後從這個位置再日後順序查找。
Alt text

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

這時候愛提問的小明又舉手了:」那個FST是神馬東東啊?」

一看就知道小明是一個上大學讀書的時候跟我同樣不認真聽課的孩子,數據結構老師必定講過什麼是FST。但沒辦法,我也忘了,這裏再補下課:

FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.

假設咱們如今要將mop, moth, pop, star, stop and top(term index裏的term前綴)映射到序號:0,1,2,3,4,5(term dictionary的block位置)。最簡單的作法就是定義個Map<String, Integer>,你們找到本身的位置對應入座就行了,但從內存佔用少的角度想一想,有沒有更優的辦法呢?答案就是:FST(理論依據在此,但我相信99%的人不會認真看完的)

Alt text

⭕️表示一種狀態

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

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

FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.

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

後面的更精彩,看累了的同窗能夠喝杯咖啡……


壓縮技巧

Elasticsearch裏除了上面說到用FST壓縮term index外,對posting list也有壓縮技巧。 小明喝完咖啡又舉手了:」posting list不是已經只存儲文檔id了嗎?還須要壓縮?」

嗯,咱們再看回最開始的例子,若是Elasticsearch須要對同窗的性別進行索引(這時傳統關係型數據庫已經哭暈在廁所……),會怎樣?若是有上千萬個同窗,而世界上只有男/女這樣兩個性別,每一個posting list都會有至少百萬個文檔id。 Elasticsearch是如何有效的對這些文檔id壓縮的呢?

Frame Of Reference

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

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

若是數學不是體育老師教的話,仍是比較容易看出來這種壓縮技巧的。

原理就是經過增量,將原來的大數變成小數僅存儲增量值,再精打細算按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存儲。

Alt text

細心的小明這時候又舉手了:」爲何是以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恰好是邊界:

Alt text


聯合索引

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

  • 利用跳錶(Skip list)的數據結構快速作「與」運算,或者
  • 利用上面提到的bitset按位「與」

先看看跳錶的數據結構:

Alt text

將一個有序鏈表level0,挑出其中幾個元素到level1及level2,每一個level越往上,選出來的指針元素越少,查找時依次從高level往低查找,好比55,先找到level2的31,再找到level1的47,最後找到55,一共3次查找,查找效率和2叉樹的效率至關,但也是用了必定的空間冗餘來換取的。

假設有下面三個posting list須要聯合索引:

Alt text

若是使用跳錶,對最短的posting list中的每一個id,逐個在另外兩個posting list中查找看是否存在,最後獲得交集的結果。

若是使用bitset,就很直觀了,直接按位與,獲得的結果就是最後的交集。


總結和思考

Elasticsearch的索引思路:

將磁盤裏的東西儘可能搬進內存,減小磁盤隨機讀取次數(同時也利用磁盤順序讀特性),結合各類奇技淫巧的壓縮算法,用及其苛刻的態度使用內存。

因此,對於使用Elasticsearch進行索引時須要注意:

  • 不須要索引的字段,必定要明肯定義出來,由於默認是自動建索引的
  • 一樣的道理,對於String類型的字段,不須要analysis的也須要明肯定義出來,由於默認也是會analysis的
  • 選擇有規律的ID很重要,隨機性太大的ID(好比java的UUID)不利於查詢

關於最後一點,我的認爲有多個因素:

其中一個(也許不是最重要的)因素: 上面看到的壓縮算法,都是對Posting list裏的大量ID進行壓縮的,那若是ID是順序的,或者是有公共前綴等具備必定規律性的ID,壓縮比會比較高;

另一個因素: 多是最影響查詢性能的,應該是最後經過Posting list裏的ID到磁盤中查找Document信息的那步,由於Elasticsearch是分Segment存儲的,根據ID這個大範圍的Term定位到Segment的效率直接影響了最後查詢的性能,若是ID是有規律的,能夠快速跳過不包含該ID的Segment,從而減小沒必要要的磁盤讀次數,具體能夠參考這篇如何選擇一個高效的全局ID方案(評論也很精彩)

 

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特色是很是節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,好比都是Ab開頭的單詞就能夠把Ab省去。這樣term dictionary能夠比b-tree更節約磁盤空間。

 --------------------------------------------------------

lucene並不是使用Tree structure
– sorted for range queries
– O(log(n)) search

而是以下核心的數據結構,FST,delta encode壓縮數組,列存儲,LZ4壓縮算法:
●Terms index: map a term prefix to a block in the dict ○ FST: automaton with weighted arcs, compact thanks to shared prefixes/suffixes 核心數據結構,本質是先後綴共享的狀態機,相似trie來搜索用戶輸入的某個單詞是否能搜到,搜到的話就跳轉到Terms dictionary裏去,搜到的結果是單詞在terms dict裏的offset(本質是數組的偏移量)
Lookup the term in the terms index
– In-memory FST storing terms prefixes
– Gives the offset to look at in the terms dictionary
– Can fast-fail if no terms have this prefix
●Terms dictionary: statistics + pointer in postings lists, Store terms and documents in arrays – binary search
• Jump to the given offset in the terms dictionary
– compressed based on shared prefixes, similarly to a burst trie
– called the 「BlockTree terms dict」
• read sequentially until the term is found
●Postings lists: encodes matching docs in sorted order ○ + positions + offsets 倒排的文檔ID都在此
• Jump to the given offset in the postings lists
• Encoded using modified FOR (Frame of Reference) delta
– 1. delta-encode
– 2. split into block of N=128 values
– 3. bit packing per block
– 4. if remaining docs, encode with vInt
●Stored fields
• In-memory index for a subset of the doc ids
– memory-efficient thanks to monotonic compression
– searched using binary search
• Stored fields
– stored sequentially
– compressed (LZ4) in 16+KB blocks

Query execution:
• 2 disk seeks per field for search
• 1 disk seek per doc for stored fields
• It is common that the terms dict / postings lists fits into the file-system cache
• 「Pulse」 optimization
– For unique terms (freq=1), postings are inlined in the terms dict
– Only 1 disk seek
– Will always be used for your primary keys

插入新數據:
Insertion = write a new segment 一直寫信segment能夠防止使用鎖
• Merge segments when there are too many of them
– concatenate docs, merge terms dicts and postings lists (merge sort!)
刪除:
Deletion = turn a bit off
• Ignore deleted documents when searching and merging (reclaims space)
• Merge policies favor segments with many deletions

優缺點:
Updates require writing a new segment
– single-doc updates are costly, bulk updates preferred
– writes are sequential
• Segments are never modified in place
– filesystem-cache-friendly
lock-free!
• Terms are deduplicated
– saves space for high-freq terms
• Docs are uniquely identified by an ord
– useful for cross-API communication
– Lucene can use several indexes in a single query
• Terms are uniquely identified by an ord
– important for sorting: compare longs, not strings
– important for faceting (more on this later)

針對field使用列存儲:
Per doc and per field single numeric values, stored in a column-stride fashion
• Useful for sorting and custom scoring
• Norms are numeric doc values

一些設計原則:
• Save file handles
– don’t use one file per field or per doc
• Avoid disk seeks whenever possible
– disk seek on spinning disk is ~10 ms
• BUT don’t ignore the filesystem cache
– random access in small files is fine
• Light compression helps
– less I/O
– smaller indexes
– filesystem-cache-friendly

針對Compression techniques的數據結構:FSTs LZ4

相關文章
相關標籤/搜索