elasticsearch 倒排索引原理

如何快速檢索色建?

Elasticsearch是通過(guò)Lucene的倒排索引技術(shù)實(shí)現(xiàn)比關(guān)系型數(shù)據(jù)庫(kù)更快的過(guò)濾及穗。特別是它對(duì)多條件的過(guò)濾支持非常好,比如年齡在18和30之間暑诸,性別為女性這樣的組合查詢(xún)棘捣。倒排索引很多地方都有介紹抛杨,但是其比關(guān)系型數(shù)據(jù)庫(kù)的b-tree索引快在哪里耘眨?到底為什么快呢像云?

籠統(tǒng)的來(lái)說(shuō)剂癌,b-tree索引是為寫(xiě)入優(yōu)化的索引結(jié)構(gòu)淤翔。當(dāng)我們不需要支持快速的更新的時(shí)候,可以用預(yù)先排序等方式換取更小的存儲(chǔ)空間佩谷,更快的檢索速度等好處旁壮,其代價(jià)就是更新慢。要進(jìn)一步深入的化谐檀,還是要看一下Lucene的倒排索引是怎么構(gòu)成的抡谐。

這里有好幾個(gè)概念。我們來(lái)看一個(gè)實(shí)際的例子桐猬,假設(shè)有如下的數(shù)據(jù):

這里每一行是一個(gè)document麦撵。每個(gè)document都有一個(gè)docid。那么給這些document建立的倒排索引就是:

可以看到溃肪,倒排索引是per field的免胃,一個(gè)字段由一個(gè)自己的倒排索引。18,20這些叫做 term惫撰,而[1,3]就是posting list羔沙。Posting list就是一個(gè)int的數(shù)組,存儲(chǔ)了所有符合某個(gè)term的文檔id厨钻。那么什么是term dictionary 和 term index扼雏?

假設(shè)我們有很多個(gè)term坚嗜,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個(gè)特定的term一定很慢诗充,因?yàn)閠erm沒(méi)有排序惶傻,需要全部過(guò)濾一遍才能找出特定的term。排序之后就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式其障,比全遍歷更快地找出目標(biāo)的term银室。這個(gè)就是 term dictionary。有了term dictionary之后励翼,可以用 logN 次磁盤(pán)查找得到目標(biāo)蜈敢。但是磁盤(pán)的隨機(jī)讀操作仍然是非常昂貴的(一次random access大概需要10ms的時(shí)間)。所以盡量少的讀磁盤(pán)汽抚,有必要把一些數(shù)據(jù)緩存到內(nèi)存里抓狭。但是整個(gè)term dictionary本身又太大了,無(wú)法完整地放到內(nèi)存里造烁。于是就有了term index否过。term index有點(diǎn)像一本字典的大的章節(jié)表。比如:

A開(kāi)頭的term ……………. Xxx頁(yè)

C開(kāi)頭的term ……………. Xxx頁(yè)

E開(kāi)頭的term ……………. Xxx頁(yè)

如果所有的term都是英文字符的話(huà)惭蟋,可能這個(gè)term index就真的是26個(gè)英文字符表構(gòu)成的了苗桂。但是實(shí)際的情況是,term未必都是英文字符告组,term可以是任意的byte數(shù)組煤伟。而且26個(gè)英文字符也未必是每一個(gè)字符都有均等的term,比如x字符開(kāi)頭的term可能一個(gè)都沒(méi)有木缝,而s開(kāi)頭的term又特別多便锨。實(shí)際的term index是一棵trie 樹(shù):

