Lucene 4.X 倒排索引原理與實現: (2) 倒排表的格式設計

1. 定長編碼

最容易想到的方式就是經常使用的普通二進制編碼,每一個數值佔用的長度相同,都佔用最大的數值所佔用的位數,如圖所示。前端

clip_image002[6]

 

這裏有一個文檔ID列表,254,507,756,1007,若是按照二進制定長編碼,須要按照最大值1007所佔用的位數10位進行編碼,每一個數字都佔用10位。數組

和詞典的格式設計中順序列表方式遇到的問題同樣,首先的問題就是空間的浪費,原本254這個數值8位就能表示,非得也用上10位。另一個問題是隨着索引文檔的增多,誰也不知道最長鬚要多少位纔夠用。緩存

2. 差值(D-gap)編碼

看過前面前端編碼的讀者可能會想到一個相似的方法,就是咱們能夠保存和前一個數值的差值,若是Term在文檔中分佈比較均勻(差很少每隔幾篇就會包含某個Term),文檔ID之間的差異都不是很大,這樣每一個數值都會小不少,所用的位數也就節省了,如圖所示。數據結構

clip_image004[6]

 

仍是上面的例子中,咱們能夠看到,這個Term在文檔中的分佈仍是比較均勻的,每隔差很少250篇文檔就會出現一次這個Term,因此計算差值的結果比較理想,獲得下面的列表254,253,249,251,每一個都佔用8位,的確比定長編碼節省了空間。測試

然而Term在文檔中均勻分佈的假設每每不成立,不少詞彙極可能是彙集出現的,好比奧運會,世界盃等相關的詞彙,在一段時間裏密集出現,而後會有很長時間不被提起,而後又出現,若是索引的新聞網頁是安裝抓取前後來編排文檔ID的狀況下,極可能出現圖所示的狀況。this

clip_image006[6]

 

在很早時間第10篇文檔出現過這個Term,而後相關的文檔沉寂了一段時間,而後在第1000篇的時候密集出現。若是使用差值編碼,獲得序列10,990,21,1,雖然不少值已經被減少了,可是因爲前兩篇文檔的差值爲990,仍是須要10位進行編碼,並無節省空間。搜索引擎

3. 一元編碼(unary)

有人可能會問了,爲何要用最大的數值所佔用的位數啊,有幾位我們就用幾位嘛。這種思想就是變長編碼,也即每一個數值所佔用的位數不一樣。然而說的容易作起來難,定長編碼模式下,每讀取b個bit就是一個數值,某個數值從哪裏開始,讀取到哪裏結束,都一清二楚,然而對於變長編碼來講就不是了,每一個數值的長度都不一樣,那一連串二進制讀出來,讀到哪裏算一個數值呢?好比上面的例子中,10和990的二進制連起來就是10101111011110,那到底1010是一個數值,1111011110是另外一個數值呢,仍是1010111是一個數值,剩下的1011110算另外一個數值呢?另外就是會不會產生歧義呢?好比一個數值A=0011是另外一個數值B=00111的前綴,那麼當二進制中出現00111的時候,究竟是一個數值A,最後一個1屬於下一個數值呢,仍是整個算做數值B呢?這都是變長編碼所面臨的問題。固然還有錯了一位,多了一位,丟掉一位致使整個編碼都混亂的問題,能夠經過各類校驗方法來保證,不在本文的討論範圍內,本文僅僅討論假設徹底正確的前提下,如何編碼和解碼。編碼

最簡單的變長編碼就是一元編碼,它的規則是這樣的:對於正整數x,用x-1個1和末尾一個0來表示。好比5,用11110表示。這種編碼方式對於小數值來說尚可,對於大數值來說比較恐怖,好比1000須要用999個1外加一個0來表示,這哪裏是壓縮啊,分明是有錢沒處使啊。可是沒關係,火車剛發明出來的時候還比馬車慢呢。這種編碼方式雖然初級,可是很好的解決了上面兩個問題。讀到哪裏結束呢?讀到出現0了,一個數值就結束了。會不會出現歧義呢?沒有一個數值是另外一個數值的前綴,固然沒有歧義了。因此一元編碼成爲不少變長編碼的基礎。lua

4. Elias Gamma編碼

Gamma編碼的規則是這樣的:對於正整數x,首先對於clip_image008[7]進行一元編碼,而後用clip_image010[7]個bit對clip_image012[7]進行二進制編碼。設計

好比對於正整數10,clip_image014[6],對於4進行一元編碼1110,計算clip_image016[6],用3個bit進行編碼010,因此最後編碼爲1110010。

能夠看出,Gamma編碼綜合了一元編碼和二進制編碼,對於第一部分一元編碼的部分,能夠知道什麼時候結束而且無歧義編碼,對於二進制編碼的部分,因爲一元編碼部分指定了二進制部分的長度,於是也能夠知道什麼時候結束並沒有歧義解碼。

好比讀取上述編碼,先讀取一系列1,到0結束,獲得1110,是一元編碼部分,值爲4,因此後面3個bit是二進制部分,讀取下面的3個bit(即使這3個bit後面還有些0和1,都不是這個數值的編碼了),010,值爲2,最後clip_image018[6],解碼成功。

Gamma編碼比單純的一元編碼好的多,對於小的數值也是頗有效的,可是當數值增大的狀況下,就暴露出其中一元編碼部分的弱勢,好比1000通過編碼須要19個bit。

5. Elias Delta編碼

咱們在Gamma編碼的基礎上,進一步減小一元編碼的影響,造成Delta編碼,它的規則:對於正整數x,首先對於clip_image008[8]進行Gamma編碼,而後用clip_image010[8]個bit對clip_image012[8]進行二進制編碼。

好比對於正整數10,clip_image014[7],首先對於4進行Gamma編碼,clip_image020[4],對於3進行一元編碼110,而後用2個bit對clip_image022[4]進行二進制編碼00,因此4的Gamma編碼爲11000,而後計算clip_image016[7],用3個bit進行編碼010,因此最後編碼爲11000010。

Delta是Gamma編碼和二進制編碼的整合,也是符合變長編碼要求的。

若是讀取上述編碼,先讀入一元編碼110,爲數值3,接着讀取3-1=2個bit爲00,爲數值0,因此Gamma編碼爲clip_image024[4],而後讀取三個bit位010,二進制編碼數值爲2,因此clip_image018[7],解碼成功。

儘管從數值10的編碼中,咱們發現Delta比Gamma使用的bit數還多,然而當數值比較大的時候,Delta就相對比較好了。好比1000通過Delta編碼須要16個bit,要優於Gamma編碼。

6. 哈夫曼編碼

前面所說的編碼方式都有這樣一個特色,「任爾幾路來 ,我只一路去」,也即不管要壓縮的倒排表是什麼樣子的,都是一個方式進行編碼,這種方法顯然不能知足日益增加的多樣物質文化的須要。接下來介紹的這種編碼方式,就是一種看人下菜碟的編碼方式。

前面也說到變長編碼無歧義,要求任何一個編碼都不是其餘編碼的前綴。學過數據結構的同窗極可能會想到——哈夫曼編碼。

哈夫曼編碼是如何看人下菜碟的呢?哈夫曼編碼根據數值出現的頻率不一樣而採用不一樣長度進行編碼,出現的次數多的編碼長度短一些,出現次數少的編碼長度長一些。

這種方法從直覺上顯然是正確的,若是一個數值出現的頻率高,就表示出現的次數多,若是採用較短的bit數進行編碼,少一位就能節約很多空間,而對於出現頻率低的數值,好比就出現一次,咱們就用最奢侈的方法編碼,也佔用不了多少空間。

固然這種方法也是符合信息論的,信息論中的香農定理給出了數據壓縮的下限。也即用來表示某個數值的bit的數值的下限和數值出現的機率是有關的:clip_image026[4]。看起來很抽象,舉一個例子,若是拋硬幣正面朝上機率是0.5,則至少使用1位來表示,0表示正面朝上,1表示沒有正面朝上。固然很對實際運用中,因爲對於數值出現的機率估計的沒有那麼準確,則編碼達不到這個最低的值,好比用2位表示,00表示正面朝上,11表示沒有正面朝上,那麼01和10兩種編碼就是浪費的。對於整個數值集合s,每一個數值所佔用的平均bit數目,即全部clip_image028[4]的平均值clip_image030[4],稱爲墒。

要對一些數值進行哈夫曼編碼,首先要經過掃描統計每一個數值出現的次數,假設統計的結果如圖所示。

clip_image032[4]

 

其次,根據統計的數據構建哈夫曼樹,構建過程是這樣的,如圖:

