爲什麼Lucene檢索可以比mysql快

Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

 

對於聯合查詢: 所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裏精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作

利用 Skip List 合併

 

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因爲3比13要小。

整個過程如下

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],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

 

從概念上來說,對於一個很長的posting list,比如:

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

我們可以把這個list分成三個block:

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

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

 

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裏的男或者女。如果有1百萬個文檔,那麼性別爲男的posting list裏就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裏也有一個類似的posting list的東西,是未經過這樣壓縮的。

因爲這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

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