目錄
基礎概念:
Elasticsearch是一個基於Apache Lucene全文搜索引擎開發的分佈式的 RESTful 風格的的實時搜索與數據分析引擎,它比Lucene更強大,並且是開源的。官方網站:https://www.elastic.co/cn/
Elasticsearch是面向文檔型數據庫,一條數據就是一個文檔,文檔序列化之後是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"
]
}
更新和查詢也是類似的操作。
基本特徵:
- 實現了用於全文檢索的倒排索引,實現了用於存儲數值數據和位置數據的 BKD 樹, 以及用於分析的列存儲。
- 將每個字段編入索引,使其可搜索,提高搜索速度。
- 實時分析的分佈式引擎,確保故障時仍安全可用。
- 可以在承載了 PB (2的50次方個字節,約爲1000個TB)級數據的成百上千臺服務器上運行。
- 可處理多種數據類型,數字、文本、地理位置、結構化、非結構化。
倒排索引:
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
談到roaring bitmaps就要先了解bitset 或者bitmap,Bitset是一種的數據結構,對應posting list如果是:[2,3,5,7,9] 那麼對於對應的bitset就是:[0,1,1,0,1,0,1,0,1,0],用0和1來表示該位置的數值的有無,這種做法就是一個byte可以代表8個文檔,當大數據量時,仍然會消耗很多內存,所以直接將bitset結構存入內存不太理想。
Elasticsearch不僅壓縮了Term Index,還對posting list 進行了壓縮,posting list雖然只存儲了文檔id,但是當文檔id很大的時候,比PB級的數據,Elasticsearch對posting list的壓縮做了兩件事:排序和大數變小數,引用一張被引用無數次的圖:
簡單解讀一下這種壓縮技巧:
- step1:在對posting list進行壓縮時進行了正序排序。
- step2:通過增量將73後面的大數變成小數存儲增量值。
- step3: 轉換成二進制,取佔最大位的數,227佔8位,前三個佔八位,30佔五位,後三個數每個佔五位。
從第三步可以看出,這種壓縮方式仍然不夠高效,所以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個字節就用個簡單的數組存放好了,比如shor[]。1KB=1024B=1024byte=8192bit,每個值一個bit,4096*2bit = 8192bit,剛好控制在1KB以內,磁盤尋道的單位是KB,超過1KB將再讀一次,這樣可以減少磁盤讀取次數。
聯合索引:
如何使用聯合索引查詢?
- Skip List 數據結構,同時遍歷多個term的posting list,互相skip
- 使用bitset數據結構,對多個term分別求出bitset,對bitset做AN操作
Skip List 跳錶原理
先了解跳錶需要先知道跳錶應該具有以下性質:
- 由多層有序鏈表組成。
- 最底層Level 1的鏈表包含所有的其他鏈表的元素。
- 如果一個元素在鏈表Level n中存在,那麼他在Level n以下的所有鏈表中都存在。
- 每個節點都包含連個指針,分別指同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直接進行壓縮,按位與,得到的結果就是最後的交集。