1) 按照次數進行排序,每一個文檔ID構成一棵僅包含根節點的樹,根節點的值即次數。

2) 將具備最小值的兩棵樹合併成一棵樹,根節點的值爲左右子樹根節點的值之和。

3) 將這棵新樹根據根節點的值,插入到隊列中相應的位置,保持隊列排序。

4) 重複第二步和第三步,直到合併成一棵樹爲止,這棵樹就是哈夫曼樹。

clip_image034[4]

 

最終,根據最後造成的哈夫曼樹,給每一個邊編號,左爲0,右爲1,而後從根節點到葉子節點的路徑上的字符組成的二進制串就是編碼,如圖所示。

clip_image036[4]

 

最終造成的編碼,咱們經過觀察能夠發現,沒有一個文檔ID的編碼是另一個的前綴,因此不存在歧義,對於二進制序列1001110,惟一的解碼是文檔ID 「123」和文檔ID 「689」,不可能有其餘的解碼方式,對於Gamma和Delta編碼,若是要保存二進制,則須要經過一元編碼或者Gamma編碼保存一個長度,才能知道這個二進制到底有多長,然而在這裏連保存一個長度的空間都省了。

固然這樣原始的哈夫曼編碼也是有必定的缺點的:

1) 若是須要編碼的數值有N個,則哈夫曼樹的葉子節點有N個,每一個都須要一個指向數值的指針,內部節點個數是N-1個,每一個內部節點包含兩個指針,如將整棵哈夫曼樹保存在內存中,假設數值和指針都須要佔用M個byte,則須要(N+N+2(N-1))*M=(4N-2)*M的空間,耗費仍是比較大的。

2) 哈夫曼樹的造成是有必定的不穩定性的,在構造哈夫曼樹的第3步中,將一棵新樹插入到排好序的隊列中的時候,若是遇到了兩個相同的值,誰排在前面?不一樣的排列方法會產生不一樣的哈夫曼樹,最終影響最後的編碼,如圖。

clip_image038[4]

 

爲了解決上面兩個問題,大牛Schwartz在論文《Generating a canonical prefix encoding》中,對哈夫曼編碼作了必定的規範(canonical),因此又稱爲規範哈夫曼編碼或者範式哈夫曼編碼。

固然哈夫曼樹仍是須要創建的,可是不作保存,僅僅用來肯定每一個數值所應該佔用的bit的數目,由於出現次數多的數值佔用bit少,出現次數少的數值佔用bit多,這個靈魂不能丟。可是若是佔用相同的bit,到底你是001,我是010,仍是倒過來,這倒沒必要遵循左爲0,右爲1,而是指定必定的規範,來消除不穩定性,並在佔用內存較少的狀況下也能解碼。

規範具體描述以下:

1) 全部要編碼的數值或者字符排好隊,佔用bit少的在前,佔用bit多的在後,對於相同的bit按照數值大小排序,或者按照字典順序排序。

2) 先從佔用bit最少的數值開始編碼,對於第一個數值,若是佔用i個bit,則應該是i個0。

3) 對於相同bit的其餘數值,則爲上一個數值加1後的二進制編碼

4) 當佔用i個bit的數值編碼完畢,接下來開始對佔用j個bit的數值進行編碼,i < j。則j的第一個數值的編碼應該是i個bit的最後一個數值的編碼加1,而後後面再追加j-i個0

5) 充分3和4完成對全部數值的編碼。

按照這個規範,圖中的編碼應該如圖:

clip_image040[4]

 

根據這些規則,不穩定性首先獲得瞭解決,不管同一個層次的節點排序如何,都會按照數值或字符的排序來決定編碼。

而後就是佔用內存的問題,若是使用範式哈夫曼編碼,則只須要存儲下面的數據結構,如圖:

clip_image042[4]

 

固然本來數值的列表仍是須要保存的,只不過順序是安裝佔用bit從小到大,相同bit數按照數值排序的,須要N*M個byte。

另外三個數組就比較小了,它們的下標表示佔用的bit的數目,也即最長的編碼須要多少個bit,則這三個數組最長就那麼長,在這個例子中,最長的編碼佔用5個bit,因此,它們僅僅佔用3*5*M個byte。

第一個數組保存的是佔用i個bit的編碼中,起始編碼是什麼,因爲相同bit數的編碼是遞增的,於是知道了起始,後面的都可以推出來。

第二個數組保存的是佔用i個bit的編碼有幾個,好比5個bit的編碼有5個,因此Number[5]=5。

第三個數組保存的是佔用i個bit的編碼中,起始編碼在數值列表中的位置,爲了解碼的時候快速找到被解碼的數值。

若是讓咱們來解析二進制序列1110110,首先讀入第一個1,首先判斷是否能構成一個1bit的編碼,Number[1]=0,佔用1個bit的編碼不存在;因此讀入第二個1,造成11,判斷是否能構成一個2bit的編碼,Number[2]=3,而後檢查FirstCode[2]=00 < 11,然而11 – 00 + 1 = 4 > Number[2],超過了2bit編碼的範圍;因而讀入第三個1,造成111,判斷是否能構成一個3bit的,Number[3]=1,而後檢查FirstCode[3]=110<111,然而111 – 110 + 1 = 2> Number[3],超過了3bit的編碼範圍;因而讀入第四個0,Number[4]=0,再讀入第五個1,判斷是否能構成一個5bit的編碼,Number[5]=4,而後檢查FirstCode[5]=11100 < 11101,11101 – 11100 + 1 = 2<4,因此是一個5bit編碼,並且是5bit編碼中的第二個,5bit編碼的第二個在位置Position[5]=5,因此此5bit編碼是數值列表中的第6項,解碼爲value[6]=345。而後讀入1,不能構成1bit編碼,11不能構成2bit編碼,110,Number[3]=1,而後檢查FirstCode[3]=110=110,因此構成3bit編碼的第一個Position[3]=4,解碼爲value[4]=789。

若是真能像理想中的那樣,在壓縮全部的倒排表以前,都可以事先經過全局的觀測來統計每一個文檔ID出現的機率,則可以實現比較好的壓縮效果。

在這個例子中,咱們編碼後使用的bit的數目爲:

clip_image044[4]

咱們再來算一下墒:

clip_image046[4]

按照香農定理最低佔用的bit數爲clip_image048[4]

能夠看出哈夫曼編碼的壓縮效果至關不錯。然而在真正的搜索引擎系統中,文檔是不斷的添加的,很難事先作全局的統計。

對於每個倒排表進行局部的統計和編碼是另外一個選擇,然而付出的代價就是須要爲每個倒排表保存一份上述的結構來進行解碼。很不幸上述的結構中包含了數值的列表,若是一個倒排表中數值重複率很高,好比100萬的長的倒排表只有10種數值,爲100萬保存10個數值的列表還能夠接受,若是重複率不高,那麼數值列表自己就和要壓縮的倒排表差很少大了,根本起不到壓縮做用。

7. Golomb編碼

若是咱們將倒排表中文檔ID的機率分佈假設的簡單一些,就不必統計出現的全部的數值的機率。好比一個簡單的假設就是:Term在文檔集集合中是獨立隨機出現的。

既然是隨機出現的,那麼就有一個機率問題,也即某個Term在某篇文檔中出現的機率是多少?假設咱們把整個倒排結構呈現如圖的矩陣的樣子,左面是n個Term,橫着是N篇文檔,若是某個Term在某篇文檔中出現,則那一位設爲1,假設裏面1的個數爲f,那麼機率clip_image050[4]

clip_image052[4]

 

正如在差值編碼一節中論述的那樣,咱們在某個Term的倒排表裏面保存的不是文檔ID,而是文檔ID的間隔的數值,咱們想要作的事情就是用最少的bit數來表示這些間隔數值。

若是全部的間隔組成的集合是已知的,則可用上一節所述的哈夫曼編碼。

咱們在這裏想要模擬的狀況是:間隔組成的集合是不肯定的,甚至是無限的,隨着新的文檔的不斷到來進行不斷的編碼。

能夠形象的想象成下面的情形,一個Term坐在那裏等着文檔一篇篇的到來,若是文檔包含本身,就掛在倒排表上。

若是文檔間隔是x,則表示的情形就是,來一篇文檔不包含本身,再來一篇仍是不包含本身,x-1篇都過去了,終於到了第x篇,包含了本身。若是對於一篇文檔是否包含本身的機率爲p,則文檔間隔x出現的機率就是clip_image054[4]

