當待搜索的數據量極爲龐大時,數據所對應的索引的數據量也會非常大。就拿最常見的倒排索引來說,特別是當用戶查詢的關鍵詞是常用詞時,這些詞所對應的倒排列表可以達到幾百兆,而將這樣龐大的索引由磁盤讀入內存,勢必會嚴重增加檢索響應時間,影響用戶的搜索體驗。爲了解決這樣的問題,學者們提出了一系列的索引壓縮技術。
實際上,我們所要處理的數據類型多如牛毛,根據不同的要求,爲這些數據設計的索引更是千變萬化,最常見的有倒排索引,複雜一點的還有各種樹形索引等等。要想總結出一種萬能的索引壓縮技術,實在是很難。但是壓縮方法的基本原理卻是相通的。本文,我將以倒排索引爲例,介紹幾種簡單的索引壓縮技術。之所以選擇倒排索引,除了它通用性強之外,也是由於其具有普遍性:倒排索引由以下兩部分構成:
- 詞典,其實就是由字符串構成的列表;
- 倒排列表,其實就是由一系列數字;
其他類型索引,不過都是由字符以及數字構成的,所以說從倒排索引的壓縮也就能延伸出對於其他索引的壓縮方法。
詞典壓縮
下表是一個典型的倒排索引,由單詞;文檔頻率(DF);倒排列表指針;3個部分組成
其中,詞典部分的存儲會浪費空間的根本原因在於分配給單詞的空間是統一的,也就是說不論你的單詞是像”we”這樣短的,還是像”confidentiality”這樣長的,都必須分配能夠容納最長單詞的空間。所以比較直接的壓縮方法是將這些單詞連續地存儲在一個區域中,而倒排列表中只是存儲這些單詞出現的起始位置。如下圖所示:
顯然,這種做法使得詞典中不存在冗餘的空間了,所有的單詞相當於被合成爲一個整的字符串。當然,當單詞數量極大的時候,還可以通過存儲單詞位置之間的差值來替代真實的單詞位置,以降低存儲單詞位置的空間消耗。比如原本單詞位置應該是”1,5,10,13,20…”,可以存儲爲”1,4,5,3,7…”。
更進一步,我們可以將上述詞典壓縮技術改進。還是把單詞連成一個整體存儲,只不過存儲前,對單詞分組,比如兩兩一組。只在倒排索引中,爲每兩個單詞存儲他們的起始位置,如下:
實際操作時,可以根據單詞的長度動態對單詞分組。查找倒排索引時,先根據位置信息讀取這個單詞分組,再進一步讀取每個單詞。從而實現對單詞索引的進一步壓縮。這裏面存在的問題是:如何區分一個單詞分組中的所有單詞,一般的做法是在每個單詞結尾處標記一個終止符,比如上圖中,我用’$’號分割這些單詞。
倒排列表壓縮
倒排列表一般的內容包括:文檔編號、詞頻、詞位置。一個3部分信息。而這3部分信息基本都是(或者說可以轉化爲)整數。所以對於這部分數據的壓縮,基本上是根據以下2個原則進行:
對於遞增整數序列,我們一般存儲其數值之間的差值,而不是數值本身(這一點在上面詞典壓縮時已經用到了)。比如倒排列表中,文檔編號和詞位置一般都是遞增的整數序列。
對於整數,採用合適的壓縮算法,編碼數據。
第一點不再贅述,我在這裏主要談一下第二點。首先介紹一下在壓縮算法中常用的編碼技術,一般是兩種:一元編碼和二進制編碼,這兩類編碼是壓縮算法的基礎構件,因爲本文中我們涉及的壓縮算法,無論其內部工作原理如何,都最終將數字表示成這兩類編碼的混合。
一元編碼(Unary Code):一般是對於大於0的整數使用。對於整數
X>1 ,編碼結果由X−1 個二進制數字1和最末尾的二進制數字0構成。例如對於3,編碼爲110
;對於5,編碼爲11110
。顯然這種編碼方式適用於小整數,對於大整數,實在是相當不經濟。二進制編碼(Binary Code):這個大家很熟悉了,就是將整數轉化爲二進制字符串。
瞭解這兩種編碼之後,可以看一些經典的壓縮算法了。由於壓縮算法實在很多,其基本思想又都比較相似。所以我只介紹下面兩類。
1. Elias-γ 算法 和 Elias-δ 算法
Elias提出了兩種壓縮算法,通過分解函數將待壓縮的數字分解成2個因子,之後分別利用一元編碼和二進制編碼表示這2個因子。
(1) Elias-γ 算法
Elias-
其中,1110
,001
,兩個編碼之間,以:
分隔,最後得到數字9的Elias-1110:001
(2) Elias-δ 算法
Elias-:
結合起來。比如,還是以數字9爲例,對於110:00
,001
,結合起來,得到最終結果110:00:001
。
根據這兩種算法的計算過程,其實不用通過論證我們也能看出Elias-
2. Golomb算法 和 Rice算法
Golomb和Rice算法在原理上與Elias提出的兩個算法一致,都是通過分解函數將大整數分解成2個因子,再對這兩個因子編碼,區別在於分解函數的不同。對於待壓縮整數
其中
(1) Golomb算法
參數
b 的選擇:Golomb和Rice算法的不同在於對參數b 的選擇。Golomb算法對於b 的選擇爲:b=ln(2)×Avg ,其中Avg 爲待壓縮數值序列的平均數,也就是說Golomb和Rice算法不僅僅考慮壓縮數值本身,而是考慮整個待壓縮的數值序列;ln(2) 是一個經驗參數(一般取0.69),當然計算得到的b 應該取整數。舉個例子,一個待壓縮序列{14,144,113,182}
平均值爲113(取整之後),此時,計算得到b=0.69×113=77 .二進制編碼長度的選擇:這是Golomb算法中1相對複雜的部分了。因爲我們要對
f2∈0,1,…,b−1 進行二進制編碼,所以f2 的編碼長度一定不會超過⌈log(b)⌉ . 因此,Golomb算法按如下規則編碼f2 :If
f2<2⌊log(b)−1⌋ ,設定f2 的編碼長度爲⌊log(b)⌋ ,不足位補0;If
f2≥2⌊log(b)−1⌋ ,設定f2 的編碼長度爲⌈log(b)⌉ ,其中,第一位置爲1,其他位正常二進制編碼成⌊log(b)⌋ 長;
壓縮實例:還是上面的待壓縮序列
{14,144,113,182}
,其平均值爲113,b=77 ,根據分解函數,得到:
根據
對001110
。最後壓縮得到的14的編碼結果爲0:001110
.
同理,編碼這個序列中的144的步驟如下:
因爲1100010
。綜上,對144壓縮的結果爲10:1100010
。
注:此處,對Golomb算法的介紹,我參考了張俊林《這就是搜索引擎》,以及Ricardo《Modern Information Retrieval》,他們兩者對Golomb算法的具體細節方面有些出入,我以後者爲準。其實具體細節沒有必要太過於糾結,重點還是在於理解這種通過分解後編碼壓縮數據的思路。
(2) Rice算法
Rice算法與Golomb算法操作基本一致,唯一一點不同在於對
b 爲2的整數次冪b 爲小於Avg的數中最大的
還是上面Golomb算法算法中的例子,此時選取
Golomb算法(Rice算法)的缺點在於無法通過對文檔的一次遍歷就獲得壓縮之後的索引。因爲我們在壓縮前必須提前知道待壓縮序列的平均值。而其優點也是很明顯的,動態處理編碼長度的方式使得壓縮效果更好。
壓縮算法評估
最後,看看對壓縮算法的優劣我們根據什麼樣的指標評估。一般來講,有以下3方面:
壓縮率:壓縮前大小與壓縮後大小的比例關係,這個意義很明顯。
壓縮速度:壓縮算法的運行時間。這個指標其實不算很重要了,原因很明顯,因爲壓縮索引是一次性的預計算,它不影響對用戶查詢的即時響應。即使時間較長,也不要緊。
解壓速度:將壓縮後的數據恢復成原始數據所消耗的時間。這是3個指標中最重要的。因爲在實際查詢發生時,需要從磁盤將壓縮數據讀入內存,解壓,搜索,最後返回結果,它直接關乎查詢響應時間。