帶你走進神一樣的Elasticsearch索引機制

佛西先森發表於2020-04-20

更多精彩內容請看我的個人部落格

前言

相比於大多數人熟悉的MySQL資料庫的索引,Elasticsearch的索引機制是完全不同於MySQL的B+Tree結構。索引會被壓縮放入記憶體用於加速搜尋過程,這一點在效率上是完爆MySQL資料庫的。但是Elasticsearch會對全部text欄位進行索引,必然會消耗巨大的記憶體,為此Elasticsearch針對索引進行了深度的優化。在保證執行效率的同時,儘量縮減記憶體空間的佔用。這篇文章就深度解析了Elasticsearch索引原理,揭開搜尋的神祕面紗。

簡介

Elasticsearch是一個基於Lucene庫的開源搜尋引擎,它提供分散式的實時檔案儲存和搜尋,可擴充套件性好,並且支援通過HTTP網路介面互動,資料以JSON格式展示。

Elasticsearch因為其極快的搜尋和聚合速度通常被應用在各種搜尋應用中,比如在自己的app裡面加一個搜尋框或者分析實時日誌(ELK系統)。

Elasticsearch會對所有輸入的文字進行處理,建立索引放入記憶體中,從而提高搜尋效率。在這一點上ES要優於MySQL的B+樹的結構,MySQL需要將索引放入磁碟,每次讀取需要先從磁碟讀取索引然後尋找對應的資料節點,但是ES能夠直接在記憶體中就找到目標文件對應的大致位置,最大化提高效率。並且在進行組合查詢的時候MySQL的劣勢更加明顯,它不支援複雜的組合查詢比如聚合操作,即使要組合查詢也要事先建好索引,但是ES就可以完成這種複雜的操作,預設每個欄位都是有索引的,在查詢的時候可以各種互相組合。

為了省事,以下統一用ES來代替Elasticsearch,其實我們在公司裡面也基本都說ES,全稱比較難讀。還有一點,因為ES使用了Lucene的庫,下面說的很多優化思路都是Lucene裡面的,但是為了講解方便,我就用ES來代替Lucene。

索引

首先需要申明的是在ES中索引的概念和MySQL裡面的概念不太一樣,這裡列出一下ES和MySQL的對應的概念,方便大家理解。

MySQL ES
庫(database) 索引(index)
表(table) 型別(type)
行(row) 文件(doc)
列(column) 欄位(field)

在ES中每個欄位都是被索引的,所以不會像MySQL中那樣需要對欄位進行手動的建立索引。

ES在建立索引的時候採用了一種叫做倒排索引的機制,保證每次在搜尋關鍵詞的時候能夠快速定位到這個關鍵詞所屬的文件。

Inverted Index

比如有三句話在資料庫中:

  1. Winter is coming
  2. Ours is fury
  3. Ths choice is yours

如果沒有倒排索引(Inverted Index),想要去找其中的choice,需要遍歷整個文件,才能找到對應的文件的id,這樣做效率是十分低的,所以為了提高效率,我們就給輸入的所有資料的都建立索引,並且把這樣的索引和對應的文件建立一個關聯關係,相當於一個詞典。當我們在尋找choice的時候就可以直接像查字典一樣,直接找到所有包含這個資料的文件的id,然後找到資料。

index

Lucene在對文件建立索引的時候,會給詞典的所有的元素排好序,在搜尋的時候直接根據二分查詢的方法進行篩選就能夠快速找到資料。不過是不是感覺有點眼熟,這不就是MySQL的索引方式的,直接用B+樹建立索引詞典指向被索引的文件。

ES做的要更深一點,ES希望把這個詞典“搬進”記憶體,直接從記憶體讀取資料不就比從磁碟讀資料要快很多嗎!問題在於對於海量的資料,索引的空間消耗十分巨大,直接搬進來肯定不合適,所以需要進一步的處理,建立詞典索引(term index)。通過詞典索引可以直接找到搜尋詞在詞典中的大致位置,然後從磁碟中取出詞典資料再進行查詢。所以大致的結構圖就變成了這樣:

index-arch

ES通過有限狀態轉化器,把term dictionary變成了term index,放進了記憶體,所以這個term index到底長什麼樣呢?

Finite State Transducers

有限狀態轉換器(Finite State Transducers)相當於是一個Trie字首樹,可以直接根據字首就找到對應的term在詞典中的位置。

比如我們現在有已經排序好的單詞mopmothpopstarstoptop以及他們的順序編號0..5,可以生成一個下面的狀態轉換圖

fst

當遍歷上面的每一條邊的時候,都會加上這條邊的輸出,比如當輸入是stop的時候會經過s/3o/1,相加得到的排序的順序是4;而對於mop,得到的排序的結果是0

但是這個樹並不會包含所有的term,而是很多term的字首,通過這些字首快速定位到這個字首所屬的磁碟的block,再從這個block去找文件列表。為了壓縮詞典的空間,實際上每個block都只會儲存block內不同的部分,比如mopmoth在同一個以mo開頭的block,那麼在對應的詞典裡面只會儲存pth,這樣空間利用率提高了一倍。

使用有限狀態轉換器在記憶體消耗上面要比遠比SortedMap要少,但是在查詢的時候需要更多的CPU資源。維基百科的索引就是使用的FST,只使用了69MB的空間,花了大約8秒鐘,就為接近一千萬個詞條建立了索引,使用的堆空間不到256MB。

順帶提一句,在ES中有一種查詢叫模糊查詢(fuzzy query),根據搜尋詞和欄位之間的編輯距離來判斷是否匹配。在ES4.0之前,模糊查詢會先讓檢索詞和所有的term計算編輯距離篩選出所有編輯距離內的欄位;在ES4.0之後,採用了由Robert開發的,直接通過有限狀態轉換器就可以搜尋指定編輯距離內的單詞的方法,將模糊查詢的效率提高了超過100倍。

現在已經把詞典壓縮成了詞條索引,尺寸已經足夠小到放入記憶體,通過索引能夠快速找到文件列表。現在又有另外一個問題,把所有的文件的id放入磁碟中會不會佔用了太多空間?如果有一億個文件,每個文件有10個欄位,為了儲存這個posting list就需要消耗十億個integer的空間,磁碟空間的消耗也是巨大的,ES採用了一個更加巧妙的方式來儲存所有的id。

Posting Lists

所謂的posting list,就是所有包含特定term文件的id的集合,需要從詞典對映到這個集合。由於整型數字integer可以被高效壓縮的特質,integer是最適合放在posting list作為文件的唯一標識的,ES會對這些存入的文件進行處理,轉化成一個唯一的整型id

在儲存資料的時候,在每一個shard裡面,ES會將資料存入不同的segment,這是一個比shard更小的分片單位,這些segment會定期合併。在每一個segment裡面都會儲存最多\(2^{31}\)個文件,每個文件被分配一個唯一的id,從\(0\)\(2^{31}-1\)

Frame of Reference