假設編碼當前的文檔間隔x用了n個bit,這個Term接着等下一篇文檔的到來,結果此次更不幸,等了x篇還包含本身,直到等到x+b篇文檔才包含本身,因而要對x+b進行編碼,x+b出現的機率爲clip_image056[4],顯然比x的機率要低,根據信息論,若是x用n個bit,則x+b要使用更多的bit,假設clip_image058[4],則最優的狀況應該多用1個bit。

這樣咱們就造成了一個遞推的狀況,假設已知文檔間隔x用了n個bit,對於clip_image060[6]來講,x+b就應該只用n+1個bit,這樣若是有了初始的文檔間隔而且進行了最優的編碼,後面的都能達到最優。

因而Golomb編碼就產生了,對於參數b(固然是根據文檔集合計算出的機率產生的),對於數值x的編碼分爲兩部分,第一部分計算clip_image062[4],而後將q+1用一元編碼,第二部分是餘數,r=x-1-qb,因爲餘數必定在0到b-1之間,則能夠用clip_image064[4]或者clip_image066[4]進行無前綴編碼(哈夫曼編碼)。

用上面的理論來看Golomb編碼就容易理解了,第一部分是用來保持上面的遞推性質的,一元編碼的性質能夠保證,數值增長1,編碼就多用1位,遞推性質要求數值x增長b,編碼增長1位,因而有了用數值x除以b,這樣clip_image068[4]。第二部分的長度對於每一個編碼都是同樣的,最多不過差一位,與數值x無關,僅僅與參數b有關,其實第二部分是用來保證初始的文檔間隔是最優的,因此哈夫曼編碼進行無前綴編碼。

例如x=9,b=6,則clip_image070[4],對q+1用一元編碼爲10,餘數r=2,首先對於全部的餘數進行哈夫曼編碼,造成如圖的哈夫曼樹,從最小的餘數開始進行範式哈夫曼編碼,0爲00,1爲01,2佔用三個bit,爲01 + 1補充一位,爲100,3爲101,4爲110,5爲111。因此x=9的編碼爲10100。

clip_image072[4]

 

接下來咱們試圖編碼x=9+6=15,b=6,則clip_image074[4],對q+1用一元編碼爲110,餘數r=2,編碼爲100,最後編碼爲110100,果然x增大b,編碼多了1位。

接下來要解決的問題就是如何肯定b的值,按照我們的理論推導clip_image060[7],計算起來有些麻煩,咱們先來計算分母部分clip_image076[4],當p接近於0的時候,由著名的極限公式clip_image078[4],因此分母約爲p,因而公式最後爲clip_image080[4]

因爲Golomb編碼僅僅須要另外保存一個參數b,因此既能夠基於整個文檔集合的機率進行編碼,這個時候clip_image082[4],也能夠應用於某一個倒排表中,對於一個倒排表進行局部編碼,以達到更好的效果,對於某一個倒排表,term的數量n=1,f=詞頻Term Freqency,clip_image084[4],這樣不一樣的倒排表使用不一樣的參數b,達到這樣一個效果,對於詞頻高的Term,文檔出現的相對緊密,用較小的b值來編碼,對於詞頻低的Term,文檔出現的相對比較鬆散,用較大的b來進行編碼。

8. 插值編碼(Binary Interpolative Coding)

前面講到的Golomb編碼表現不凡,實現了較高的壓縮效果。然而一個前提條件是,假設Term在文檔中出現是獨立隨機的,在倒排表中,文檔ID的插值相對比較均勻的狀況下,Golomb編碼表現較好。

然而Term在文檔中卻每每出現的不那麼隨機,而每每是相關的話題彙集在一塊兒的出現的。因而倒排表每每造成以下的狀況,如圖.

clip_image086[4]

 

咱們能夠看到,從文檔ID 8到文檔ID 13之間,文檔是相對比較彙集的。對於彙集的文檔,咱們能夠利用這個特性實現更好的壓縮。

若是咱們已知第1篇文檔的ID爲8,第3篇文檔的ID爲11,那麼第2篇文檔只有兩種選擇9或者10,因此能夠只用1位進行編碼。還有更好的狀況,好比若是咱們已知第3篇文檔ID爲11,第5篇文檔ID爲13,則第6篇文檔別無選擇,只有12,能夠不用編碼就會知道。

這種方法能夠形象的想象成爲,咱們從1到20共20個坑,咱們要將文檔ID做爲標杆插到相應的坑裏面,咱們老是採用限制兩頭在中間找坑的方式,仍是上面的例子,若是咱們已經將第1篇文檔插到第8個坑裏,已經將第3篇文檔插到第11個坑裏,下面你要將第2篇文檔插到他們兩個中間,只有兩個坑,因此1個bit就夠了。固然一開始一個標杆尚未插的時候,選擇的範圍會比較的大,因此須要較多的bit來表示,當已經有不少的標杆插進去了之後,選擇的範圍會愈來愈小,須要的bit數也愈來愈小。

下面詳細敘述一下編碼的整個過程,如圖所示。

clip_image088[4]

 

最初的時候,咱們要處理的是整個倒排表,長度Length爲7,面對的從Low=1到High=20總共有20個坑。仍是採起限制兩頭中間插入的思路,咱們先找到中間數值11,而後找一坑插入它,那兩頭如何限制呢?是否是從1到20均可以插入呢?固然不是,由於數值11的左面有三個數值Left=3,一個數值一個坑的話,至少要留三個坑,數值11的右面也有三個數值Right=3,則右面也要留三個坑,因此11這根標杆只能插到從4到17共14個坑裏面,也就是說共有14中選擇,用二進制表示的話,須要clip_image090[4]bit來存儲,咱們用4位來編碼11-4=7爲0111。

第一根標杆的插入將倒排表和坑都分紅了兩部分,咱們能夠分而治之。左面一部分咱們稱之<Length=3, Low=1, High=10>,由於它要處理的倒排表長度爲3,並且必定是放在從1到10這10個坑裏面的。同理,右面一部分咱們稱之<Length=3, Low=12, High=20>,表示另外3個數值組成的倒排表要放在從12到20這些坑裏。

先來處理<Length=3, Low=1, High=10>這一部分,如圖。

clip_image092[4]

 

一樣選取中間的數值8,而後左面須要留一個坑Left=1,右面須要留一個坑Right=1,因此8所能插入的坑從2到9共8個坑,也就是8中選擇,用二進制表示,須要clip_image094[4]bit來存儲,因而編碼8-2=6爲110。

標杆8的插入將倒排表和坑又分爲兩部分,仍是用上面的表示方法,左面一部分爲<Length=1,Low=1,High=7>,表示只有一個值的倒排表要插入從1到7這七個坑中,右面一部分爲<Length=1,Low=9,High=10>,表示只有一個值的倒排表要插入從9到10這兩個坑中。

咱們來處理<Length=1,Low=1,High=7>部分,如圖。

clip_image096[4]

 

只有一個數值3,左右也不用留坑,因此能夠插入從1到7任何一個坑,共7中選擇,須要3bit,編碼3-1=2爲010。

對於<Length=1,Low=9,High=10>部分,如圖。

clip_image098[4]

 

只有一個數值9,能夠選擇的坑從9到10兩個坑,共兩種選擇,須要1bit,編碼9-9=0爲0。

再來處理<Length=3, Low=12, High=20>部分,如圖。

clip_image100[4]

 

選擇插入中間數值13,左面須要留一個坑Left=1,右面須要留一個坑Right=1,因此13能夠插入在從13到19這7個坑裏,共7種選擇,須要3bit,編碼13-13=0爲000。

數值13的插入將倒排表和坑分爲兩部分,左面<Length=1, Low=12, High=12>,只有一個數值的倒排表要插入惟一的一個坑,右面<Length=1,Low=14,High=20>,只有一個數值的倒排表插入從14到20的坑。

對於<Length=1, Low=12, High=12>,如圖,一個數一個坑,不用佔用任何bit就能夠。

clip_image102[4]

 

對於<Length=1,Low=14,High=20>,如圖,只有一個值17,放在14到20之間7個坑中,有7中選擇,須要3bit,編碼17-14=3爲011。

clip_image104[4]

 

綜上所述,最終的編碼爲0111 110 010 0 000 011,共17位。若是用Golomb編碼差值<3,5,1,2,1,1,4>,經計算b=2,則編碼共18位。差值編碼表現更好。

那麼解碼過程應該如何呢?初始咱們知道<Length=7,Low = 1,High=20>,首先解碼的是中間的也即第3個數值,因爲Left=3,Right=3,則可這個數值一定在從4到17,表示這14種選擇須要4位,於是讀取最初的4位0111爲7,加上Low + Left = 4,第3個數值解碼爲11。