例子是一個(gè)包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹(shù)。這棵樹(shù)不會(huì)包含所有的term我碟,它包含的是term的一些前綴放案。通過(guò)term index可以快速地定位到term dictionary的某個(gè)offset,然后從這個(gè)位置再往后順序查找矫俺。再加上一些壓縮技術(shù)(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一吱殉,使得用內(nèi)存緩存整個(gè)term index變成可能。整體上來(lái)說(shuō)就是這樣的效果恳守。

現(xiàn)在我們可以回答“為什么Elasticsearch/Lucene檢索可以比mysql快了考婴。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲(chǔ)在磁盤(pán)上的催烘。檢索一個(gè)term需要若干次的random access的磁盤(pán)操作沥阱。而Lucene在term dictionary的基礎(chǔ)上添加了term index來(lái)加速檢索,term index以樹(shù)的形式緩存在內(nèi)存中伊群。從term index查到對(duì)應(yīng)的term dictionary的block位置之后考杉,再去磁盤(pán)上找term策精,大大減少了磁盤(pán)的random access次數(shù)。

額外值得一提的兩點(diǎn)是:term index在內(nèi)存中是以FST(finite state transducers)的形式保存的崇棠,其特點(diǎn)是非常節(jié)省內(nèi)存咽袜。Term dictionary在磁盤(pán)上是以分block的方式保存的,一個(gè)block內(nèi)部利用公共前綴壓縮枕稀,比如都是Ab開(kāi)頭的單詞就可以把Ab省去询刹。這樣term dictionary可以比b-tree更節(jié)約磁盤(pán)空間。

如何聯(lián)合索引查詢(xún)萎坷?

所以給定查詢(xún)過(guò)濾條件 age=18 的過(guò)程就是先從term index找到18在term dictionary的大概位置凹联,然后再?gòu)膖erm dictionary里精確地找到18這個(gè)term,然后得到一個(gè)posting list或者一個(gè)指向posting list位置的指針哆档。然后再查詢(xún) gender=女 的過(guò)程也是類(lèi)似的蔽挠。最后得出 age=18 AND gender=女 就是把兩個(gè) posting list 做一個(gè)“與”的合并。

這個(gè)理論上的“與”合并的操作可不容易瓜浸。對(duì)于mysql來(lái)說(shuō)澳淑,如果你給age和gender兩個(gè)字段都建立了索引,查詢(xún)的時(shí)候只會(huì)選擇其中最selective的來(lái)用插佛,然后另外一個(gè)條件是在遍歷行的過(guò)程中在內(nèi)存中計(jì)算之后過(guò)濾掉杠巡。那么要如何才能聯(lián)合使用兩個(gè)索引呢?有兩種辦法:

使用skip list數(shù)據(jù)結(jié)構(gòu)朗涩。同時(shí)遍歷gender和age的posting list忽孽,互相skip绑改;

使用bitset數(shù)據(jù)結(jié)構(gòu)谢床,對(duì)gender和age兩個(gè)filter分別求出bitset,對(duì)兩個(gè)bitset做AN操作厘线。

PostgreSQL 從 8.4 版本開(kāi)始支持通過(guò)bitmap聯(lián)合使用兩個(gè)索引识腿,就是利用了bitset數(shù)據(jù)結(jié)構(gòu)來(lái)做到的。當(dāng)然一些商業(yè)的關(guān)系型數(shù)據(jù)庫(kù)也支持類(lèi)似的聯(lián)合索引的功能造壮。Elasticsearch支持以上兩種的聯(lián)合索引方式渡讼,如果查詢(xún)的filter緩存到了內(nèi)存中(以bitset的形式),那么合并就是兩個(gè)bitset的AND耳璧。如果查詢(xún)的filter沒(méi)有緩存成箫,那么就用skip list的方式去遍歷兩個(gè)on disk的posting list。

利用 Skip List 合并

以上是三個(gè)posting list旨枯。我們現(xiàn)在需要把它們用AND的關(guān)系合并蹬昌,得出posting list的交集。首先選擇最短的posting list攀隔,然后從小到大遍歷皂贩。遍歷的過(guò)程可以跳過(guò)一些元素栖榨,比如我們遍歷到綠色的13的時(shí)候,就可以跳過(guò)藍(lán)色的3了明刷,因?yàn)?比13要小婴栽。

整個(gè)過(guò)程如下

Next -> 2

Advance(2) -> 13

Advance(13) -> 13

Already on 13

Advance(13) -> 13 MATCH!!!

Next -> 17

Advance(17) -> 22

Advance(22) -> 98

Advance(98) -> 98

Advance(98) -> 98 MATCH!!!

最后得出的交集是[13,98],所需的時(shí)間比完整遍歷三個(gè)posting list要快得多辈末。但是前提是每個(gè)list需要指出Advance這個(gè)操作愚争,快速移動(dòng)指向的位置。什么樣的list可以這樣Advance往前做蛙跳挤聘?skip list:

從概念上來(lái)說(shuō)准脂,對(duì)于一個(gè)很長(zhǎng)的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個(gè)list分成三個(gè)block:

[1,3,13] [101,105,108] [255,256,257]

然后可以構(gòu)建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對(duì)應(yīng)的block檬洞。這樣就可以很快地跨block的移動(dòng)指向位置了狸膏。

Lucene自然會(huì)對(duì)這個(gè)block再次進(jìn)行壓縮。其壓縮方式叫做Frame Of Reference編碼添怔。示例如下:

考慮到頻繁出現(xiàn)的term(所謂low cardinality的值)湾戳,比如gender里的男或者女。如果有1百萬(wàn)個(gè)文檔广料,那么性別為男的posting list里就會(huì)有50萬(wàn)個(gè)int值砾脑。用Frame of Reference編碼進(jìn)行壓縮可以極大減少磁盤(pán)占用。這個(gè)優(yōu)化對(duì)于減少索引尺寸有非常重要的意義艾杏。當(dāng)然mysql b-tree里也有一個(gè)類(lèi)似的posting list的東西韧衣,是未經(jīng)過(guò)這樣壓縮的。

因?yàn)檫@個(gè)Frame of Reference的編碼是有解壓縮成本的购桑。利用skip list畅铭,除了跳過(guò)了遍歷的成本,也跳過(guò)了解壓縮這些壓縮過(guò)的block的過(guò)程勃蜘,從而節(jié)省了cpu硕噩。

利用bitset合并

Bitset是一種很直觀(guān)的數(shù)據(jù)結(jié)構(gòu),對(duì)應(yīng)posting list如:

[1,3,4,7,10]

對(duì)應(yīng)的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個(gè)文檔按照文檔id排序?qū)?yīng)其中的一個(gè)bit缭贡。Bitset自身就有壓縮的特點(diǎn)炉擅,其用一個(gè)byte就可以代表8個(gè)文檔。所以100萬(wàn)個(gè)文檔只需要12.5萬(wàn)個(gè)byte阳惹。但是考慮到文檔可能有數(shù)十億之多谍失,在內(nèi)存里保存bitset仍然是很奢侈的事情。而且對(duì)于個(gè)每一個(gè)filter都要消耗一個(gè)bitset莹汤,比如age=18緩存起來(lái)的話(huà)是一個(gè)bitset快鱼,18<=age<25是另外一個(gè)filter緩存起來(lái)也要一個(gè)bitset。

所以秘訣就在于需要有一個(gè)數(shù)據(jù)結(jié)構(gòu):

可以很壓縮地保存上億個(gè)bit代表對(duì)應(yīng)的文檔是否匹配filter;

這個(gè)壓縮的bitset仍然可以很快地進(jìn)行AND和 OR的邏輯操作攒巍。

Lucene使用的這個(gè)數(shù)據(jù)結(jié)構(gòu)叫做 Roaring Bitmap嗽仪。

其壓縮的思路其實(shí)很簡(jiǎn)單。與其保存100個(gè)0柒莉,占用100個(gè)bit闻坚。還不如保存0一次,然后聲明這個(gè)0重復(fù)了100遍兢孝。

這兩種合并使用索引的方式都有其用途窿凤。Elasticsearch對(duì)其性能有詳細(xì)的對(duì)比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡(jiǎn)單的結(jié)論是:因?yàn)镕rame of Reference編碼是如此 高效跨蟹,對(duì)于簡(jiǎn)單的相等條件的過(guò)濾緩存成純內(nèi)存的bitset還不如需要訪(fǎng)問(wèn)磁盤(pán)的skip list的方式要快雳殊。

如何減少文檔數(shù)?

一種常見(jiàn)的壓縮存儲(chǔ)時(shí)間序列的方式是把多個(gè)數(shù)據(jù)點(diǎn)合并成一行窗轩。Opentsdb支持海量數(shù)據(jù)的一個(gè)絕招就是定期把很多行數(shù)據(jù)合并成一行夯秃,這個(gè)過(guò)程叫compaction。類(lèi)似的vivdcortext使用mysql存儲(chǔ)的時(shí)候痢艺,也把一分鐘的很多數(shù)據(jù)點(diǎn)合并存儲(chǔ)到mysql的一行里以減少行數(shù)仓洼。

