ElasticSearch探索之路(四)索引原理:倒排索引、列式存储、Fielddata、索引压缩、联合索引
文章目錄
- 倒排索引
- Term dictionary與Term index
- 列式存儲(chǔ)——Doc Values
- Fielddata
- 索引壓縮
- FOR編碼
- Roaring Bitmaps
- 聯(lián)合索引
倒排索引
例如,假設(shè)我們有兩個(gè)文檔,每個(gè)文檔的 content 域包含如下內(nèi)容:
為了創(chuàng)建倒排索引,首先我們需要借助分詞器,將每個(gè)文檔的 content 域拆分成單獨(dú)的詞(我們稱它為 詞條 或 tokens、term ),創(chuàng)建一個(gè)包含所有不重復(fù)詞條的排序列表,然后列出每個(gè)詞條出現(xiàn)在哪個(gè)文檔。
Term Doc_1 Doc_2 ------------------------- Quick | | X The | X | brown | X | X dog | X | dogs | | X fox | X | foxes | | X in | | X jumped | X | lazy | X | X leap | | X over | X | X quick | X | summer | | X the | X | ------------------------如果我們想搜索 quick brown ,我們只需要查找包含每個(gè)詞條的文檔:
Term Doc_1 Doc_2 ------------------------- brown | X | X quick | X | ------------------------ Total | 2 | 1這里我們匹配到了兩個(gè)文檔(為了節(jié)省空間,這里返回的只是文檔ID,最后再通過(guò)文檔id去查詢到具體文檔。)。對(duì)于搜索引擎來(lái)說(shuō),用戶總希望能夠先看到相關(guān)度更高的結(jié)果,因此實(shí)際使用時(shí)我們通過(guò)一些算法來(lái)進(jìn)行權(quán)重計(jì)算,將查詢的結(jié)果按照權(quán)重降序返回。
Term dictionary與Term index
Elasticsearch為了能夠快速的在倒排索引中找到某個(gè)term,他會(huì)按照字典序對(duì)所有的term進(jìn)行排序,再通過(guò)二分查找來(lái)找到term,這就是Term Dictionary,但即使有了Term Dictionary,O(logN)的磁盤讀寫仍然是影響性能的一大問(wèn)題。
B-Tree通過(guò)減少磁盤尋道次數(shù)來(lái)提高查詢性能,Elasticsearch也是采用同樣的思路,直接通過(guò)內(nèi)存查找term,不讀磁盤,但是如果term太多,term dictionary也會(huì)很大,放內(nèi)存不現(xiàn)實(shí),于是有了Term Index,從下圖可以看出,Term Index其實(shí)就是一個(gè)Trie樹(前綴樹)
Term index這棵樹不會(huì)包含所有的term,它包含的是term的一些前綴。通過(guò)term index可以快速地定位到term dictionary的某個(gè)offset,然后從這個(gè)位置再往后順序查找。
查找流程為什么Elasticsearch/Lucene檢索比mysql快呢?
Mysql只有term dictionary這一層,是以b-tree排序的方式存儲(chǔ)在磁盤上的。檢索一個(gè)term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎(chǔ)上添加了term index來(lái)加速檢索,term index以樹的形式緩存在內(nèi)存中。從term index查到對(duì)應(yīng)的term dictionary的block位置之后,再去磁盤上找term,大大減少了磁盤的random access次數(shù)。
列式存儲(chǔ)——Doc Values
Doc values的存在是因?yàn)榈古潘饕粚?duì)某些操作是高效的。 倒排索引的優(yōu)勢(shì)在于查找包含某個(gè)項(xiàng)的文檔,而對(duì)于從另外一個(gè)方向的相反操作并不高效,即:確定哪些項(xiàng)是否存在單個(gè)文檔里,聚合需要這種次級(jí)的訪問(wèn)模式。
以排序來(lái)舉例——雖然倒排索引的檢索性能非常快,但是在字段值排序時(shí)卻不是理想的結(jié)構(gòu)。
- 在搜索的時(shí)候,我們能通過(guò)搜索關(guān)鍵詞快速得到結(jié)果集。
- 當(dāng)排序的時(shí)候,我們需要倒排索引里面某個(gè)字段值的集合。換句話說(shuō),我們需要轉(zhuǎn)置倒排索引。
轉(zhuǎn)置 結(jié)構(gòu)經(jīng)常被稱作 列式存儲(chǔ) 。它將所有單字段的值存儲(chǔ)在單數(shù)據(jù)列中,這使得對(duì)其進(jìn)行操作是十分高效的,例如排序、聚合等操作。
在Elasticsearch中,Doc Values就是一種列式存儲(chǔ)結(jié)構(gòu),在索引時(shí)與倒排索引同時(shí)生成。也就是說(shuō)Doc Values和倒排索引一樣,基于 Segement生成并且是不可變的。同時(shí)Doc Values和倒排索引一樣序列化到磁盤。
Doc Values常被應(yīng)用到以下場(chǎng)景:
- 對(duì)一個(gè)字段進(jìn)行排序
- 對(duì)一個(gè)字段進(jìn)行聚合
- 某些過(guò)濾,比如地理位置過(guò)濾
- 某些與字段相關(guān)的腳本計(jì)算
下面舉一個(gè)例子,來(lái)講講它是如何運(yùn)作的
假設(shè)存在以下倒排索引
Term Doc_1 Doc_2 Doc_3 ------------------------------------ brown | X | X | dog | X | | X dogs | | X | X ------------------------------------那么其生成的DocValues如下(實(shí)際存儲(chǔ)時(shí)不會(huì)存儲(chǔ)doc_id,值所在的順位即為doc_id)
Doc_id Values ------------------ Doc_1 | brown | Doc_1 | dog | Doc_2 | brown | Doc_2 | dogs | Doc_3 | dog | Doc_3 | dogs | ------------------假設(shè)我們需要計(jì)算出brown出現(xiàn)的次數(shù)
GET /my_index/_search {"query":{"match":{"body":"brown"}},"aggs":{"popular_terms":{"terms":{"field":"body"}}},"size" : 0 }下面來(lái)分析上述請(qǐng)求在ES中是如何來(lái)進(jìn)行查詢的:
但是doc_values仍存在一些問(wèn)題,其不支持analyzed類型的字段,因?yàn)檫@些字段在進(jìn)行文本分析時(shí)可能會(huì)被分詞處理,從而導(dǎo)致doc_values將其存儲(chǔ)為多行記錄。但是在我們實(shí)際使用時(shí),為什么仍然能對(duì)analyzed的字段進(jìn)行聚合操作呢?這時(shí)就需要介紹一下Fielddata
Fielddata
doc values不生成分析的字符串,那為什么這些字段仍然可以使用聚合呢?是因?yàn)槭褂昧薴ielddata的數(shù)據(jù)結(jié)構(gòu)。與doc values不同,fielddata構(gòu)建和管理100%在內(nèi)存中,常駐于JVM內(nèi)存堆。
從歷史上看,fielddata 是所有字段的默認(rèn)設(shè)置。但是Elasticsearch已遷移到doc values以減少 OOM 的幾率。分析的字符串是仍然使用fielddata的最后一塊陣地。 最終目標(biāo)是建立一個(gè)序列化的數(shù)據(jù)結(jié)構(gòu)類似于doc values ,可以處理高維度的分析字符串,逐步淘汰 fielddata。
它的一些特性如下
- 延遲加載。如果你從來(lái)沒(méi)有聚合一個(gè)分析字符串,就不會(huì)加載fielddata到內(nèi)存中,其是在查詢時(shí)候構(gòu)建的。
- 基于字段加載。 只有很活躍地使用字段才會(huì)增加fielddata的負(fù)擔(dān)。
- 會(huì)加載索引中(針對(duì)該特定字段的) 所有的文檔,而不管查詢是否命中。邏輯是這樣:如果查詢會(huì)訪問(wèn)文檔 X、Y 和 Z,那很有可能會(huì)在下一個(gè)查詢中訪問(wèn)其他文檔。
- 如果空間不足,使用最久未使用(LRU)算法移除fielddata。
因此,在聚合字符串字段之前,請(qǐng)?jiān)u估情況:
- 這是一個(gè)not_analyzed字段嗎?如果是,可以通過(guò)doc values節(jié)省內(nèi)存 。
- 否則,這是一個(gè)analyzed字段,它將使用fielddata并加載到內(nèi)存中。這個(gè)字段因?yàn)镹-grams有一個(gè)非常大的基數(shù)?如果是,這對(duì)于內(nèi)存來(lái)說(shuō)極度不友好。
索引壓縮
FOR編碼
在Elasticsearch中,為了能夠更方便的計(jì)算交集和并集,它要求倒排索引是有序的,而這個(gè)特點(diǎn)同時(shí)也帶來(lái)了一個(gè)額外的好處,我們可以使用增量編碼來(lái)壓縮倒排索引,也就是FOR(Frame of Reference)編碼
增量編碼壓縮,將大數(shù)變小數(shù),按字節(jié)存儲(chǔ)
FOR編碼分為三個(gè)步驟
- 增量編碼
- 增量分區(qū)
- 位壓縮
如下圖所示,如果我們的倒排索引中存儲(chǔ)的文檔id為[73, 300, 302, 332, 343, 372],那么經(jīng)過(guò)增量編碼后的結(jié)果則為[73, 227, 2, 30, 11, 29]。這種壓縮的好處在哪里呢?我們通過(guò)增量將原本的大數(shù)變成了小數(shù),使得所有的增量都在0~255之間,因此每一個(gè)值就只需要使用一個(gè)字節(jié)就可以存儲(chǔ),而不會(huì)使用int或者bigint,大大的節(jié)約了空間。
接著,第二步我們將這些增量分到不同的區(qū)塊中(Lucene底層用了256個(gè)區(qū)塊,下面為了方便展示之用了兩個(gè))。
第三步,我們計(jì)算出每組數(shù)據(jù)中最大的那個(gè)數(shù)所占用的bit位數(shù),例如下圖中區(qū)塊1最大的為227,所以只占用8個(gè)bit位,所以三個(gè)數(shù)總共占用3 * 8bits即3字節(jié)。而區(qū)塊2最大為29,只占用5個(gè)bit位,因此這三個(gè)數(shù)總共占用3 * 5bits即差不多2字節(jié)。通過(guò)這種方法,將原本6個(gè)整數(shù)從24字節(jié)壓縮到了5字節(jié),效果十分出色。
ROF編碼實(shí)例Roaring Bitmaps
FOR編碼對(duì)于倒排索引來(lái)說(shuō)效果很好,但對(duì)于需要存儲(chǔ)在內(nèi)存中的過(guò)濾器緩存等不太合適,兩者之間有很多不同之處:
- 由于我們僅僅緩存那些經(jīng)常使用的過(guò)濾器,因此它的壓縮率并不需要像倒排索引那么高(倒排索引需要對(duì)每個(gè)詞都進(jìn)行編碼)。
- 緩存過(guò)濾器的目的就是為了加速處理效率,因此它必須要比重新執(zhí)行過(guò)濾器要快,因此使用一個(gè)好的數(shù)據(jù)結(jié)構(gòu)和算法非常重要。
- 緩存的過(guò)濾器存儲(chǔ)在內(nèi)存之中,而倒排索引通常存儲(chǔ)在磁盤中。
基于以上的不同,對(duì)于緩存來(lái)說(shuō)FOR編碼并不適用,因此我們還需要考慮其他的一些選擇。
- 整數(shù)數(shù)組:數(shù)組可能是我們馬上能想到的最簡(jiǎn)單的實(shí)現(xiàn)方式,我們將文檔id存儲(chǔ)在數(shù)組中,這樣就使得我們的迭代變得非常簡(jiǎn)單,但是這種方法的內(nèi)存利用率又十分低下,因?yàn)槊總€(gè)文檔都需要4個(gè)字節(jié)。
- Bitmaps:在數(shù)據(jù)分布密集的下,位圖是一個(gè)很好的選擇。它本質(zhì)上就是一個(gè)數(shù)組,其中每一個(gè)文檔id占據(jù)一個(gè)位,用0和1來(lái)標(biāo)記文檔是否存在。這種方法大大節(jié)約了內(nèi)存,將一個(gè)文檔從4字節(jié)降低到了一個(gè)位,但是一旦數(shù)據(jù)分布稀疏,此時(shí)的位圖性能將大打折扣,因?yàn)闊o(wú)論數(shù)據(jù)量多少,位圖的大小都是由數(shù)據(jù)的上下區(qū)間來(lái)決定的。
- Roaring Bitmaps:Roaring Bitmaps即是對(duì)位圖的一種優(yōu)化,它會(huì)根據(jù)16位最高位將倒排索引劃分為多塊,如第一個(gè)塊將對(duì)0到65535之間的值進(jìn)行編碼,第二個(gè)塊將在65536和131071之間進(jìn)行編碼。在每一個(gè)塊中,我們?cè)賹?duì)低16位進(jìn)行編碼,如果它的值小于4096在使用數(shù)組,否則就使用位圖。由于我們編碼的時(shí)候只會(huì)對(duì)低16位進(jìn)行編碼,因此在這里數(shù)組每個(gè)元素只需要2個(gè)字節(jié)
為什么要使用4096作為數(shù)組和位圖選取的閾值呢?
下面是官方給出的數(shù)據(jù)報(bào)告,在一個(gè)塊中只有文檔數(shù)量超過(guò)4096,位圖的內(nèi)存優(yōu)勢(shì)才會(huì)凸顯出來(lái)
位圖與數(shù)組內(nèi)存利用對(duì)比這就是Roaring Bitmaps高效率的原因,它基于兩種完全不同的方案來(lái)進(jìn)行編碼,并根據(jù)內(nèi)存效率來(lái)動(dòng)態(tài)決定使用哪一種方案。
官方也給出了幾種方案的性能測(cè)試
迭代性能 跳過(guò)性能——稀疏集 跳過(guò)性能——密集集 內(nèi)存占用 構(gòu)造時(shí)間從上述對(duì)比可以看出,沒(méi)有一種方法是完美的,但是以下兩種方法的巨大劣勢(shì)使得它們不會(huì)被選擇
- 數(shù)組:性能很好,但是內(nèi)存占用巨大。
- Bitmaps:數(shù)據(jù)稀疏分布的時(shí)候內(nèi)存和性能都會(huì)大打折扣。
因此在綜合考量下,Elasticsearch還是選擇使用Roaring Bitmaps,并且在很多大家了解的開源大數(shù)據(jù)框架中,也都使用了這一結(jié)構(gòu),如Hive、Spark、Kylin、Druid等。
聯(lián)合索引
如果多個(gè)字段索引的聯(lián)合查詢,倒排索引如何滿足快速查詢的要求呢?
- 跳表:同時(shí)遍歷多個(gè)字段的倒排索引,互相skip。
- 位圖:對(duì)多個(gè)過(guò)濾器分別求出位圖,對(duì)這幾個(gè)位圖做AND操作。
Elasticsearch支持以上兩種的聯(lián)合索引方式,如果查詢的過(guò)濾器緩存到了內(nèi)存中(以位圖的形式),那么合并就是兩個(gè)位圖的AND。如果查詢的過(guò)濾器沒(méi)有緩存,那么就用跳表的方式去遍歷兩個(gè)硬盤中的倒排索引。
假設(shè)有下面三個(gè)倒排索引需要聯(lián)合索引:
假設(shè)有三個(gè)倒排索引- 如果使用跳表,則對(duì)最短的倒排索引中的每一個(gè)id,逐個(gè)在另外兩個(gè)倒排索引中查看是否存在,來(lái)判斷是否存在交集。
- 如果使用位圖,則直接將幾個(gè)位圖按位與運(yùn)算,最終得到的結(jié)果就是最后的交集。
總結(jié)
以上是生活随笔為你收集整理的ElasticSearch探索之路(四)索引原理:倒排索引、列式存储、Fielddata、索引压缩、联合索引的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ElasticSearch探索之路(三)
- 下一篇: ElasticSearch探索之路(五)