已知第3個數值爲11後,則左面應該有三個數值,並且必定是從1到10,表示爲<Length=3, Low=1, High=10>,右面的也應該有三個數值,並且必定是從12到20,表示爲<Length=3, low=12, high=20>。

先解碼左面<Length=3, Low=1, High=10>,解碼中間的數值,也即第1個數值,因爲Left=1,Right=1,則這個數值一定從2到9,表示8種選擇須要3位,於是讀出3位110,爲6,加上Low+Left=2,第1個數值解碼爲8。

數值8左面還有一個數值,在1到7之間,表示7種選擇須要3位,讀出3位010,爲2,加上Low=1,第0個數值解碼爲3。

數值8右面還有一個數值,在9到10之間,表示2種選擇須要1位,讀出1位0,爲0,加上Low=9,第2個數值解碼爲9。

而後解碼<Length=3, low=12, high=20>,解碼中間的數值,也即第5個數值,因爲Left=1,Right=1,則這個數值一定從13到19,表示7中選擇須要3位,讀出3位000,爲0,加上low=13,第5個數值解碼爲13。

數值13左面還有一個數值,在12到12之間,一定是12,無需讀取,第4個數值解碼爲12。

數值13右面還有一個數值,在14到20之間,表示7種選擇須要3位,讀出3位011,爲3,加上low=14,則第6個數值解碼爲17。

解碼完畢。

9. Variable Byte編碼

上述全部的編碼方式有一個共同點,就是須要一位一位的進行處理,稱爲基於位的編碼(bitwise)。這樣一分錢一分錢的節省,的確符合我們勤儉持家的傳統美德,也能節約很多存儲空間。

然而在計算機中,數據的存儲和計算的都是以字(Word)爲單位進行的,一個字中包含的位數成爲字長,好比32位,或者64位。一個字包含多個字節(Byte),字節成爲存儲的基本單位。若是使用bitwise的編碼方法,則意味着在編碼和解碼過程當中面臨者大量的位操做,從而影響速度。

對於信息檢索系統來說,相比於存儲空間的節省,查詢速度尤其重要。因此咱們將目光從省轉移到快,基於字節編碼(Bytewise)是以一個或者多個字節(Byte)爲單位的。

最多見的基於字節的編碼就是變長字節編碼(Variable Byte),它的規則比較簡單,一個Byte共8個bit,其中最高位的1個bit表示一個flag,0表示這是最後一個字節,1表示這個數還沒完,後面還跟着其餘的字節,另外7個bit是真正的數值。

如圖所示,好比編碼120,表示成二進制是1111000沒有超過7個bit,因此用一個byte就能保存,最高位置0。若是編碼130,表示成二進制是10000010,已經有8個bit了,因此須要用兩個byte來保存,第一個byte保存第一個bit,最高位置1,接下來的一個byte保存剩下的7個bit,最高位置0。若是數值再大一些,好比20000,則須要三個byte才能保存。

clip_image106[4]

 

變長字節編碼的解碼也相對簡單,每次讀一個byte,而後判斷最高位,若是是0則結束,若是是1則再讀一個byte,而後再判斷最高位,直到遇到最高位爲0的,將幾個byte的數據部分拼接起來便可。

從變長字節編碼的原理能夠看出,相對於基於位的編碼,它是一次處理一個字節的,相應付出的代價就是空間有些浪費,好比130編碼後的第一個字節,原本就保存一個1,仍是用了7位。

變長字節編碼做爲基於字節的編碼方式,的確比基於位的編碼方式表現出來較好的速度。在Falk Scholer的論文《Compression of Inverted Indexes For Fast Query Evaluation》中,很好的比較了這兩種類型的編碼方式。

如圖所示,圖中的簡稱的意思是Del表示Delta編碼,Gam表示Gamma編碼,Gol表示Golomb編碼,Ric表示Rice編碼,Vby表示Variable Bytes編碼,D表示文檔ID,F表示Term的頻率Term Frequency,O表示Term在文檔中的偏移量Offset。GolD-GamF-VbyO表示用Golomb編碼來表示文檔ID,用Gamma編碼來表示Term頻率,用Vby來表示偏移量。

文中對大小兩個文檔集合進行的測試,從圖中咱們能夠看出變長字節編碼雖然在空間上有所浪費,然而在查詢速度上卻表現良好。

clip_image108[4]

 

10. PFORDelta編碼

變長字節編碼的一個缺點就是雖然它是基於byte進行編碼的,可是每解碼一個byte,都要進行一次位操做。

解決這個問題的一個思路就是,將多個byte做爲一組(Patch)放在一塊兒,將Flag集中起來,做爲一個Header保存每一個數值佔用幾個byte,一次判斷一組,咱們稱爲Signature block,如圖所示。

clip_image110[4]

 

對於Header中的8個bit,分別表示接下來8個byte的flag,前三個0表示前三個byte各編碼一個數值,接下來1表示下一個byte屬於第四個數值,而後接下來的1表示下一個byte也屬於第四個數值,接下來0表示沒有下一個byte了,於是110表示的三個byte編碼一個數值。最後10表示最後兩個byte編碼第五個數值。

細心的同窗可能看出來了,Header裏面就是一元編碼呀。

那麼再改進一下,在Header裏面我們用二進制編碼,每兩位組成一個二進制碼,這個二進制數表示每個數值的長度,長度的單位是一個byte,這樣兩位能夠表示32個bit,基本能夠表示全部的整數。00表示一個byte,01表示2個byte,10表示3個byte,11表示4個byte,這種方式稱爲長度編碼(Length Encoding),如圖。

clip_image112[4]

 

若是數比較大,32位不夠怎麼辦?用三位,那總共8位也不夠分的啊?因而有人改變思路,Header裏面的8位並不表示長度,而是8個flag,每一個flag表示是否可以壓縮到n個byte,n是一個參數,0表示能,則壓縮爲n個byte,1表示不能,則用原來的長度表示。這種方法叫作Binary Length Encoding。如同所示。

clip_image114[4]

 

這裏參數n=2,也即若是一個32位整數能壓縮爲2個byte,則壓縮,不然就用所有32位表示。好比第三個數字,其實用三位就可以表示的,可是因爲不能壓縮成爲2個byte,也是用完整的32位來表示的。

Binary Length Encoding已經爲將數值分組打包(Patch)壓縮提供了一個很好的思路,就是將數值分爲兩大部分,能夠壓縮的便打包處理,太大不能壓縮的可單獨處理。這種思想成爲PForDelta編碼的基礎。

然而Binary length Encoding是將能壓縮的和不能壓縮的混合起來存儲的,這實際上是不利於咱們批量壓縮和解壓縮的,必須一個數值一個數值的判斷。

而PForDelta作了改進,將兩部分的分開存儲。試想若是m個數值都是壓縮成b個bit的,就能夠打包在一塊兒,這樣批量讀出m*b個bit,一塊兒解壓即可。而其餘不可壓縮的,咱們放在另外的地方。只要咱們經過參數,控制b的大小,使得能壓縮的佔多數,不能壓縮的佔少數,批量處理完打包好的,而後再一個個料理不能打包的殘兵遊勇。可壓縮部分咱們成爲編碼區域(Code Section),不可壓縮的部分咱們成爲異常區域(Excepton Section)。

固然分開存儲有個很大的問題,就是不能保持原來數值列表的順序,而咱們要壓縮的倒排表是須要保持順序的。如同所示。

clip_image116[4]

 

一個最直接的想法是,如圖(a),在原來異常數值(Exception Value)出現的位置保留一個指針,指向異常區域中這個數值的位置。然而一個很大的問題就是這個指針只能佔用b個bit,每每不夠一個指針的長度。

另一個替代的方法是,如圖(b),咱們若是知道第一個異常數值出現的位置(簡稱異常位置),而且知道異常區域的起始位置,咱們能夠在b個bit裏面保存下一個異常位置的偏移量(由於偏移量爲0沒有意義,因此存放0表示距離1,存放i表示距離i+1),因爲編碼區域是密集保存的,因此b個bit每每夠用。解壓縮的時候,咱們先批量將整個編碼區域解壓出來,而後找到第一個異常位置,本來放在這個位置的數值應該是異常區域的第一個值,而後異常位置裏面解壓出3,說明第二個異常位置是當前位置加4,找到第二個異常位置後,本來放在這個位置的數值應該是異常區域的第二個值,以此類推。這個將異常位置串起來的鏈表咱們稱爲異常鏈。

