這篇就說一個資訊檢索裡面理解最簡單的一個東西吧,它就叫做倒排表或者倒排索引。但是這隻是個名字,我想大家都知道它是什麼就行了,不必糾結於名稱。先說說倒排表張什麼樣子吧!
倒排表以詞做索引,內容為包含該詞的文檔編號。對於可知,文檔1、3、5、7、9包含詞"Cat",文檔2、5、8、10包含詞"Dog"。你可能問這麼簡單的東西能幹啥?其實他就是搜尋引擎中的最關鍵的核心資料結構。那麼搜尋引擎如何根據使用者的查詢來找到相關的文檔呢?如果使用者查詢“Cat”,那麼只要順著Cat鏈把文檔1、3、5、7、9返回給使用者就行了。如果使用者想得到同時包含“Cat”與“Dog“的文檔怎麼辦?這個過程就類似於我們歸併排序時合并兩個有序段的過程,利用兩個指標分別指向Cat鏈的第一個元素和Dog鏈的第一個元素,之後通過比較兩個指標處文檔編號的大小進行相應的移動,找到兩個鏈中共同的文檔。整個過程的時間複雜度是線性。上面介紹的是一種簡化版的倒排表,實際搜尋引擎中使用的倒排表要比這個複雜一些,也有可能使用多個不同的倒排表來完成不同的搜尋任務,但是本質上都差不多。
那麼如何建立這個倒排表呢?其實大概過程很簡單:提出文檔中的每一個詞,並把該文檔編號插入到該詞索引的鏈上。這裡的主要問題在於提取詞上,英文文檔詞與詞之間是由空格以及標點符號隔開的,但是漢語裡面沒有這麼明顯的分割標誌,所以有一門學問叫做自然語言處理,裡面專門研究了漢語的分詞方法,現在的大多數分詞方法都能夠達到90%多的正確率。
對於邏輯上這麼容易理解倒排表,在實際上實現並不容易。除了分詞外,不容易就不容易在這個倒排表的規模將是相當之大,以至於一台電腦的記憶體空間甚至一台電腦的磁碟空間都不足以存下整個倒排表。實際中的搜尋引擎索引的網頁數目可達到百億,以每個網頁平均1000個詞來計算,每個文檔編號按照4位元組整數來儲存,那麼粗略算下來倒排表也至少需要10000000000*1000*4B=40TB的儲存空間,那麼對於怎麼儲存這個倒排表確實是一個棘手的問題。
解決大資料存放區的形式有兩種,一種是分布式儲存,一種是壓縮。前一種方式很好理解,意思就是一台機器存不下的東西,那麼就把它存在多台機器上,這裡面也需要有一些複雜的技術。當然,如果倒排表格儲存體在多台機器上,對使用者的請求的處理也就變得複雜了一些。後一種壓縮技術就是把資料的規模變小,用更加緊湊的表現形式對來源資料進行表示,達到內容不變但是儲存空間變小的目的,實際中我們常用的壓縮軟體就是這個道理。比如Linux下的Gzip利用的就是LZ77演算法和哈夫曼曼編碼來對任意的位元據進行壓縮的。
分布式儲存本人不太瞭解,所以這些技術的介紹就得大家自己找找園子裡的牛人的部落格了。而資料壓縮相對比較簡單,本文就介紹一下倒排表的壓縮演算法。有人會說,那用現成的壓縮公用程式對倒排表壓縮不就可以了嗎?雖然可以減少倒排表的儲存空間,但是這種平時用的壓縮公用程式壓縮出的資料的訪問效率比較低。
例如:“我是中國人”在Gzip壓縮之後,在壓縮資料中找是否有“中”字,你就不得不把所有的資料解壓之後才能得到。如果是在壓縮之後的倒排表中去找一個文檔,那麼不得不把倒排表全部解壓,而這個解壓過程本身就是一個費時的過程,所以不划算。
現在回到倒排表的壓縮問題上來,解決一個問題必須理論聯絡實際才能得到令人滿意的效果。
在介紹倒排表資料壓縮演算法之前,先介紹幾個常用的技術,也能夠有效減少倒排表的大小。
這裡先從實際應用背景作為切入點來最佳化倒排表。這個系統是給人用的,那麼一個人會輸入什麼樣的查詢詞呢?
對於英文來說,會不會有人去查”the”,”a”, “an”等等這些詞?顯然這些詞沒有什麼實際意義。人們往往更喜歡查詢一些名字、動詞。所以倒排表沒有必為”the”,”a”, “an”這些詞建立索引,這樣倒排表的規模會明顯減少不小。”the”, “a”, “an”這種詞叫做停用詞。有人專門去研究什麼樣的詞可以當成停用詞,以至於找到了幾百個可以認為是停用詞的詞。而這些詞我們會經常用到。大家可以隨便找一篇英文文章,把其中所有的停用詞去掉,之後看看還有多少個詞。所以不對停用詞做索引能夠有效減少倒排表的大小。漢語也有相應的停用詞表,所以這個技術對中文檢索也同樣適用。
還有一個技術叫做詞幹化。這個技術貌似只對歐洲的文字才有作用,對漢字作用不大。這裡也介紹一下。比如單詞“cars”是“car”的複數形式,但是對於我們來說這兩個詞的意思都一樣,所以把“cars”用“car”代替是一個好方法。“car”可以認為是“cars”的詞幹。實際上一個詞的單複數、時態等都能夠進行詞幹化。這種方法甚至比去除停用詞的效果還要好。大家也可以試試隨便找一篇文章,之後把所有的詞都詞幹化看看還有多少個不同的詞。和上面的技術一樣,也有人專門研究英文單詞的詞幹化,提供詞幹化的服務。
以上兩個技術聯合起來用就可以很大程度上的減少倒排表的大小,雖然這兩個技術也對檢索帶來些負面的問題,但是始終認為利大於弊。
現在回到正題,如果利用了上面兩個方法後,倒排表還是很大怎麼辦?那麼壓縮演算法開始上場了。這裡我們不使用Gzip等複雜的演算法,由於倒排表的形式簡單,所以對簡單的問題有著簡單的解決方案。
文檔數目必須用4位元組整數表示,但是一個索引之中的文檔編號相鄰間隔不會太大,可以用少於4位元組表示。例如,如果我們確定文檔編號間隔不會超過兩個位元組表示的範圍,那麼對於一個倒排表我們只把第一個文檔編號用4位元組表示,其它的用2位元組間隔來表示,這種方法可以使倒排表的大小為原來的一半。如本文一開始給的數字來說,倒排表大小可以由40TB變成20TB。上面給這個思想可行也很誘人,只不過還存在一個必須解決的問題:文檔間隔有可能和文檔編號是一個量級的,用兩位元組無法表示。
下面介紹一種可變位元組編碼方式:利用整數個位元組對文檔間隔進行編碼。每個位元組的第1個位元代表延續位,後7個位元代表編碼位元。如果第一個位元位1,代表這個位元組是編碼的最後一個位元組,否則不是最後一個位元組。
例如:
5表示成可變位元組編碼二進位形式為: 10000101
214577表示成可變位元組編碼二進位形式為: 00001101 00001100 10110001。
對於第二個例子給出其還原方式:拿出每個位元組的低七位組合得到一個21位元位元,這個數字就是214577。這種編碼方式的編解碼過程都非常簡單,而且時間開銷很小,相比於Gzip這種方法要好的多。
這種編碼方式出奇的簡單,以致於你認為它的實際效果能好到哪去。有人在權威的文檔集合(大概包含800000篇文檔)上構建倒排表,原始大小為250MB,而經過變長編碼得到的倒排表大小隻有116MB。要解釋這種現象就可能需要Zipf定律了,這個定律大家可以有興趣查一查。
除了可變長編碼,還有諸如γ編碼等等,這些編碼方式操作儲存單元的粒度要比一個位元組要小,所以編碼方式比可變長編碼複雜,能得到更好的理論壓縮效率和實際壓縮效率。但是同樣在這800000文檔機上構建倒排表,編碼使倒排表的大小變為101MB,僅僅比可變長編碼少13%。
假設通過詞幹化,去除停用詞能夠使倒排表大小降低1/10,之後可變長編碼又使倒排表大小降低1/2,那麼現在我們的倒排表大小就為18TB左右了。雖然這麼大的資料還是得需要分布式儲存的方法,但是這樣可以減少對機器數量的需求,是比較節省成本的。而對於一個程式員來說,想要做一個單台機器索引幾百萬網頁的搜尋引擎來說,也是一件可能的事了。