因爲倒排索引文件每每佔用巨大的磁盤空間,咱們天然想到對數據進行壓縮。同時,引進壓縮算法後,使得磁盤佔用減小,操做系統在query processing過程當中磁盤讀取效率也能提高。另外,壓縮算法不只要考慮壓縮效果,還要照顧到query processing過程的解壓縮效率。算法
總的來講,好的索引壓縮算法須要最大化兩個方面:編程
一、減小磁盤資源佔用數組
二、加快用戶查詢響應速度緩存
其中,加快響應速度比減小磁盤佔用更爲重要。本文主要介紹PForDelta壓縮算法,其簡單易懂,可以提供可觀的數據壓縮,同時具有很是高的響應速度,所以普遍的運用於不少實際信息檢索系統中。數據結構
一個posting單元由<DocID、TF、Term position…>組成。對於每一個DocID,其保存在硬盤中的大小取決於文件集最大文檔編號的大小。這樣形成編號較小的DocID分配了和編號較大的DocID(上百萬)同樣的存儲空間,浪費了資源。因爲每一個posting是根據DocID順序存儲,因此不須要保存DocID,只須要保存先後兩個DocID的差值,這樣能夠大大減少DocID儲存空間,這種方式成爲Delta Encoding。以下圖:分佈式
對於tf值,根據Zipf定律,tf值較小的term佔大多數,咱們能夠對這類tf值少分配一些空間保存。而tf大的term佔少數,對這些tf分配多空間儲存。基於上述排列特性,每每將docID和tf及其餘數據分開放置,方便數據壓縮。最終,總體的存儲結構以下圖所示:ide
爲了方便分佈式存儲倒排索引文件,Data Block是硬盤中的基礎存儲單元。因爲創建過程須要,每一個term 的postinglist被拆分爲多個部分保存在多個block中(如圖不一樣顏色的block表明存儲不一樣term的postinglist)。也就是說,每一個block內部可能包含多個term的postinglist片斷。函數
Data block的基本組成單元是數據塊(chunk),每一個chunk通常包含固定數量的posting,圖中所示一個chunk包含128個posting,這些posting都屬於同一個term。其中將DocID、tf和position分開排放,方便壓縮。post
這樣以block爲單元,以chunk爲基礎元素的索引存儲的方式,一方面能夠支持使用caching的方法緩存最經常使用term的postinglist,提升query響應速度。另外一方面,全部壓縮解壓縮過程都以chunk爲單位,都在chunk內部進行。當須要查找某一term的postinglist時,不須要對全部文件進行解壓縮。對於不相關的chunk直接忽略,只須要對少部分block中的目標chunk進行處理,這樣又從另外一個方面大大縮短了query響應時間。這也是chunk機制設置的初衷。接下來,咱們討論如何對一個chunk結構進行壓縮和解壓縮處理。ui
PForDelta算法最先由Heman在2005年提出(Heman et al ICDE 2006),它容許同時對整個chunk數據(例128個數)進行壓縮處理。基礎思想是對於一個chunk的數列(例128個),認爲其中佔多數的x%數據(例90%)佔用較小空間,而剩餘的少數1-x%(例10%)纔是致使數字存儲空間過大的異常值。所以,對x%的小數據統一使用較少的b個bit存儲,剩下的1-x%數據單獨存儲。
舉個例子,假設咱們有一串數列23, 41, 8, 12, 30, 68, 18, 45, 21, 9, ..。取b = 5,即認爲5個bit(32)能存儲數列中大部分數字,剩下的超過32的數字單獨處理。從可見的隊列中,超過32的數字有41, 68, 45。那麼PForDelta壓縮後的數據以下圖所示(圖中將超過32的數字稱爲異常值exception):
圖中第一個單元(5bit)記錄第一個異常值的位置,其值爲「1」表示間隔1個b-bit以後是第一個異常值。第一個異常值出如今「23」以後,是「41」,其儲存的位置在隊列的最末端,而其在128個5bit數字中的值「3」表示間隔3個b-bit以後,是下一個異常值,即「68」,以後依次類推。異常值用32bit記錄,在隊列末尾從後向前排列。
上述隊列就對應一個chunk(DocID),還須要另外記錄b的取值和一個chunk壓縮後的長度。這樣就完整的對一個chunk數據進行了壓縮。
可是這樣算法有一個明顯的不足:若是兩個異常值的間隔很是大(例如超過32),咱們須要加入更多的空間來記錄間隔,而且還須要更多的參數來記錄多出多少空間。爲了不這樣的問題,出現了改進的算法NewPFD。
在PForDelta算法基礎上,H. Yan et.al WWW2009提出NewPFD算法及 OptPFD算法。
NewPFD算法
因爲PForDelta算法最大的問題是若是異常值間隔太大會形成b-bit放不下。NewPFD的思路是:128個數最多須要7個bit就能保存,若是能將第二部分中保存異常值的32bit進行壓縮,省出7bit的空間用於保存這個異常值的位置,問題就迎刃而解了。同時更天然想到,若是異常值位置信息保存在隊列後方的32bit中,那麼隊列第一部分原用於記錄異常值間隔的對應部分空間就空餘出來了,能夠利用這部分作進一步改進。
所以,NewPFD的算法是,假設128個數中,取b=5bit,即32做爲閾值。數列中低於32的數字正常存放,數列中大於32的數字,例如41 (101001) 將其低5位(b-bit)放在第一部分,將其剩下的高位(overflow)存放在隊列末端。咱們依然以PForDelta中的例子做爲說明,一個128位數列23, 41, 8, 12, 30, 68, 18, 45, 21, 9, ..。通過NewPFD算法壓縮後的形式以下圖所示:
NewPFD算法壓縮後的數據依然包括兩部分,第一部分128個b-bit數列,省去了第一個異常值位置單元;第二部分異常值部分包含異常值的位置和異常值的高位數字。例如,對於異常值「41」其2進制碼爲101001,那麼低5位01001保存在數據塊第一部分。在第二部分中,先保存位置信息(「41」的位置是「1」,表示原數列第2個),再以字節爲單位保存高位「1」即「0000 0001」,這樣反而只須要附加2個字節(一個保存位置,一個保存高位)就能夠儲存本來須要4個字節保存的異常值。而對於高位字節,還能夠繼續使用壓縮算法進行壓縮,本文再也不繼續討論。
除了數據列,NewPFD算法還須要另外保存b值和高位佔的字節數(稱爲a值)。由於參數ab已經肯定了數據塊的長度,所以chunk長度值不用再單獨記錄。
OptPFD算法
OptPFD算法在NewPFD之上,認爲每一個數據壓縮單元chunk應該有適應本身數據的獨立a值和b值,這樣雖然須要保存大量的ab值,可是畢竟數據量小不會影響太大的速度,相反,因爲對不一樣chunk單獨壓縮,使壓縮效果更好,反而提升瞭解壓縮的效果。
對於b的選取,一般選擇2^b能夠覆蓋數列中90%的數字,也就是控制異常值在10%左右,這樣能夠得到壓縮效果和解壓縮效率的最大化。
瞭解了壓縮算法原理,下面咱們來看倒排索引文件具體如何壓縮。咱們採用OptPFD算法,先定義數據結構和參數:
一、定義一次壓縮64個數字;每次選擇其中的90%數字做爲正常數字,剩餘10%做爲異常值;
二、創建一個結構體PForBlock,保存壓縮後的數據,內部包含數據和壓縮參數a、b;
三、定義一個PForDelta算法對象,主要包括兩個成員函數GetAB和compress,分別表示計算壓縮參數ab值以及實施具體壓縮算法。其中數組bsets_[9],記錄可取的b值,爲了方便計算參數b。爲了數據對齊,刪除了一些b的取值。
四、以4字節爲一個單元存放壓縮數據,爲了解壓縮時代碼更簡潔高效。例如64個待壓縮數字經計算後取b = 3,則4個字節32bit一共能存放10個壓縮後的數字,剩餘32-3*10 = 2bit高位留空。(實際編碼中可任意選擇編程方案)
1 #define COMPRESS_BLOCK_SIZE 64 2 #define PFOR_THRESHOLD 0.9 3 4 struct PForBlock { 5 unsigned a,b; 6 vector<unsigned> data; 7 }; 8 9 class PForCompressor { 10 public: 11 PForCompressor() { 12 bsets_[0]=1;bsets_[1]=2;bsets_[2]=3;bsets_[3]=4;bsets_[4]=5;bsets_[5]=6; 13 bsets_[6]=8;bsets_[7]=10;bsets_[8]=16; 14 } 15 PForBlock compress(const vector<unsigned> &v); 16 17 protected: 18 void getAB(const vector<unsigned> &v); 19 unsigned bsets_[9]; 20 unsigned a_, b_; 21 };
其中,getAB()和compress()函數具體代碼以下:
1 void PForCompressor::getAB(const vector<unsigned> &v) { 2 vector<unsigned> u = v; 3 sort(u.begin(), u.end()); 4 unsigned threshold = u[((unsigned)(double)u.size()*PFOR_THRESHOLD - 1)]; 5 unsigned max_num = u[u.size() - 1]; 6 // Get b 7 unsigned bn = 0; 8 for (; bn < 8; ++bn) { 9 if ((threshold >> bsets_[bn]) <= 0) 10 break; 11 } 12 b_ = bsets_[bn]; 13 // Get a 14 max_num >>= b_; 15 a_ = 1; 16 for (; a_ < 4; ++a_) { 17 if ((1 << (a_ * 8)) > max_num) 18 break; 19 } 20 }
1 PForBlock PForCompressor::compress(const vector<unsigned> &v) { 2 getAB(v); 3 unsigned threshold = 1 << b_; 4 vector<unsigned> tail; 5 vector<unsigned>::iterator it; 6 PForBlock block; 7 block.a = a_; 8 block.b = b_; 9 10 //90% fit numbers and the low b-bit of exceptions are stored at the head 11 unsigned head_size = (v.size() + 32 / b_ + 1) / (32 / b_); 12 for(unsigned i = 0; i < head_size; ++i) 13 block.data.push_back(0); 14 for(unsigned i = 0; i < v.size(); ++i) { 15 unsigned low = v[i] & (threshold - 1); 16 block.data[i / (32 / b_)] |= low << i % (32 / b_) * b_; 17 if(v[i] >= threshold) { 18 tail.push_back(i); 19 unsigned high = v[i] >> b_; 20 for(unsigned l = 0; l < a_; ++l) { 21 tail.push_back((high >> (l * 8)) & 255); 22 } 23 } 24 } 25 26 // high-bit of exceptions are stored at the end using a-bytes each. 27 unsigned temp = 0; 28 unsigned i; 29 for(i = 0; i < tail.size(); ++i) { 30 temp |= tail[i] << (i * 8 % 32); 31 if(i % 4 == 3) { 32 block.data.push_back(temp); 33 temp = 0; 34 } 35 } 36 if(i % 4 != 0) 37 block.data.push_back(temp); 38 39 return block; 40 }
一、解壓前已經知道壓縮數據的參數a和b(由程序或配置文件另外保存),先對壓縮數據的第一部分解壓,以壓縮64個數字爲例,則將64個b-bit從壓縮數據中第一部分提取出來;
二、以後再對第二部分解壓,因爲已經知道異常值在數列中的索引位置(1字節)和異常值的高位bit(a字節),將異常值的高位比特加入其第一部分的低位比特中,就完成了解壓過程。
三、因爲壓縮算法按照4字節爲一個單元進行存儲壓縮數據,而不一樣的參數b值對應不一樣的存放方案,所以最高效的解壓縮編程是每種壓縮方案單獨編寫一個解壓縮程序。
先來看解壓縮對象定義
1 class PForDecompressor { 2 public: 3 PForDecompressor(); 4 void Decompress(unsigned a,unsigned b,unsigned item_num,unsigned *data,unsigned data_length,unsigned* res); 5 6 private: 7 typedef void(PForDecompressor::*step1Funs)(); 8 typedef void(PForDecompressor::*step2Funs)(unsigned); 9 step1Funs step1_[17]; 10 step2Funs step2_[5]; 11 12 unsigned item_num_; 13 unsigned data_length_; 14 unsigned* data_; 15 unsigned* res_; 16 17 void step3(); 18 void step1B1(); 19 void step1B2(); 20 void step1B3(); 21 void step1B4(); 22 void step1B5(); 23 void step1B6(); 24 void step1B8(); 25 void step1B10(); 26 void step1B16(); 27 void step1Ex(); 28 29 void step2A1(unsigned b); 30 void step2A2(unsigned b); 31 void step2A3(unsigned b); 32 void step2A4(unsigned b); 33 };
一、壓縮時設定參數b的取值能夠是一、二、三、四、五、六、八、十、16,解壓時分別對應step1Bx()函數。這個9個函數以函數指針的形式,以b的數值做爲索引,保存在一個具備17個元素的數組裏。爲了方便檢測b的合法性,非法的b取值也設置在了數組中,由step1Ex表示。當step1Ex被調用時,會返回一個異常。
二、壓縮時參數a的取值多是1~4,解壓時分別對應step2Ax(),解壓第二部分時,須要用到參數b,所以將其做爲函數入參。和解壓第一部分同樣,也將這4個函數以函數指針的形式保存在數組裏,方便讀取。
因此,根據算法初始化的要求,天然能夠得出類構造函數定義:
1 PForDecompressor::PForDecompressor() { 2 step1_[0]=&PForDecompressor::step1Ex; 3 step1_[1]=&PForDecompressor::step1B1; 4 step1_[2]=&PForDecompressor::step1B2; 5 step1_[3]=&PForDecompressor::step1B3; 6 step1_[4]=&PForDecompressor::step1B4; 7 step1_[5]=&PForDecompressor::step1B5; 8 step1_[6]=&PForDecompressor::step1B6; 9 step1_[7]=&PForDecompressor::step1Ex; 10 step1_[8]=&PForDecompressor::step1B8; 11 step1_[9]=&PForDecompressor::step1Ex; 12 step1_[10]=&PForDecompressor::step1B10; 13 step1_[11]=&PForDecompressor::step1Ex; 14 step1_[12]=&PForDecompressor::step1Ex; 15 step1_[13]=&PForDecompressor::step1Ex; 16 step1_[14]=&PForDecompressor::step1Ex; 17 step1_[15]=&PForDecompressor::step1Ex; 18 step1_[16]=&PForDecompressor::step1B16; 19 step2_[1]=&PForDecompressor::step2A1; 20 step2_[2]=&PForDecompressor::step2A2; 21 step2_[3]=&PForDecompressor::step2A3; 22 step2_[4]=&PForDecompressor::step2A4; 23 }
得到壓縮數據後,正式開始解壓縮,調用Decompress成員函數:
1 void PForDecompressor::Decompress(unsigned a,unsigned b,unsigned item_num,unsigned *data,unsigned data_length,unsigned* res) { 2 item_num_ = item_num; 3 data_length_ = data_length; 4 data_ = data; 5 res_= res; 6 (this->*step1_[b])(); 7 (this->*step2_[a])(b); 8 }
因爲已經寫好解壓第一部分和解壓第二部分的函數,所以Decompress函數代碼很是簡單,只須要將b和a做爲索引值分別傳入數組中,調用對應的函數進行第一部分和第二部分的解壓。
咱們以b = 3 和 a = 1爲例,簡單看解壓步驟,其餘參數相似。
以b=3解壓縮第一部分:
1 void PForDecompressor::step1B3() 2 { 3 unsigned l = (item_num_ + 9) / 10; 4 unsigned i, block; 5 unsigned *con = res_; 6 for(i = 0; i < l; ++i) 7 { 8 block = *(data_++); 9 data_length_--; 10 con[0] = block & 7; 11 con[1] = (block>>3) & 7; 12 con[2] = (block>>6) & 7; 13 con[3] = (block>>9) & 7; 14 con[4] = (block>>12) & 7; 15 con[5] = (block>>15) & 7; 16 con[6] = (block>>18) & 7; 17 con[7] = (block>>21) & 7; 18 con[8] = (block>>24) & 7; 19 con[9] = (block>>27) & 7; 20 con += 10; 21 } 22 }
第一部分數據,包含原始數據的正常值和異常數據的低b-bit值,它們都以b-bit存放,共64個。
當b=3時,也就是4個字節32bit能夠存放10個b-bit。
所以程序中,data_表示原始待解壓縮的數據流,l表示壓縮數據第一部分的長度,res_/con 是4字節爲單位的數組,用來存放解壓縮後的數據,block每次取出data_的4個字節來完成解壓縮過程;
一、由於32個bit存放10個數據,能夠容易計算出l值,注意防止除法向下取整形成結果錯誤;
二、每次取出4字節數據放在block中;
三、從低位到高位,依次取出block中的bit,每次3個,將其存放入res_中;
第一部分解壓縮完畢,來看a = 1時的第二部分:
1 void PForDecompressor::step2A1(unsigned b) 2 { 3 unsigned block; 4 while(data_length_ > 0) 5 { 6 block = *(data_++); 7 data_length_--; 8 res_[block & 255] += ((block >> 8) & 255) << b; 9 res_[(block >> 16) & 255] += ((block >> 24)) << b; 10 } 11 }
因爲當a = 1時,一個異常值佔2個字節(一個字節索引,一個字節存放高位bit),那麼一個4字節單元能夠存放2個異常值。
一、解壓縮的過程很是簡單,每次取出原始數據流的4個字節,其中包含兩個異常值。
二、第一個字節是第一個異常值在原數列中的索引,第二個字節是它的高bit,將高bit左移b位,加入到第一部分已經解壓出的對應位置中;
三、對於第三個字節和第四個字節也是同理。
當a = 2時,一個異常值佔3個字節(一個字節索引,二個字節存放高位bit),一個4字節不能徹底包含完整的異常值,所以每次須要取出12個字節,從而對4個異常值進行解壓縮。
其他參數取值對應的代碼:
1 void PForDecompressor::step1B1() 2 { 3 unsigned l=(item_num_+31)/32; 4 unsigned i,block; 5 unsigned *con=res_; 6 for(i=0;i<l;i++) 7 { 8 block=*(data_++); 9 data_length_--; 10 con[0] = block & 1; 11 con[1] = (block>>1) & 1; 12 con[2] = (block>>2) & 1; 13 con[3] = (block>>3) & 1; 14 con[4] = (block>>4) & 1; 15 con[5] = (block>>5) & 1; 16 con[6] = (block>>6) & 1; 17 con[7] = (block>>7) & 1; 18 con[8] = (block>>8) & 1; 19 con[9] = (block>>9) & 1; 20 con[10] = (block>>10) & 1; 21 con[11] = (block>>11) & 1; 22 con[12] = (block>>12) & 1; 23 con[13] = (block>>13) & 1; 24 con[14] = (block>>14) & 1; 25 con[15] = (block>>15) & 1; 26 con[16] = (block>>16) & 1; 27 con[17] = (block>>17) & 1; 28 con[18] = (block>>18) & 1; 29 con[19] = (block>>19) & 1; 30 con[20] = (block>>20) & 1; 31 con[21] = (block>>21) & 1; 32 con[22] = (block>>22) & 1; 33 con[23] = (block>>23) & 1; 34 con[24] = (block>>24) & 1; 35 con[25] = (block>>25) & 1; 36 con[26] = (block>>26) & 1; 37 con[27] = (block>>27) & 1; 38 con[28] = (block>>28) & 1; 39 con[29] = (block>>29) & 1; 40 con[30] = (block>>30) & 1; 41 con[31] = (block>>31) & 1; 42 con+=32; 43 } 44 }
1 void PForDecompressor::step1B2() 2 { 3 unsigned l=(item_num_+15)/16; 4 unsigned i,block; 5 unsigned *con=res_; 6 for(i=0;i<l;i++) 7 { 8 block=*(data_++); 9 data_length_--; 10 con[0] = block & 3; 11 con[1] = (block>>2) & 3; 12 con[2] = (block>>4) & 3; 13 con[3] = (block>>6) & 3; 14 con[4] = (block>>8) & 3; 15 con[5] = (block>>10) & 3; 16 con[6] = (block>>12) & 3; 17 con[7] = (block>>14) & 3; 18 con[8] = (block>>16) & 3; 19 con[9] = (block>>18) & 3; 20 con[10] = (block>>20) & 3; 21 con[11] = (block>>22) & 3; 22 con[12] = (block>>24) & 3; 23 con[13] = (block>>26) & 3; 24 con[14] = (block>>28) & 3; 25 con[15] = (block>>30) & 3; 26 con+=16; 27 } 28 }
1 void PForDecompressor::step1B5() 2 { 3 unsigned l=(item_num_+5)/6; 4 unsigned i,block; 5 unsigned *con=res_; 6 for(i=0;i<l;i++) 7 { 8 block=*(data_++); 9 data_length_--; 10 con[0] = block & 31; 11 con[1] = (block>>5) & 31; 12 con[2] = (block>>10) & 31; 13 con[3] = (block>>15) & 31; 14 con[4] = (block>>20) & 31; 15 con[5] = (block>>25) & 31; 16 con+=6; 17 } 18 }
1 void PForDecompressor::step1B6() 2 { 3 unsigned l=(item_num_+4)/5; 4 unsigned i,block; 5 unsigned *con=res_; 6 for(i=0;i<l;i++) 7 { 8 block=*(data_++); 9 data_length_--; 10 con[0] = block & 63; 11 con[1] = (block>>6) & 63; 12 con[2] = (block>>12) & 63; 13 con[3] = (block>>18) & 63; 14 con[4] = (block>>24) & 63; 15 con+=5; 16 } 17 }
1 void PForDecompressor::step1B8() 2 { 3 unsigned l=(item_num_+3)/4; 4 unsigned i,block; 5 unsigned *con=res_; 6 for(i=0;i<l;i++) 7 { 8 block=*(data_++); 9 data_length_--; 10 con[0] = block & 255; 11 con[1] = (block>>8) & 255; 12 con[2] = (block>>16) & 255; 13 con[3] = (block>>24) & 255; 14 con+=4; 15 } 16 }
1 void PForDecompressor::step1B10() 2 { 3 unsigned l=(item_num_+2)/3; 4 unsigned i,block; 5 unsigned *con=res_; 6 for(i=0;i<l;i++) 7 { 8 block=*(data_++); 9 data_length_--; 10 con[0] = block & 1023; 11 con[1] = (block>>10) & 1023; 12 con[2] = (block>>20) & 1023; 13 con+=3; 14 } 15 }
1 void PForDecompressor::step1B16() 2 { 3 unsigned l=(item_num_+1)/2; 4 unsigned i,block; 5 unsigned *con=res_; 6 for(i=0;i<l;i++) 7 { 8 block=*(data_++); 9 data_length_--; 10 con[0] = block & ((1<<16)-1); 11 con[1] = block>>16; 12 con+=2; 13 } 14 }
1 void PForDecompressor::step1Ex() 2 { 3 cerr<<"Invalid b value"<<endl; 4 }
1 void PForDecompressor::step2A2(unsigned b) 2 { 3 unsigned block1,block2; 4 while(data_length_ > 0) 5 { 6 block1 = *(data_++); 7 data_length_--; 8 res_[block1 & 255]+=((block1>>8) & 65535)<<b; 9 if(data_length_ == 0) break; 10 block2 = *(data_++); 11 data_length_--; 12 res_[block1>>24]+=(block2 & 65535)<<b; 13 if(data_length_ == 0) break; 14 block1 = *(data_++); 15 data_length_--; 16 res_[(block2>>16) & 255]+=((block2>>24) + ((block1 & 255)<<8))<<b; 17 res_[(block1>>8) & 255]+=(block1>>16)<<b; 18 } 19 }
1 void PForDecompressor::step2A3(unsigned b) 2 { 3 unsigned block; 4 while(data_length_ > 0) 5 { 6 block= *(data_++); 7 data_length_--; 8 res_[block & 255]+=(block>>8)<<b; 9 } 10 }
1 void PForDecompressor::step2A4(unsigned b) 2 { 3 unsigned block1,block2; 4 while(true) 5 { 6 if(data_length_<=1) break; 7 block1 = *(data_++); 8 block2 = *(data_++); 9 data_length_ -= 2; 10 res_[block1 & 255]+=( (block1>>8) + ((block2 & 255)<<24) )<<b; 11 if(data_length_ == 0) break; 12 block1 = *(data_++); 13 data_length_--; 14 res_[(block2>>8) & 255]+=( (block2>>16) + ((block1 && 65535)<<16) )<<b; 15 if(data_length_ == 0) break; 16 block2 = *(data_++); 17 data_length_--; 18 res_[(block1>>16) & 255]+=( (block1>>24) + ((block2 && 16777215)<<8) )<<b; 19 if(data_length_ == 0) break; 20 block1 = *(data_++); 21 data_length_--; 22 res_[block2>>24]+=block1; 23 } 24 }