然而若是很不幸,b個bit不夠用,下一個異常位置的偏移量超過了2b個bit。如圖(c),b=2,然而下一個異常位置距離爲7,2位放不開,咱們就須要在距離爲4的位置人爲插入一個異常位置,當前位置裏面寫11,異常位置裏面寫7-4-1=2,固然異常區域中也須要插入一個不存在的數值。這樣作的缺點是增長了無用的數值,下降了壓縮率,爲了減小這種狀況的出現,因此實踐中b不要小於4。

這就是PForDelta的基本思路。PForDelta,全稱Patched Frame Of Reference-Delta,其中壓縮後的n個bit,稱爲一個Frame,Patched就是將這些Frame進行打包,Delta表示咱們打包壓縮的是差值。

PForDelta是將數值分塊(Block)存儲的,一塊中能夠包含百萬個數值,大小能夠達到幾個Megabyte,通常的方法是,在內存中保存一個m個Megabyte的緩存區域(Buffer),而後不斷的讀取數據,將這個緩存區域按照必定的格式填滿,即可以寫入硬盤,而後再繼續壓縮。

塊內部的格式如圖所示。

clip_image118[4]

 

塊內部分爲四個部分,在圖中這四個部分是分開畫的,實際上是一個部分緊接着下一個部分的。編碼區域和異常區域之間可能會有一些空隙,下面介紹中會講到爲何。在圖中的例子裏面,咱們還假設32bit足夠保存原始數值。

第一部分是Header,裏面保存了這個塊的大小,好比1M,大小應該是32或者64的整數倍。另外保存了壓縮的數值所用的bit數爲b。

第二部分是Entry point的數組,有N項,每個Entry管理128個數值。每一項32位,其中前7位表示這個Entry管理的128個數值中第一個異常位置,後25位保存了這128個數值的異常區域的起始位置。這個數組的存在是爲了在一個塊中隨機訪問。

第三部分是編碼區域(Code Section),存放了一系列壓縮爲b個bit的數值,每128個被一個entry管理,總共有128*N個數值,數值是從前向後依次排放的。在這個部分中,異常位置是以異常鏈的形式串起來的。

第四部分是異常區域(Exception Section),存放不能壓縮,以32個bit存儲原始數值的部分。這一部分的數值是從後往前排放的。因爲每128個數值中異常數值的個數不是固定的,因此僅僅靠這部分不能肯定哪些屬於哪一個entry管理。在一個entry中,有指向起始位置的指針,而後根據編碼區域中的異常鏈,依次一個一個找異常數值。

編碼區域是從前日後增加的,異常區域是從後往前增加的,在緩存塊中,當中間的空間不足的時候,就留下一段空隙。爲了提升效率,咱們但願解壓縮的時候是字對齊(word align)的,也即但願一次處理32個bit。假設Header是佔用32個bit,每一個Entry也是32個bit,異常區域是32個bit一個數值的,然而編碼區域則不是,好比5個bit一個數值,假設一共有100個數值,則須要500個bit,不是32的整數倍,最後多餘20個bit,則須要填充12個0,而後使得編碼區域字對齊後再存放異常區域。索性咱們的設計中,一個entry是管理128個數值的,因此最後必定會是32的整數倍,必定是字對齊的,不須要填充,能夠保證寫入硬盤的時候,編碼區域和異常區域是緊密相鄰的。

PForDelta的字對齊和批量處理,意味着咱們已經從一個bit一個bit處理的我的手工業時代,到了機械大工業時代。如圖。在硬盤上是海量的索引文件,是由多個PForDelta塊組成的,在壓縮和解壓過程當中,須要有一部分緩存在內存中,而後其中一個塊能夠進入CPU Cache,每塊的結構都是32位對齊的,對於32位機器,寄存器也是32位的。因而咱們能夠想象,CPU就像一個卓別林扮演的工人,來了32個bit,處理完畢,接着下一個32位,流水做業。

clip_image120[4]

 

下面我們就經過一個例子,具體看一下PForDelta的壓縮和解壓方法。

咱們假設有如下266個數值:

Input = [26, 24, 27, 24, 28, 32, 25, 29, 28, 26, 28, 31, 32, 30, 32, 26, 25, 26, 31, 27, 29, 25, 29, 27, 26, 26, 31, 26, 25, 30, 32, 28, 23, 25, 31, 31, 27, 24, 32, 30, 24, 29, 32, 26, 32, 32, 26, 30, 28, 24, 23, 28, 31, 25, 23, 32, 30, 27, 32, 27, 27, 28, 32, 25, 26, 23, 30, 31, 24, 29, 27, 23, 29, 25, 31, 29, 25, 23, 31, 32, 32, 31, 29, 25, 31, 23, 26, 27, 31, 25, 28, 26, 27, 25, 24, 24, 30, 23, 29, 30, 32, 31, 25, 24, 27, 31, 23, 31, 29, 28, 24, 26, 25, 31, 25, 26, 23, 29, 29, 27, 30, 23, 32, 26, 31, 27, 27, 29, 23, 32, 28, 28, 23, 28, 31, 25, 25, 26, 24, 30, 25, 28, 26, 28, 32, 27, 23, 31, 24, 25, 31, 27, 31, 24, 24, 24, 30, 27, 28, 23, 25, 31, 27, 24, 23, 25, 30, 23, 24, 32, 26, 31, 28, 25, 24, 24, 23, 28, 28, 28, 32, 29, 27, 27, 29, 25, 25, 32, 27, 31, 32, 28, 27, 32, 26, 23, 26, 31, 24, 32, 29, 27, 27, 25, 31, 31, 24, 23, 32, 30, 28, 29, 29, 28, 32, 26, 26, 27, 27, 29, 24, 25, 31, 27, 30, 28, 29, 27, 31, 25, 26, 26, 30, 31, 29, 30, 31, 26, 24, 29, 28, 25, 30, 24, 25, 23, 24, 32, 23, 32, 24, 27, 28, 29, 27, 31, 28, 29, 29, 32, 25, 26, 27, 29, 23, 26]

根據上面說過的原理,足夠須要三個entry來管理。

首先在索引過程當中,這些數值是一個個到來的,通過初步的統計,發現數值32是最大的,而且佔到總數的10%多一點,因此咱們能夠將32做爲異常數值。其餘的數值都在0-31之間,用5個bit就能夠表示,因此b=5。

下面咱們就能夠開始壓縮了,咱們是一個entry一個entry的來壓縮的,因此128個數值爲一組,代碼以下:

//存放編碼區域壓縮前數值

int[] codes = new int[128];

//記錄每一個異常數值的位置,miss[i]表示第i個異常數值的位置

int[] miss = new int[numOfExceptions];

int numOfCodes = 0;

int numOfExcepts = 0;

int numOfJump = 0;

//第一個循環,構造編碼區,而且統計異常數值位置

//每128個數值一組,或者不夠128則剩下的一組

while(from < input.length && numOfCodes < 128){

//統計從上次遇到異常數值開始,遇到的普通數值的個數

numOfJump = (input[from] > maxCode)?0:(numOfJump+1);

//若是兩個異常數值之間的間隔太大,則必須認爲插入一個異常數值。maxJumpCode是指b=5的狀況下能表示的最大間隔31。之因此判斷numOfExcepts > 0,是由於第一個異常位置用7個bit保存在entry裏面,因此在哪裏均可以。

if(numOfJump > maxJumpCode && numOfExcepts > 0){

codes[numOfCodes] = -1;

miss[numOfExcepts] = numOfCodes;

numOfCodes++;

numOfExcepts++;

numOfJump = 0;

}

//編碼區域的構造。這個地方是最簡單的狀況,就是input的數值直接進入編碼區域,這裏還能夠用其餘的編碼方式(好比用Golomb)進行一次編碼。

codes[numOfCodes] = input[from];

//只有遇到異常數值的時候numOfExcepts才加一

miss[numOfExcepts] = numOfCodes;

numOfExcepts += (input[from] > maxCode)?1:0;

numOfCodes++;

from++;

}

//構造完編碼區域後,能夠對entry進行初始化,7位保存第一個異常位置,25位保存異常區域的起始位置。

int prev = miss[0];

entries[curEntry++]=prev << 25 | (curException & 0x1FFFFFF);

//第二個循環,構造異常鏈和異常區域

exceptionSection[curException--] = codes[prev];

for(int i=1; i < numOfExcepts; i++){

int cur = miss[i];

codes[prev] = cur - prev - 1;

prev = cur;

exceptionSection[curException--] = codes[cur];

}

codes[prev] = numOfCodes - prev - 1;

//最後將編碼區域壓縮,其中codes是壓縮前的數值,numOfCodes是數值的個數,codeSection是一個int數組,用於存放壓縮後的數值,curCode是當前codeSection能夠從哪一個位置開始寫入,bwidth=5

