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方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章