在進行查詢的時候經常會進行組合查詢,比如查詢同時包含choicethe的文件,那麼就需要分別查出包含這兩個單詞的文件的id,然後取這兩個id列表的交集;如果是查包含choice或者the的文件,那麼就需要分別查出posting list然後取並集。為了能夠高效的進行交集和並集的操作,posting list裡面的id都是有序的。同時為了減小儲存空間,所有的id都會進行delta編碼(delta-encoding,我覺得可以翻譯成增量編碼

比如現在有id列表[73, 300, 302, 332, 343, 372],轉化成每一個id相對於前一個id的增量值(第一個id的前一個id預設是0,增量就是它自己)列表是[73, 227, 2, 30, 11, 29]。在這個新的列表裡面,所有的id都是小於255的,所以每個id只需要一個位元組儲存。

實際上ES會做的更加精細,它會把所有的文件分成很多個block,每個block正好包含256個文件,然後單獨對每個文件進行增量編碼,計算出儲存這個block裡面所有文件最多需要多少位來儲存每個id,並且把這個位數作為頭資訊(header)放在每個block 的前面。這個技術叫Frame of Reference,我翻譯成索引幀。

比如對上面的資料進行壓縮(假設每個block只有3個檔案而不是256),壓縮過程如下

frame-of-ref

在返回結果的時候,其實也並不需要把所有的資料直接解壓然後一股腦全部返回,可以直接返回一個迭代器iterator,直接通過迭代器的next方法逐一取出壓縮的id,這樣也可以極大的節省計算和記憶體開銷。

通過以上的方式可以極大的節省posting list的空間消耗,提高查詢效能。不過ES為了提高filter過濾器查詢的效能,還做了更多的工作,那就是快取。

Roaring Bitmaps

ES會快取頻率比較高的filter查詢,其中的原理也比較簡單,即生成(fitler, segment)和id列表的對映,但是和倒排索引不同,我們只把常用的filter快取下來而倒排索引是儲存所有的,並且filter快取應該足夠快,不然直接查詢不就可以了。ES直接把快取的filter放到記憶體裡面,對映的posting list放入磁碟中。

ES在filter快取使用的壓縮方式和倒排索引的壓縮方式並不相同,filter快取使用了roaring bitmap的資料結構,在查詢的時候相對於上面的Frame of Reference方式CPU消耗要小,查詢效率更高,代價就是需要的儲存空間(磁碟)更多。

Roaring Bitmap是由int陣列和bitmap這兩個資料結構改良過的成果——int陣列速度快但是空間消耗大,bitmap相對來說空間消耗小但是不管包含多少文件都需要12.5MB的空間,即使只有一個檔案也要12.5MB的空間,這樣實在不划算,所以權衡之後就有了下面的Roaring Bitmap。

  1. Roaring Bitmap首先會根據每個id的高16位分配id到對應的block裡面,比如第一個block裡面id應該都是在0到65535之間,第二個block的id在65536和131071之間
  2. 對於每一個block裡面的資料,根據id數量分成兩類
    • 如果數量小於4096,就是用short陣列儲存
    • 數量大於等於4096,就使用bitmap儲存

在每一個block裡面,一個數字實際上只需要2個位元組來儲存就行了,因為高16位在這個block裡面都是相同的,高16位就是block的id,block id和文件的id都用short儲存。

cache

至於4096這個分界線,因為當數量小於4096的時候,如果用bitmap就需要8kB的空間,而使用2個位元組的陣列空間消耗就要少一點。比如只有2048個值,每個值2位元組,一共只需要4kB就能儲存,但是bitmap需要8kB。

總結

ES為了提高搜尋效率、優化儲存空間做了很多工作。

為了能夠快速定位到目標文件,ES使用倒排索引技術來優化搜尋速度,雖然空間消耗比較大,但是搜尋效能提高十分顯著。

由於索引數量巨大,ES無法直接把全部索引放入記憶體,轉而建立詞典索引,構建有限狀態轉換器(FST)放入記憶體,進一步提高搜尋效率。

資料文件的id在詞典內的空間消耗也是巨大的,ES使用了索引幀(Frame of Reference)技術壓縮posting list,帶來的壓縮效果十分明顯。

ES的filter語句採用了Roaring Bitmap技術來快取搜尋結果,保證高頻filter查詢速度的同時降低儲存空間消耗。

參考

MySQL和Lucene(Elasticsearch)索引對比分析
Elasticsearch from the Bottom Up, Part 1
時間序列資料庫的祕密 (2)——索引
Frame of Reference and Roaring Bitmaps
Using Finite State Transducers in Lucene
Lucene's FuzzyQuery is 100 times faster in 4.0

相關文章