curCode += pack(codes, numOfCodes, codeSection, curCode, bwidth);

整個過程是兩次循環構造未壓縮的編碼區域和異常區域,以下面的表格所示。表格中每一列中上面的數值是input,下面的數值是未壓縮編碼區域數值,其中黃色的部分即是異常位置:

Entry 1的未壓縮編碼區域

image

image

Entry 2的未壓縮編碼區域,其中第214個異常位置和第248個異常位置中間隔了33個位置,沒法用5個bit表示,因而在第216個位置人爲插入一個異常位置,就是紅色的部分。

image

image

Entry 3的未壓縮編碼區域,原本input中只有266個數值,這裏又添加兩個0數值(綠色的部分)是爲何呢?由於每一個數值壓縮後將佔用5個bit,若是隻有11個數值的話共55位,而要求字對齊的話,須要64位,於是須要人爲添加9個0.

image

下面應該對編碼區域進行壓縮了,在大多數的實現中,壓縮代碼多少有些晦澀難懂。通常來講,會對每一種b有一個代碼實現,在這裏咱們舉例列出的是b=5的代碼實現。

整個過程咱們能夠想象成codeSection是一條條32位大小的袋子,而codes是一系列待壓縮的32位的物品,其中貨真價實的就5位,其餘都是水分(都是0),下面要作的事情就是把待壓縮的物品一件件拿出來,把有水分擠掉,而後往袋子裏面裝。

裝的時候就面臨一個問題,32不是5的整數倍,放6個還空着2位,放7個不夠空間,這循環怎麼寫啊?因此只能以最小公倍數32*5=160位爲一個處理批次,放在一個循環裏面,也即每一個循環處理5個袋子,32個物品,使得32個物品正好能放在5個袋子裏面。

//bwidth=5

private static int pack(int[] codes, int numOfCodes, int[] codeSection,

int curCode, int bWidth) {

int cur = 0;

// suppose bwidth = 5

// bwidth不必定能被32的整除,因此每32個一組,保證處理完之後,32*bwidth個bit,必定是字對齊的。

while (cur < numOfCodes) {

codeSection[curCode + 0] = 0;

codeSection[curCode + 1] = 0;

codeSection[curCode + 2] = 0;

codeSection[curCode + 3] = 0;

codeSection[curCode + 4] = 0;

//curCode + 0是第一個袋子,先放codes裏面從cur+0到cur+5六個物品後,還空着2位,因而把第七個物品前2位截出來,放進去。0x18二進制11000,做用就是最後5位保留前兩位,而後右移3位,就把前2位放到了袋子的最後2位。

codeSection[curCode + 0] |= codes[cur + 0] << (32 - 5);

codeSection[curCode + 0] |= codes[cur + 1] << (32 - 10);

codeSection[curCode + 0] |= codes[cur + 2] << (32 - 15);

codeSection[curCode + 0] |= codes[cur + 3] << (32 - 20);

codeSection[curCode + 0] |= codes[cur + 4] << (32 - 25);

codeSection[curCode + 0] |= codes[cur + 5] << (32 - 30);

codeSection[curCode + 0] |= (codes[cur + 6] & 0x18) >> 3;

//curCode+1是第二個袋子。剛纔第七個物品前2位被截了放在第一個袋子裏,那麼首先剩下的3位放在第二個袋子的開頭,0x07就是00111,也就是截取後三位。而後再放5個物品,還空着4位,因而第十三個物品截取前四位(0x1E二進制11110)。

codeSection[curCode + 1] |= (codes[cur + 6] & 0x07) << (32 - 3);

codeSection[curCode + 1] |= codes[cur + 7] << (32 - 3 - 5);

codeSection[curCode + 1] |= codes[cur + 8] << (32 - 3 - 10);

codeSection[curCode + 1] |= codes[cur + 9] << (32 - 3 - 15);

codeSection[curCode + 1] |= codes[cur + 10] << (32 - 3 - 20);

codeSection[curCode + 1] |= codes[cur + 11] << (32 - 3 - 25);

codeSection[curCode + 1] |= (codes[cur + 12] & 0x1E) >> 1;

//curCode + 2第三個袋子。先放第十三個物品剩下的1位(0x01二進制00001),而後再放入6個物品,最後空着1位。將第二十個物品的第1位截取出來(0x10二進制10000)放入。

codeSection[curCode + 2] |= (codes[cur + 12] & 0x01) << (32 - 1);

codeSection[curCode + 2] |= codes[cur + 13] << (32 - 1 - 5);

codeSection[curCode + 2] |= codes[cur + 14] << (32 - 1 - 10);

codeSection[curCode + 2] |= codes[cur + 15] << (32 - 1 - 15);

codeSection[curCode + 2] |= codes[cur + 16] << (32 - 1 - 20);

codeSection[curCode + 2] |= codes[cur + 17] << (32 - 1 - 25);

codeSection[curCode + 2] |= codes[cur + 18] << (32 - 1 - 30);

codeSection[curCode + 2] |= (codes[cur + 19] & 0x10) >> 4;

//curCode + 3第四個袋子。先放第二十個物品剩下的4位(0x0F二進制位01111)。而後放5個物品,最後還空着3位。將第二十六個物品截取3位放入(0x1C二進制11100)。

codeSection[curCode + 3] |= (codes[cur + 19] & 0x0F) << (32 - 4);

codeSection[curCode + 3] |= codes[cur + 20] << (32 - 4 - 5);

codeSection[curCode + 3] |= codes[cur + 21] << (32 - 4 - 10);

codeSection[curCode + 3] |= codes[cur + 22] << (32 - 4 - 15);

codeSection[curCode + 3] |= codes[cur + 23] << (32 - 4 - 20);

codeSection[curCode + 3] |= codes[cur + 24] << (32 - 4 - 25);

codeSection[curCode + 3] |= (codes[cur + 25] & 0x1C) >> 2;

//curCode + 4第五個袋子。先放第二十六個物品剩下的2位。最後這個袋子還剩30位,正好放下6個物品。

codeSection[curCode + 4] |= (codes[cur + 25] & 0x03) << (32 - 2);

codeSection[curCode + 4] |= codes[cur + 26] << (32 - 2 - 5);

codeSection[curCode + 4] |= codes[cur + 27] << (32 - 2 - 10);

codeSection[curCode + 4] |= codes[cur + 28] << (32 - 2 - 15);

codeSection[curCode + 4] |= codes[cur + 29] << (32 - 2 - 20);

codeSection[curCode + 4] |= codes[cur + 30] << (32 - 2 - 25);

codeSection[curCode + 4] |= codes[cur + 31] << (32 - 2 - 30);

//處理下一組

cur += 32;

curCode += 5;

}

int numOfWords = (int) Math.ceil((double) (numOfCodes * 5) / 32.0);

return numOfWords;

}

通過壓縮後,整個block的格式以下,整個block被組織成int數組,[]裏面的數值爲下標:

Header:這部分只佔用32位,在這裏包含四部分,第一部分5位,表示b=5。第二部分表示Entry部分的長度,佔用了3個32位,也即有三個Entry。第三部分表示編碼區域的長度,佔用了42個

image

image

Entry列表:包含3個entry,每一個entry佔用32位,前7位表示第一個異常位置,後25位表示這個entry在異常區域中的起始位置。

image

編碼區域。總共佔用42個int,每5個int能夠存放32個壓縮後的編碼(每一個編碼佔用5位)。第三個entry共佔用兩個int,保存了11個數值佔用55位,另外人爲補充9個0.

image

異常區域。在塊中,異常區域是從後向前延伸的。其中從74到60的紅色部分屬於Entry 1,從59到50的黃色部分屬於Entry 2,綠色部分屬於Entry 3。

image

當須要讀取這個快的時候,便須要對這個塊進行解碼。

首先經過解析Header來獲得全局信息。

接下來須要讀取Entry列表,來解析每一個Entry中的信息。

而後對於每一個Entry進行解碼,代碼以下:

//解析entry,獲得全局信息

int entrycode = entries[i];

int firstExceptPosition = entrycode >> 25;

int curException = entrycode & 0x1FFFFFF;

//進行解壓縮,將編碼區域5位一個數值解壓爲一個int數組。Codes就是解壓後的數組,tempCodeSection指向編碼區域這個Entry的起始位置,numOfCodes是須要解壓的數值的個數,bwidth=5.

Unpack(codes, numOfCodes, tempCodeSection, bwidth);

//第一個循環將異常數值還原

int cur = firstExceptPosition;

while (cur < numOfCodes && curException >= lastException) {

int jump = codes[cur];

codes[cur] = block[curException--];

cur = cur + jump + 1;

}

