索引構造

顧名思義這章就是要談怎樣構造索引的問題,或者說在有限內存和有限時間內,怎麼樣高效的對大數據集構造索引文件。一旦有了這個索引文件,那麼索引的壓縮,基於索引的排序,前面的章節都已經講過。

 

鏈接列表

先來看看最一般的方法,在內存中構建這樣的數據結構,包含一個term字典,這個字典本身可以用數組,hash表,二分查找樹來實現,字典中的每項,都包含一個指向term的倒排列表的指針,那麼對於一個term的倒排列表一般用單項鍊表來實現,因爲這個是動態的,就是說每一項包含文檔號,文檔內頻率,和下一項指針。

然後遍歷每一篇文檔,對於文檔中的每個term,在字典中如果有就直接把文檔號和頻率掛在這個term的倒排列表後面,如果沒有,先在字典中加上這個term,然後再掛。

當所有文檔都處理完了, 我們就在內存中保存了一個完整的倒排索引,最後一步就是把這個內存中的索引存儲到磁盤上的一個倒排文件。

那麼這個方法,容易理解,又簡單看上去很好,但有個問題,就是如果對於大文檔集,那內存的消耗是很大的。

 

那麼時間換空間,你可以把鏈表緩存到數據庫,或虛擬內存上面去,當然這個方法不可行,因爲效率太低,在index文檔時,需要先遍歷倒排列表去找到每個term所對應的鏈表, 這種隨機訪問會有大量的磁盤讀寫。

 

基於排序的倒排

對於大文檔集的索引,在有限內存情況下,不可能把索引都放在內存裏,必然要放到磁盤上,那麼對於磁盤的問題是,隨機訪問的效率很低。所以如果所有索引項都是有序的存放在文件中,那麼這樣順序的訪問磁盤文件還是比較高效的。

那麼什麼樣的數據結構適合在文件中排序了,答案是三元組 <t, d, fd,t >

這樣在index文檔時就不用去遍歷查找了, 三元組本身就記錄了所有的關係,當然三元組會造成數據冗餘,在每個三元組中都要保留termid

算法是這樣的:

1. 創建空的字典S,和一個磁盤上的臨時文件

2. 遍歷每一篇文檔,對文檔中的term,字典中沒有的話,加到字典中,有的話,讀出termid,組成三元組<t, d, fd,t >並存到臨時文件中。

3. 開始排序,因爲內存有限不可能把所有三元組都讀進來排序,所以要分段排序

就是每次讀入內存能夠容納的k條三元組,按termid升序排序,termid相同的,就按docid升序排序

那麼這排序後的k條三元組就是一個有序段(sorted run)

然後將有序段寫回臨時文件

這樣不斷的讀入直到全部處理完

4. 現在的狀況就是臨時文件裏面都是有序段,那麼下面要做的就是merge,如果初始有R個有序段,那麼通過logR趟(pass)merge,生成最終的有序段,即排序完成,這是個標準的外排序的做法。

5. 最後將這個臨時文件順序讀出來生成最終的壓縮倒排文件,完成後可刪掉臨時文件。

這個方法的好處就是一直是順序讀磁盤,沒有那種需要查找遍歷的隨機訪問。但有個問題就是會耗費比較多的磁盤,因爲你在做兩段merge的時候,merge的結果必須存到一個新的臨時文件中,就是峯值需要超過原始文件2倍的磁盤空間。

 

索引壓縮

前面說了,基於排序的倒排要耗費過多的磁盤資源, 所以下面要談的是,怎麼在創建倒排文件時儘量減少磁盤資源的耗費。

壓縮臨時文件

臨時文件裏面都是存放了三元組 <t, d, fd,t >,對於 d, fd,t 的壓縮前面在索引壓縮時候就談過,方法很多

那麼就來看看t的壓縮

在每個有序歸併段中,t值是非減的,所以選擇差分編碼是自然的選擇,就是記錄這個t和前一個的差值,這個t-gap是零或大於零的整數。

可以直接用一元編碼來進行編碼,那麼可以想象的出,這樣存儲t所需的空間是很小的

可以看出想要有比較好的壓縮效果,必須對有序段做壓縮,上面的算法改成這樣

對於2,3合併爲

遍歷每篇文檔,取出term組成三元組<t, d, fd,t >這裏不直接存到臨時文件,先存在內存中,當內存中存放的三元組條數達到k時,對這k條三元組進行排序,然後壓縮,最後把這個壓縮過的有序段寫入臨時文件。

4.因爲有序段時壓縮編碼過的,所以在並歸的時候要先解碼,並歸完再壓縮編碼,寫回臨時文件

 

原地多路並歸

並歸階段是處理器密集型,而不是磁盤密集型, 所以使用多路並歸可以進一步降低倒排時間。

推廣一下, 假定當前全部的R個並歸段進行R路並歸,先要從每個並歸段讀入一個b字節的塊,這個b的大小取決於內存大小,這個塊是每個並歸段的buffer,當一個並歸段的塊被讀完,則從磁盤上再讀入一塊。

R路並歸需要用到最小堆來從候選集合中有效的找到最小值。

那麼不斷的從堆頂取到最小值寫入新的臨時文件中, 直到最後完成排序。那麼這樣我們還是需要2倍原始文件的磁盤耗費,能不能進行原地並歸了,下面就介紹一下原地多路並歸算法,這個算法比較複雜,也挺有意思

原地並歸,就是不佔用更多的磁盤空間,並歸完後還是寫回原臨時文件,而不用寫入新的臨時文件,爲達到這個目的有幾點是需要認真考慮的

