1. 程式人生 > 其它 >Elasticsearch底層原理基本解析

Elasticsearch底層原理基本解析

基礎概念:

Elasticsearch是一個基於Apache Lucene全文搜尋引擎開發的分散式的 RESTful 風格的的實時搜尋與資料分析引擎,它比Lucene更強大,並且是開源的。官方網站:https://www.elastic.co/cn/

Elasticsearch是面向文件型資料庫,一條資料就是一個文件,和資料結構mongoDB類似,文件序列化之後是JSON格式,例如一條使用者資料:

{
    "name":"tino",
    "age":"25",
    "department":"DC",
    "hobies":[
        "sports",
        "music",
        "movie"
    ]
}

相當於oracle/mysql資料庫中的一張user表中的一條記錄,這user表有name、age、department、hobies欄位,而在Elasticsearch中,這是一個文件,而user則是整個文件的一個型別,Elasticsearch和關係型資料庫術語的對照基本上是:

Elasticsearch 使用的是標準的 RESTful API 和 JSON,它的互動可以通過HTTP請求,也可以通過java API,如果插入一條記錄,可以傳送一個HTTP請求來實現:

POST /user/add
{
    "name":"tino",
    "age":"25",
    "department":"DC",
    "hobies":[
        "sports",
        "music",
        "movie"
    ]
}

更新和查詢也是類似的操作。

基本特徵:

  1. 實現了用於全文檢索的倒排索引,實現了用於儲存數值資料和位置資料的 BKD 樹, 以及用於分析的列儲存。
  2. 將每個欄位編入索引,使其可搜尋,提高搜尋速度。
  3. 實時分析的分散式引擎,確保故障時仍安全可用。
  4. 可以在承載了 PB (2的50次方個位元組,約為1000個TB)級資料的成百上千臺伺服器上執行。
  5. 可處理多種資料型別,數字、文字、地理位置、結構化、非結構化。

倒排索引:

Elasticsearch最強大的就是為每個欄位提供了倒排索引,當查詢的時候不用擔心沒有索引可以利用,什麼是倒排索引,舉個簡單例子:

文件ID

年齡

性別

1

25

2

32

3

25


每一行是一個文件(document),每個document都有一個文件ID。那麼給這些文件建立的倒排索引就是:

年齡的索引:

25

[1,3]

32

[2]

性別的索引:

[1,2]

[3]

可以看到,倒排索引是針對每個欄位的,每個欄位都有自己的倒排索引,25、32這些叫做term,[1,3]這種叫做posting list,

它是一個int的陣列,儲存了所有符合某個term的文件id,這時候我們想找出年齡為25的人,就會很快速,但是這裡只有兩個term,如果有成百上千個term呢,那找出某個term就會很慢,因為term還沒有排序,解決這個問題需要了解兩個概念:Term Dictionary 和 Term Index。

Term Dictionary

Elasticsearch為了能快速找到某個term,將所有的term進行了排序,然後二分法查詢term,類似於上學時候老師教我們的翻新華字典的方式,所以這叫做Term Dictionary,這種查詢方式其實和傳統關係型資料庫的B-Tree的方式很相似,所以這並不是Elasticsearch快的原因。

Term Index

如果說Term Dictionary是直接去二分法翻字典,那麼Term Index就是字典的目錄頁(當然這比我們真的去翻字典目錄快多了),假設我們的term如果全是英文單詞,那麼Term Index就是26個字母表,但是通常term未必都是英文,而可以是任意的byte陣列。因為就算26個英文字元也不一定都有對應的term,比如:a開頭的term只有一個,c開頭的term有一百萬個,x開頭的term一個也沒有,這樣查詢到c的時候又會很慢了。所以通常情況下Term Index 是包含term的一些字首的一棵樹,例如這樣的一個Term Index:

這樣的情況下通過Term Index據可以快速定位到某個offset(分支的開端),然後以此位置向下查詢,再加上FST(Finite-State Transducer,Lucene4.0開始使用該演算法來查詢Term 在Dictionary中的位置)的壓縮技術,將Term Index 快取到記憶體中,通過Term Index 找到對應的Term Dictionary的 block,然後再去磁碟直接找到term,減少磁碟的隨機讀寫次數,大大的提升查詢效率。(FST在下個章節單獨介紹)

Posting List壓縮技術 Roaring Bitmap