//第二個循環輸出結果而且跳過人爲添加的異常數值

for (int j = 0; j < codes.length; j++) {

if (codes[j] > 0) {

output[curOutput++] = codes[j];

}

}

對編碼區域的解壓方式也正好是壓縮方式的逆向過程。是從袋子裏面將物品拿出來的時候了。

private static void Unpack(int[] codes, int numberOfCodes, int[] codeSection,

int bwidth) {

int cur = 0;

int curCode = 0;

while (cur < numberOfCodes) {

//從第一個袋子中,拿出6個物品,還剩2位。

codes[cur + 0] = (codeSection[curCode + 0] >> (32 - 5)) & 0x1F;

codes[cur + 1] = (codeSection[curCode + 0] >> (32 - 10)) & 0x1F;

codes[cur + 2] = (codeSection[curCode + 0] >> (32 - 15)) & 0x1F;

codes[cur + 3] = (codeSection[curCode + 0] >> (32 - 20)) & 0x1F;

codes[cur + 4] = (codeSection[curCode + 0] >> (32 - 25)) & 0x1F;

codes[cur + 5] = (codeSection[curCode + 0] >> (32 - 30)) & 0x1F;

codes[cur + 6] = (codeSection[curCode + 0] << 3) & 0x18;

//第一個袋子中的2位和第二個袋子中的前3位組成第7個物品。而後接着從第二個袋子中拿出5個物品,剩下4位。

codes[cur + 6] |= (codeSection[curCode + 1] >> (32 - 3)) & 0x07;

codes[cur + 7] = (codeSection[curCode + 1] >> (32 - 3 - 5)) & 0x1F;

codes[cur + 8] = (codeSection[curCode + 1] >> (32 - 3 - 10)) & 0x1F;

codes[cur + 9] = (codeSection[curCode + 1] >> (32 - 3 - 15)) & 0x1F;

codes[cur + 10] = (codeSection[curCode + 1] >> (32 - 3 - 20)) & 0x1F;

codes[cur + 11] = (codeSection[curCode + 1] >> (32 - 3 - 25)) & 0x1F;

codes[cur + 12] = (codeSection[curCode + 1] << 1) & 0x1E;

//第二個袋子的最後4位和第三個袋子的前1位組成一個物品,而後在第三個袋子裏面拿出6個物品,剩下1位。

codes[cur + 12] |= (codeSection[curCode + 2] >> (32 - 1)) & 0x01;

codes[cur + 13] = (codeSection[curCode + 2] >> (32 - 1 - 5)) & 0x1F;

codes[cur + 14] = (codeSection[curCode + 2] >> (32 - 1 - 10)) & 0x1F;

codes[cur + 15] = (codeSection[curCode + 2] >> (32 - 1 - 15)) & 0x1F;

codes[cur + 16] = (codeSection[curCode + 2] >> (32 - 1 - 20)) & 0x1F;

codes[cur + 17] = (codeSection[curCode + 2] >> (32 - 1 - 25)) & 0x1F;

codes[cur + 18] = (codeSection[curCode + 2] >> (32 - 1 - 30)) & 0x1F;

codes[cur + 19] = (codeSection[curCode + 2] << 4) & 0x10;

//第三個袋子的最後1位和第四個袋子的前4位組成一個物品,而後從第四個袋子中拿出5個物品,剩下3位。

codes[cur + 19] |= (codeSection[curCode + 3] >> (32 - 4)) & 0x0F;

codes[cur + 20] = (codeSection[curCode + 3] >> (32 - 4 - 5)) & 0x1F;

codes[cur + 21] = (codeSection[curCode + 3] >> (32 - 4 - 10)) & 0x1F;

codes[cur + 22] = (codeSection[curCode + 3] >> (32 - 4 - 15)) & 0x1F;

codes[cur + 23] = (codeSection[curCode + 3] >> (32 - 4 - 20)) & 0x1F;

codes[cur + 24] = (codeSection[curCode + 3] >> (32 - 4 - 25)) & 0x1F;

codes[cur + 25] = (codeSection[curCode + 3] << 2) & 0x1C;

//第四個袋子剩下的3位和第五個袋子的前2位組成一個物品,而後第五個袋子取出6個物品。

codes[cur + 25] |= (codeSection[curCode + 4] >> (32 - 2)) & 0x03;

codes[cur + 26] = (codeSection[curCode + 4] >> (32 - 2 - 5)) & 0x1F;

codes[cur + 27] = (codeSection[curCode + 4] >> (32 - 2 - 10)) & 0x1F;

codes[cur + 28] = (codeSection[curCode + 4] >> (32 - 2 - 15)) & 0x1F;

codes[cur + 29] = (codeSection[curCode + 4] >> (32 - 2 - 20)) & 0x1F;

codes[cur + 30] = (codeSection[curCode + 4] >> (32 - 2 - 25)) & 0x1F;

codes[cur + 31] = (codeSection[curCode + 4] >> (32 - 2 - 30)) & 0x1F;

cur += 32;

curCode += 5;

}

}

11. Simple Family

另外一種多個數值打包在一塊兒,而且是字對齊的編碼方式,就是下面咱們要介紹的Simple家族。

對於32位機器,一個字是32個bit,咱們但願這32個bit裏面能夠放多個數值。好比1位的放32個,2位的放16個,3位放10個,不過浪費2位,4位放8個,5位放6個,6位放5個,7位和8位都是4個算同樣,9位,10位都是放3個,11位,12位一直到16位都是放2個,32位放1個,共10種方法。那麼來了32位,咱們怎麼區分裏面是哪一種方法呢?在放置真正的數據以前,須要放置一個選擇器(selector),來表示咱們是怎麼存儲的,10種方法4位就夠了。

若是這4位另外存儲,就不是字對齊的了,因此仍是將這4位放在32位裏面,這樣數據位就剩了28位了。那這28位怎麼安排呢?1位的28個,2位的14個,3位的9個,4位的7個,5位的5個,6位和7位的4個,8位和9位的3個,10位到14位的都是2個,15位到28位的都是1個,共9種。若是一樣存儲2個,固然選最長的位數14,因此造成如下的表格:

image

因爲一共9種狀況,因此這種編碼方式稱爲Simple-9。

4位selector來表示9,太浪費了,浪費了差很少一半的編碼(24=16),若是少一種狀況,8種的話,就少一位作selector,多一位保存數值了,好比去掉selector=e這種狀況,全部5位的都保存成7位,這樣保存數據就是29位了,很遺憾,29是個質數,除了1和自己不能被任何數整除,多出的一位也每每浪費掉。

因而咱們再進一步,用30位來保存數值,這樣會有10種狀況,並且浪費的狀況也減小了。以下面的表格所示:

image

 

雖然看起來30比28要好一些,然而剩下的兩位如何表示10種狀況呢?咱們假設文檔編號之間的差值不會劇烈抖動,一下子大一下子小,而是維持在一個穩定的水平,就算髮生忽然的變化,變化完畢後也能保持較穩定的水平。因此咱們能夠採起這樣的策略,首先對於第一個32位,假設selector是某一個數r,好比r=f,6位保存一個數值,那麼下一個32位開始的兩位表示四種狀況:

1) 若是用的位數少,好比3位就夠,則selector=r-1=e,也即用5位保存

2) 若是用的位數不變,仍是用6位保存,則selector=r

3) 若是用的位數多 ,selector=r+1=g,也即用7位保存

4) 若是r+1=7位放不下,好比須要10位,說明了變化比較忽然,因而r取最大值j,用30位來保存。

一旦出現了忽然的較大變化,則會使用最大的selector j,而後隨着忽然變化後的慢慢平滑,selector還會降到一個文檔的值。固然,若是事先通過統計,發現最大的文檔間隔也不過須要15位,則對於第四種狀況,可使得r=i。

這種用相對的方式來表示10種狀況,成爲Relative-10。

既然selector只須要2位,那麼上面的表格中selector=d和selector=g的沒有用的兩位,不是也能夠成爲下一個32位的selector麼?這樣下一個整個32位均可以來保存數據了。

另外上面表格中每一個數值的編碼長度一欄爲何會從7跳到10呢?是由於8位,9位,10位都只能保存3個,固然用最大的10位了。然而若是沒有用的2位能夠留給下一個32位作selector,那就有必要作區分了,好比一個值只須要9位,我們就用9位,三九二十七,剩下的三位中的兩位做爲下一個32位的selector。另外14位和15位也是這個狀況,若是兩個14位,就能夠剩下兩位做爲下一個的selector,若是是兩個15位就沒有給下一個剩下什麼。