1. 要進行R路並歸,會先從每個並歸段讀入b字節的塊到內存,那麼容易想到我們只要把排序的結果也組成b字節的塊,就可以覆蓋到這些已被讀過的塊空間上,實現原地並歸。

問題是每個並歸塊不一定是b的整數倍,最後讀出的一個塊可能會小於b,那麼這樣這個不規則的塊會給這個算法帶來困擾,那麼這個的解決辦法很簡單,就是padding,對每個並歸段後面加上padding,使他一定爲b的整數倍。

 

2. 按上面的說法,我們把排好序的塊寫回被讀過的空塊中,有個問題是這些空塊是零散的,無序的。所以我們需要一個快表block table來記錄塊的順序,如block_table[1]表示當前臨時文件中的第一塊實際的塊順序是多少,比如block_table[1]=3,表示現在放第一塊的應該移到第三塊。

那麼所以最後我們還要根據block table對臨時文件進行重排,以恢復原來的順序,這個算法是可以在線性時間裏完成的

算法如下,算法的目的就是滿足block_table[i]=i,

從1到n遍歷塊表,如果block_table[i] 設爲k不等於i, 說明臨時文件第i塊裏面放的不是真正順序上的第i塊,

那麼做法是,把當前的第i塊放到緩存中,然後去找block_table[j]=i,就是找出順序上的第i塊,放到i位置上

現在第j塊空出來了, 看一下k是否等於j,如果等於正好放到j位置上

如果不等於,繼續去找block_table[s]=j,放到j位置上,這樣一直找下去,直到block_table[k],可以把緩存中的第k塊放進去。

當遍歷一遍,保證從1到n塊都滿足block_table[i]=i,則重排完成。

 

3. b值大小的選取,如果b值選的過大會導致內存中放不下R+1個b塊,選的過小會耗費過多的磁盤讀寫次數。

做法是給b一個2的冪形式的初值,這樣當b過大時,方便b/2來折半,而且這種方法保證歸併段仍然是新的b值的倍數。

那麼具體做法就是當b*(R+1) > M時,set b = b/2

 

那麼給出原地並歸的完整算法,

1. 初始化

    創建空字典S

    創建空臨時文件

    設 L爲字典佔的空間大小

    設 k =(M -L)/w, k爲並歸段包含三元組的個數,w爲一個三元組 <t, d, fd,t >所佔用的空間

    設 b = 50Kb

    設 R =0, R爲並歸段的個數

 

2. 處理文檔和生成臨時文件

    遍歷每一篇文檔D

          parse文檔成term

          對於每個term

                如果term在S中存在,取出termid

                在S中不存在

                     把term加到S中

                     更新L,k (因爲字典S變大,使可用內存變小,所以k會變小)

                把<t, d, fd,t >加到triple數組中

 

           任何時候,當triple數組中的triple個數達到k時,生成一個並歸段

                 對並歸段按term進行排序

                 對各個field進行壓縮

                 最後對並歸段進行padding,保證是b的倍數,寫入臨時文件

                 R = R+1

                 當b*(R+1) > M時,set b = b/2

 

3. 並歸

    從每個並歸段讀入一塊,在這些塊的number加到freelist裏面, 就是說這些塊讀過了,可以被輸出塊覆蓋

    從每個塊取一個triple,共R個triple,建最小堆

    不斷取堆頂triple, 放到輸出塊中, 並從相應的塊中取新的triple加到堆中

    當輸出塊滿的時候,從freelist中找一個空的塊,如果沒有空的,就創建一個新的塊append到臨時文件後。

    把輸出塊寫入這個空塊,更新freelist和block table

    同時當每個並歸段讀入的塊耗完的時候,再讀相應並歸段的next block,並更新freelist

 

4. 臨時文件重排

5. Truncate 並釋放尾部無用的空間

 

壓縮的內存內倒排

現在把基於排序的倒排方法放在一邊, 回到前面提到的內存內的鏈結列表方法,這種方法結合壓縮技術也可以達到比較好的效果。

現在將要描述的這個方法,基於一個假設就是,每個術語t的文檔頻率ft 在倒排前已知。 當然想要已知,實現中,就是先遍歷統計一遍,第二遍再來建立倒排。

那麼費這個勁去預先統計術語t的文檔頻率ft 有什麼好處嗎

a. 之前每個term的倒排列表都是用單向鏈表的結構來實現,因爲你不知道到底有多少文檔,必須動態的;那麼現在你知道文檔頻率,那麼就可以確切知道需要分配多少空間了,可以用數組來代替單項鍊表,那麼至少可以省下這個next指針的耗費。

b. 知道文檔的總數N,那麼文檔編號可以用logN位去編碼,而不用整型大小32位

c. 同樣知道最大的文檔內頻率m,也可以用logm位編碼fd,t

總之就是說,事先知道一些信息,可以減少數據編碼的可能性,也就是說信息熵變小,所以用於編碼的位數也就會變小,從而節省了空間。

 

當然這樣做對於內存的佔用還是很大,那麼又有底下兩種方法,來減少內存的消耗

基於字典的切分

基於文本的切分

原理就是把全部倒排列表放內存裏面太大,那麼就把建倒排的任務分成若干個小任務,每次在內存裏面只保留倒排的一部分

基於字典的切分,就是每次只建立和部分term相關的倒排,分多趟覆蓋整個字典。

基於文本的切分,就是每次只建立部分文本的倒排,分多趟覆蓋整個文本集。

 

 

 

 

 

 

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