1. 程式人生 > 實用技巧 >ElasticSearch索引核心原理

ElasticSearch索引核心原理

FST有窮狀態轉換器:

  Finite StateTransducers 簡稱 FST,通常中文譯作有窮狀態轉換器或者有限狀態感測器

  FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output。FST是一項將一個位元組序列對映到block塊的技術

  假設我們現在要將mop, moth, pop, star, stop and top(term index裡的term字首)對映到序號:0,1,2,3,4,5(term dictionary的block位置)。最簡單的做法就是定義個Map<string, integer="">,

  大家找到自己的位置對應入座就好了,但從記憶體佔用少的角度想想,有沒有更優的辦法呢?答案就是:FST。

  ⭕(上圖的大圈圈) 表示一種狀態,-->表示狀態的變化過程,上面的字母/數字表示狀態變化和權重,將單詞分成單個字母通過⭕ 和-->表示出來,0權重不顯示。如果⭕ 後面出現分支,就標記權重,最後整條路徑上的權重加起來就是這個單詞對應的序號。當遍歷上面的每一條邊的時候,都會加上這條邊的輸出,比如當輸入是 stop 的時候會經過 s/3 和o/1 ,相加得到的排序的順序是 4 ;而對於 mop ,得到的排序的結果是 0但是這個樹並不會包含所有的term,而是很多term(分詞)的字首,通過這些字首快速定位到這個字首所屬的磁碟的block,再從這個block去找文件列表。為了壓縮詞典的空間,實際上每個block都只會儲存block內不同的部分,比如 mop 和 moth 在同一個以 mo 開頭的block,那麼在對應的詞典裡面只會儲存 p 和th ,這樣空間利用率提高了一倍。

  使用有限狀態轉換器在記憶體消耗上面要比遠比 SortedMap 要少,但是在查詢的時候需要更多的CPU資源。維基百科的索引就是使用的FST,只使用了69MB的空間,花了大約8秒鐘,就為接近一千萬個詞條建立了索引,使用的堆空間不到256MB。現在已經把詞典壓縮成了詞條索引,尺寸已經足夠小到放入記憶體,通過索引能夠快速找到文件列表。現在又有另外一個問題,把所有的文件的id放入磁碟中會不會佔用了太多空間?如果有一億個文件,每個文件有10個欄位,為了儲存這個posting list就需要消耗十億個integer的空間,磁碟空間的消耗也是巨大的,ES採用了一個更加巧妙的方式來儲存所有的 id。

Frame Of Reference(索引幀):

  增量編碼壓縮,將大數變小數,按位元組儲存

  Elasticsearch裡除了上面說到用FST壓縮 term index(分詞索引)外,對posting list(文件ID列表)也有壓縮技巧。posting list不是已經只儲存文件id了嗎?還需要壓縮?我們再看以下的例子,如果Elasticsearch需要對同學的性別進行索引會怎樣?

  如果男同學和女同學數量很接近,傳統關係型資料庫針對性別列的索引是不會起到作用,如果差距大,還是會走索引的。

  如果有上千萬個同學,而世界上只有男/女這樣兩個性別,每個posting list都會有至少百萬個文件id。Elasticsearch是如何有效的對這些文件id壓縮的呢?

  在進行查詢的時候經常會進行組合查詢,比如查詢同時包含man和woman的文件,那麼就需要分別查出包含這兩個單詞的文件的id,然後取這兩個id列表的交集;如果是查包含man或者woman的文件,那麼就需要分別查出posting list然後取並集。為了能夠高效的進行交集和並集的操作。為了方便壓縮,Elasticsearch要求posting list是有序的(為了提高搜尋的效能,再任性的要求也得滿足)。同時為了減小儲存空間,所有的id都會進行delta編碼。

  比如現在有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),壓縮過程如下

  這種壓縮演算法的原理就是通過增量,將原來的大數變成小數僅儲存增量值,再精打細算按bit排好隊,最後通過位元組儲存,而不是大大咧咧的儘管是2也是用int(4個位元組)來儲存。

  在返回結果的時候,其實也並不需要把所有的資料直接解壓然後一股腦全部返回,可以直接返回一個迭代器 iterator ,直接通過迭代器的 next 方法逐一取出壓縮的id,這樣也可以極大的節省計算和記憶體開銷。通過以上的方式可以極大的節省posting list的空間消耗,提高查詢效能。不過ES為了提高filter過濾器查詢的效能,還做了更多的工作,那就是快取。

快取技巧之Roaring Bitmaps 咆哮點陣圖:

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

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

  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需要打破這個魔咒就一定要用到某些指數特性.

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

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

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

  由此見得,Elasticsearch使用的倒排索引確實比關係型資料庫的B-Tree索引快。

  注意:一個Lucene索引(也就是一個elasticsearch分片)不能處理多於21億篇文件,或者多於2740億的唯一詞條。但達到這個極限之前,我們可能就沒有足夠的磁碟空間了!

倒排索引如何做聯合索引:

  如果多個field索引的聯合查詢,倒排索引如何滿足快速查詢的要求呢?利用跳錶(Skip list)的資料結構快速做“與”運算,或者利用上面提到的bitset按位“與”。先看看跳錶的資料結構:

  將一個有序連結串列level0,挑出其中幾個元素到level1及level2,每個level越往上,選出來的指標元素越少,查詢時依次從高level往低查詢,比如45,先找到level2的25,最後找到45,查詢效率和2叉樹的效率相當,但也是用了一定的空間冗餘來換取的。

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

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

  如果使用bitset(基於bitMap),就很直觀了,直接按位與,得到的結果就是最後的交集。注意,這是我們倒排索引實現聯合索引的方式,不是我們ES就是這樣操作的。

總結和思考:

  Elasticsearch的索引思路:將磁盤裡的東西儘量搬進記憶體,減少磁碟隨機讀取次數(同時也利用磁碟順序讀特性),結合各種奇技淫巧的壓縮演算法,用及其苛刻的態度使用記憶體。

  所以,對於使用Elasticsearch進行索引時需要注意:

  1. 不需要索引的欄位,一定要明確定義出來,因為預設是自動建索引的
  2. 同樣的道理,對於String型別的欄位,不需要analysis(分詞)的也需要明確定義出來,因為預設也是會analysis的
  3. 選擇有規律的ID很重要,隨機性太大的ID(比如java的UUID)不利於查詢

  關於最後一點,有多個因素:,其中一個(也許不是最重要的)因素: 上面看到的壓縮演算法,都是對Posting list裡的大量ID進行壓縮的,那如果ID是順序的,或者是有公共字首等具有一定規律性的ID,壓縮比會比較高;另外一個因素: 可能是最影響查詢效能的,應該是最後通過Posting list裡的ID到磁碟中查詢Document資訊的那步,因為Elasticsearch是分Segment儲存的,根據ID這個大範圍的Term定位到Segment的效率直接影響了最後查詢的效能,如果ID是有規律的,可以快速跳過不包含該ID的Segment,從而減少不必要的磁碟讀次數,具體可以參考我們的課程,如何選擇一個高效的全域性ID方案。