這樣selector由10種狀況變成了12種狀況,如圖下面的表格所示:

image

image

 

若是上一個32位剩下2位做爲selector,則當前的32位則能夠所有用於保存數據。固然也會有剩餘,可留給後來人。那麼32位所有用來保存數據的狀況下,selector應該以下面表格所示:

image

 

這樣每一個32位都分爲兩種狀況,一種是上一個留下了2位作selector,自己32位都保存數據,另外一種是上一個沒有留下什麼,自己2位作selector,30位作數據。

這種上一個32位爲下一個留下遺產,共12種狀況的,成爲Carryover-12編碼。

下面舉一個具體的例子,好比咱們想編碼以下的輸入:

int[] input = {5, 30, 120, 60, 140, 160, 120, 240, 300, 200, 500, 800, 300, 900};

首先定義上面的兩個編碼表:

class Item {

public Item(int lengthOfCode, int numOfCodes, boolean leftForNext) {

this.lengthOfCode = lengthOfCode;

this.numOfCodes = numOfCodes;

this.leftForNext = leftForNext;

}

int lengthOfCode;

int numOfCodes;

boolean leftForNext;

}

static Item[] noPreviousSelector = new Item [12];

static {

noPreviousSelector[0] = new Item(1, 30, false);

noPreviousSelector[1] = new Item(2, 15, false);

noPreviousSelector[2] = new Item(3, 10, false);

noPreviousSelector[3] = new Item(4, 7, true);

noPreviousSelector[4] = new Item(5, 6, false);

noPreviousSelector[5] = new Item(6, 5, false);

noPreviousSelector[6] = new Item(7, 4, true);

noPreviousSelector[7] = new Item(9, 3, true);

noPreviousSelector[8] = new Item(10, 3, false);

noPreviousSelector[9] = new Item(14, 2, true);

noPreviousSelector[10] = new Item(15, 2, false);

noPreviousSelector[11] = new Item(28, 1, true);

}

static Item[] hasPreviousSelector = new Item [12];

static {

hasPreviousSelector[0] = new Item(1, 32, false);

hasPreviousSelector[1] = new Item(2, 16, false);

hasPreviousSelector[2] = new Item(3, 10, true);

hasPreviousSelector[3] = new Item(4, 8, false);

hasPreviousSelector[4] = new Item(5, 6, true);

hasPreviousSelector[5] = new Item(6, 5, true);

hasPreviousSelector[6] = new Item(7, 4, true);

hasPreviousSelector[7] = new Item(8, 4, false);

hasPreviousSelector[8] = new Item(10, 3, true);

hasPreviousSelector[9] = new Item(15, 2, true);

hasPreviousSelector[10] = new Item(16, 2, false);

hasPreviousSelector[11] = new Item(28, 1, true);

}

造成的編碼格式如圖

clip_image122[4]

 

假設約定的起始selector爲6,也即表3-10的g行。對於第一個32位,是不會有前面遺傳下來的selector的,因此前兩位表示selector=1,也即就用原始值6,第g行,接下來應該是4個7位的數值,最後剩餘兩位做爲下一個32位的selector。Selector=2,因此用6+1=7,也即表3-11的h行,下面的整個32位都是數值,保存了4個8位的,沒有遺留下什麼。接下來的32位中,頭兩位是selector=1,仍是第7行,也即第h行,只不過是表3-10中的,因此接下來應該是3個9位,遺留下最後兩位selector=2,也便是7+1=8行,也即表3-11的第i行,接下來應該是3個10位的。

整個解碼的過程以下:

//block是編碼好的塊,defaultSelector是默認的selector

private static int[] decompress(int[] block, int defaultSelector, int numOfCodes) {

//當前處理到編碼塊的哪個int

int curInBlock = 0;

//解碼結果

int[] output = new int[numOfCodes];

int curInOutput = 0;

//前一個selector,用於根據相對值計算當前selector的值

int prevSelector = defaultSelector;

int curSelector = 0;

//最初的編碼表用固然是沒有遺留的

Item[] curSelectorTable = noPreviousSelector;

//還沒有處理的bit數

int bitsRemaining = 0;

//當前int中每一個編碼的bit數

int bitsPerCode = curSelectorTable[curSelector].lengthOfCode;

//當前要解碼的32位編碼

int cur = 0;

//一個循環,當curInBlock > block.length的時候,編碼塊已經處理完畢,可是還須要等待最後一個32位處理完畢,當bitsRemaining大於等於bitsPerCode的時候,說明還有沒處理完的編碼

while (curInBlock < block.length || bitsRemaining >= bitsPerCode) {

//當bitsRemaining不足一個編碼的時候,說明當前的32位處理完畢,應該讀入下一個32位了。

if(bitsRemaining < bitsPerCode){

//當bitsRemaining小於2,說明當前32位剩下的不足2位,不夠給下一個作selector的,於是下一個selector在下一個32位的頭兩位。

if(bitsRemaining < 2){

//取下一個32位

cur = block[curInBlock++];

//前兩位爲selector

int selector = (cur >> 30) & 0x03;

//根據selector的相對值計算當前的selector

if(selector == 0){

curSelector = prevSelector - 1;

} else if (selector == 1){

curSelector = prevSelector;

} else if (selector == 2) {

curSelector = prevSelector + 1;

} else {

curSelector = curSelectorTable.length - 1;

}

prevSelector = curSelector;

//當前32位中數據僅僅佔30位

bitsRemaining = 30;

//使用編碼表3-10

curSelectorTable = noPreviousSelector;

}

//若是bitRemaining大於等於2,足夠給下一個作selector,則解析最後兩位爲selector。

else {

int selector = cur & 0x03;

if(selector == 0){

curSelector = prevSelector - 1;

} else if (selector == 1){

curSelector = prevSelector;

} else if (selector == 2) {

curSelector = prevSelector + 1;

} else {

curSelector = curSelectorTable.length - 1;

}

prevSelector = curSelector;

//取下一個32位,所有用於保存數值

cur = block[curInBlock++];

bitsRemaining = 32;

//使用編碼表3-11

curSelectorTable = hasPreviousSelector;

}

bitsPerCode = curSelectorTable[curSelector].lengthOfCode;

}

//在bitRemaing中截取一個編碼,解碼到輸出,更新bitsRemaining

int mask = (1 << bitsPerCode) - 1;

output[curInOutput++] = (cur >> (bitsRemaining - bitsPerCode)) & mask;

bitsRemaining = bitsRemaining - bitsPerCode;

}

return output;

}

12. 跳躍表

上面說了不少的編碼方式,可以讓倒排表又小又快的存儲和解碼。

可是對於倒排表的訪問除了順序讀取,還有隨機訪問的問題,好比我想獲得第31篇文檔的ID。

第一點,差值編碼使得每一個文檔ID都很小,是個不錯的選擇。第二點,上面所述的不少編碼方式都是變長的,一個挨着一個存儲的。這兩點使得隨機訪問成爲一個問題,首先由於第二點咱們根本不知道第30篇文檔在文件中的什麼位置,其次就算是找到了,由於第二點,找到的也是差值,須要知道上一個才能知道本身,然而上一個也是差值,難不成每訪問一篇文檔整個倒排表都解壓縮一遍?

看過前面前端編碼的同窗可能想起了,差值和前綴這不差很少的概念麼?弄幾個排頭兵不就行啦。

如圖,上面的倒排表被用差值編碼表示成了下面一行的數值。咱們將倒排表分紅組,好比3個數值一組,每組有個排頭兵,排頭兵不是差值編碼的,這樣若是你找第31篇文檔,那它確定在第10組裏面的第二個,只須要解壓一個組的就能夠了。

clip_image124[4]

 

有了跳躍表,根據文檔ID查找位置也容易了,好比要在文檔57,在排頭兵中能夠直接跳過17,34,45,確定在52的組裏,發現52+5=57,一進組就找到了。

固然排頭兵若是比較大,也能夠用差值編碼,是基於前一個排頭兵的差值,因爲排頭兵比較少,反正查找的時候要一個個看,都解壓了也沒問題。

若是鏈表比較長,致使排頭兵比較多,沒問題還能夠有多級跳躍表,排頭兵還有排頭兵,排長上面是連長。這樣查找的時候,先查找連長,找到所在的連再查找排長,而後再進組查找。

上面提到的PForDelta的編碼方式能夠和跳躍表進行整合,因爲PForDelta的編碼區域是定長的,基本能夠知足隨機訪問,然而對於差值問題,能夠再entry中添加排頭兵的值的信息,使得128個數值成爲一組。

咱們姑且稱這種方式爲跳躍表規則。

相關文章
相關標籤/搜索