lucene posting list 編碼之Frame of Reference

本文是:https://www.elastic.co/cn/blog/frame-of-reference-and-roaring-bitmaps 文章的翻譯及理解。

lucene 在存儲 doc 時,會爲每個 doc 分配一個 doc_id。doc_id 是 segment 維度(index->shard->segment)的一個數值,這個數值的範圍是[0,2^32-1],因此:一個 segment 最多允許存儲 2^32-1 個 doc

Inside each segment, documents are given an identifier between 0 and the number of documents in the segment (up to 231-1)

一個倒排鏈(posting list)中的 doc_id 是有序的,爲了高效地存儲倒排鏈,lucene 使用了 FOR(Frame Of Reference) 編碼,其核心是 for-delta。

現在有一個 posting list,其中有 6 個 doc_id:[73,300,302,332,343,372],應該如何高效存儲呢?Java 中一個 int 型佔用 4B,存儲 6 個 int 型數字需要:4B*6=24B。那:FOR 編碼之後是如何存儲的呢?看下圖:
image

第1步:delta 編碼

第1個 doc_id 是73,加上 delta 227 等於:300,就是第2個 doc_id 的數值。類似地,73+227+2=302 就是第3個 doc_id 的數值。因此,只需要把第1個doc_id記錄下來,然後保存後面每個doc_id 的 delta 值,就能知道每個 doc_id 的數值了。

73 0+73=73
227 73+227=300
302 300+2=302
332 302+30=332
343 332+11=343
372 343+29=372

如果 delta 的取值範圍是[0,255],那麼只需要 1B 就可以存儲 delta。因此,相比於存儲 doc_id 原始值(需要 4B),存儲 delta 能起到壓縮效果。

第2步:split into blocks

第1步的分析有個前提假設是: delta 的取值範圍是[0,255],實際中:delta 的取值範圍是不確定的,爲了 delta 的取值範圍不大於“某個值”,可以將 posting list 拆分成 block,計算出這個 block 內最大的 delta 是多少,然後將該 delta 存儲到 block 表頭,block 表頭 delta 佔用的 bit 數量就是:後續 delta 所需要的 bit 數量的最大值。

Lucene computes the maximum number of bits required to store deltas in a block, adds this information to the block header, and then encodes all deltas of the block using this number of bits

以上圖示例:拆分了 2 個 block 塊,第1個 block 塊的 delta 取值是:[73,227,2],最大的 delta 是 227,這個 block 塊中的每個 delta 需要使用 8個bit 存儲。而第2個 block 塊的 delta 取值是:[30,11,29],最大的 delta 是 30,這個 block 塊中每個 delta 只需要使用 5個 bit (因爲 5個 bit 存儲範圍是: 2^5-1=31)都可以存儲下來。

因此,經過 delta-encoding 之後,6個 doc_id 所需要的存儲從原來的 24B 減少到現在的 7B。(38bit + 35bit = 7B)

可以看出:block 塊的拆分,其目的是在一部分 doc_id 中尋找一個合適的 最大 delta 值,使得這部分doc_id 對應的 delta 不超過 最大 delta,從而保證最佳壓縮效果。

還有一個需要對 posting list 的 doc_id 壓縮編碼的地方是:ES 的 filter cache。關於 filter cache 可先參考:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-cache.html

filter cache 有3種數據結構來實現:

  1. integer array
  2. bitmap
  3. roaring bitmap

對於 integer array,如果 filter cache 只緩存幾個 doc_id(少量的doc_id),那麼使用 integer array 內存是OK的,但是若 filter cache 緩存了100M個 doc_id,則需要 400MB 內存空間,因此integer array 不適用於“dense sets” 場景。

bitmap 和 roaring bitmap 是採用 bit 思想存儲 int 型整數,一個 int 型整數只需要1個bit存儲,100萬個 int 型整數大約需要12.5MB內存空間,壓縮效果非常好。

A bitmap is an array where each entry takes only one bit, so they only have two possible values: 0 or 1.... If we compare to option 1, memory usage is much better on dense filters since we would now only need 100M bits = 12.5MB

roaring bitmap 相比於 bitmap,多了一個 split block 步驟,簡單說就是:高16位與低16位分別encoding,進一步優化壓縮效果。

文章的最後對比了這3種數據結構在:內存佔用、遍歷操作、skip操作 這2種 Lucene 最基本的 search operation 的性能。總結起來就是:如果 filter cache 需要緩存的 doc_id 非常的多,那麼很適用 bitmap。integer array 雖然快,但是太耗費內存。

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