這個(gè)過(guò)程可以示例如下:

可以看到,行變成了列了堤舒。每一列可以代表這一分鐘內(nèi)一秒的數(shù)據(jù)色建。

Elasticsearch有一個(gè)功能可以實(shí)現(xiàn)類(lèi)似的優(yōu)化效果,那就是Nested Document舌缤。我們可以把一段時(shí)間的很多個(gè)數(shù)據(jù)點(diǎn)打包存儲(chǔ)到一個(gè)父文檔里箕戳,變成其嵌套的子文檔。示例如下:

{timestamp:12:05:01, idc:sz, value1:10,value2:11}

{timestamp:12:05:02, idc:sz, value1:9,value2:9}

{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

{

max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,

records: [

{timestamp:12:05:01, value1:10,value2:11}

{timestamp:12:05:02, value1:9,value2:9}

{timestamp:12:05:02, value1:18,value:17}

]

}

這樣可以把數(shù)據(jù)點(diǎn)公共的維度字段上移到父文檔里国撵,而不用在每個(gè)子文檔里重復(fù)存儲(chǔ)陵吸,從而減少索引的尺寸。

在存儲(chǔ)的時(shí)候卸留,無(wú)論父文檔還是子文檔走越,對(duì)于Lucene來(lái)說(shuō)都是文檔,都會(huì)有文檔Id耻瑟。但是對(duì)于嵌套文檔來(lái)說(shuō),可以保存起子文檔和父文檔的文檔id是連續(xù)的赏酥,而且父文檔總是最后一個(gè)喳整。有這樣一個(gè)排序性作為保障,那么有一個(gè)所有父文檔的posting list就可以跟蹤所有的父子關(guān)系裸扶。也可以很容易地在父子文檔id之間做轉(zhuǎn)換框都。把父子關(guān)系也理解為一個(gè)filter,那么查詢(xún)時(shí)檢索的時(shí)候不過(guò)是又AND了另外一個(gè)filter而已呵晨。前面我們已經(jīng)看到了Elasticsearch可以非常高效地處理多filter的情況魏保,充分利用底層的索引熬尺。

使用了嵌套文檔之后,對(duì)于term的posting list只需要保存父文檔的doc id就可以了谓罗,可以比保存所有的數(shù)據(jù)點(diǎn)的doc id要少很多粱哼。如果我們可以在一個(gè)父文檔里塞入50個(gè)嵌套文檔,那么posting list可以變成之前的1/50檩咱。

原文出處:http://www.infoq.com/cn/articles/database-timestamp-02

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末揭措,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子刻蚯,更是在濱河造成了極大的恐慌绊含,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炊汹,死亡現(xiàn)場(chǎng)離奇詭異躬充,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)讨便,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)麻裳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人器钟,你說(shuō)我怎么就攤上這事津坑。” “怎么了傲霸?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵疆瑰,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我昙啄,道長(zhǎng)穆役,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任梳凛,我火速辦了婚禮耿币,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘韧拒。我一直安慰自己淹接,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布叛溢。 她就那樣靜靜地躺著塑悼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪楷掉。 梳的紋絲不亂的頭發(fā)上厢蒜,一...
    開(kāi)封第一講書(shū)人閱讀 51,708評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼斑鸦。 笑死愕贡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的巷屿。 我是一名探鬼主播固以,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼攒庵!你這毒婦竟也來(lái)了嘴纺?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤浓冒,失蹤者是張志新(化名)和其女友劉穎栽渴,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體稳懒,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡闲擦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了场梆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墅冷。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖或油,靈堂內(nèi)的尸體忽然破棺而出寞忿,到底是詐尸還是另有隱情,我是刑警寧澤顶岸,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布腔彰,位于F島的核電站,受9級(jí)特大地震影響辖佣,放射性物質(zhì)發(fā)生泄漏霹抛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一卷谈、第九天 我趴在偏房一處隱蔽的房頂上張望杯拐。 院中可真熱鬧,春花似錦世蔗、人聲如沸端逼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)裳食。三九已至,卻和暖如春芙沥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工而昨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留救氯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓歌憨,卻偏偏與公主長(zhǎng)得像着憨,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子务嫡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355