什麼是bitmap?

  1. 談到Roaring Bitmap就要先了解bitmap或者BitSet(java中的BitSet實現就是用了bitmap的方式)。
  2. bitmap是一種資料結構,與Posting List 的對應關係如下:
  3. postingList=[2,3,5,7,9]
     
    bitmap=[0,1,1,0,1,0,1,0,1,0]
  4. 從上可以看出來Bitset是用0和1來表示該位置的數值的有無,這種做法就是一個byte可以代表8個文件,不過當大資料量時,仍然會消耗很多記憶體,所以直接將bitset結構存入記憶體也是不太理想的,所以出現了壓縮率更高的Roaring Bitmap。
  5. Elasticsearch不僅壓縮了Term Index,還對posting list 進行了壓縮,posting list雖然只儲存了文件id,但是當文件id很大的時候,比PB級的資料,Elasticsearch對posting list的壓縮做了兩件事:排序和大數變小數,引用一張被引用無數次的圖:
  6. 簡單解讀一下這種壓縮技巧:
    1. step1:在對posting list進行壓縮時進行了正序排序。
    2. step2:通過增量將73後面的大數變成小數儲存增量值。
    3. step3: 轉換成二進位制,取佔最大位的數,227佔8位,前三個佔八位,30佔五位,後三個數每個佔五位。
  7. 從第三步可以看出,這種壓縮方式仍然不夠高效,所以Lucene使用的資料結構叫做Roaring Bitmap,其壓縮的原理可以理解為,與其儲存100個0,佔用100個bit,還不如儲存0一次,然後宣告這個0有100個,它以兩個自己可以表示的最大數65535為界,將posting list分塊,比如第一塊是0-65535,第二塊是65536-131071,如圖:

壓縮技巧解讀:

step1:從小到大進行排序。
step2:將大數除以65536,用除得的結果和餘數來表示這個大數。
step3::以65535為界進行分塊。
注意:如果一塊超過了4096 個值,直接用bitset存,2個位元組就用個簡單的陣列存放好了,比如short[],修正一下:1KB=1024B=1024byte=8192bit,每個值一個bytes,4096*2bytes=8192bytes,剛好達到每一個block的界限4096 = 65536 / 2 / 8

聯合索引:

如何使用聯合索引查詢?

  1. Skip List 資料結構,同時遍歷多個term的posting list,互相skip
  2. 使用bitset資料結構,對多個term分別求出bitset,對bitset做AN操作

Skip List 跳錶原理

先了解跳錶需要先知道跳錶應該具有以下性質:

  1. 由多層有序連結串列組成。
  2. 最底層Level 1的連結串列包含所有的其他連結串列的元素。
  3. 如果一個元素在連結串列Level n中存在,那麼他在Level n以下的所有連結串列中都存在。
  4. 每個節點都包含連個指標,分別指同Level連結串列的下一個元素和下一層的元素。

這是一個有序列連結串列:

從連結串列中搜索(27,44,61)需要查詢的次數為:2+4+6 = 12次,可以得到所有結果,這樣做其實沒有用到連結串列的有序性,我們在查詢44、66時候其實都做了一些重複查詢,勢必會造成效率低的問題,這時候可以用Skip List演算法來優化查詢次數,把某些節點提取出來,將連結串列分成兩級:

這樣我們在查詢44和61的時候次數就得到了簡化,因為列表時有序的,所以當我們查到27的時候,44的大概位置就知道了,查到44的時候,61的大概位置也就知道了,避免了一部分重複查查詢,這時候我們找到所以結果的次數為2+3+4 = 9次,查詢61的時候似乎又對44的查詢重複了一次,似乎還有優化的空間,那我們再對二級連結串列再進行一次分級:

這時候可以看到我們查詢61的時候只需要從15-50-61,三次就可以查詢到結果,沒有去走查詢44時的36,所以現在查詢次數得到了再次優化,也就是2+3+3 = 8次,有人說:就優化了一次查詢,需要做得這麼複雜嗎,能有那麼大的效能優勢嗎?的確,這裡資料量很少,組合查詢的索引也只有三個,看起來確實沒有優勢,但是當面對PB級資料的時候,連結串列的分級將無限擴大大,我們在查詢結果時所"跳" 的跨度也會非常大,原先需要查詢一百萬次的結果,可能僅僅三次就查詢到了,這個時候就會顯得非常高效,從Skip List的查詢原理可以看出,它的高效其實是犧牲了一定的空間冗餘換來的,所以在有些情況下還是使用bitset更加的直觀,比如下面這種資料結構:

111 222 333 444
111 222 333 444 555 666 777
111 222 333 444 555 666 777 888

如果使用跳錶,查詢第一行資料在另外兩行中查詢看是否存在,為了得到最後得到交集的結果,操作就要繁瑣的多。

如果使用bitset直接進行壓縮,按位與,得到的結果就是最後的交集。

轉載自:Elasticsearch底層原理基本解析_Tino's Space-CSDN部落格