Lucene索引過程中 內存管理與數據存儲


Lucene的索引過程分兩個階段,第一階段把文檔索引到內存中;第二階段,即內存滿了,就把內存中的數據刷新到硬盤上。

         倒排索引信息在內存存儲方式


         Lucene有各種Field,比如StringField,TextField,IntField,FloatField,DoubleFieldLucene在處理的過程中把各種Field都處理成相應的byte[],以最本質的方式來看待各種Field的內容,統一了數據的存儲形式。


         在寫入內存階段,第一步就是需要理清各個類之間的關係。


在索引的過程中,需要有ByteBlockPool,IntBlockPool, ParallelPostingsArray三個類來協調配合存儲數據. ByteBlockPool存儲Term信息/Freq信息/Prox信息,IntBlockPool起着協調控制的作用; ParallelPostingsArray同時起着協調控制和統計docFreq的作用.三者緊密結合,構成了Lucene索引內存階段的鐵三角.


Lucene的設計裏,IntBlockPoolByteBlockPool的作用域是IndexChain,即每個IndexChain都會生成獨立的ByteBlockPoolIntBlockPool ,這樣就不會出現多線程間可變數據共享的問題,這種做法實際上是一種約定方式的線程封閉,即ByteBlockPool本身並不是線程安全的,不像ThreadLocal或者棧封閉。由於每個IndexChain都需要處理多個Field,所以IntBlockPoolByteBlockPoolField所共享的。需要注意的是ParallelPostingsArray的作用域是Field,即每個Field都有一個postingsArray


IndexChainTermHash開始,各個類的協調關係如下圖所示:


wKioL1PLpI2ARmVoAAKVWQvlkE0207.jpg


第一次看這幅圖會有錯綜複雜的感覺,的確如此。有以下幾點需要注意:

1. TermsHash創建了IntBlockPool和ByteBlockPool。其中bytePool和termBytePool指向同一個對象。而且整個圖中所用到的intPool和bytePool都是共享TermsHash創建的對象。


2. BytesRefHash中的bytesStart和ParallelPostingsArray中的textStarts共享同一個對象。


3. IntBlockPool管理着ByteBlockPool的Slice塊信息的寫入起始位置

把目光專注到ParallelPostingsArray的三個成員變量上面:


textStarts存儲的是每一個termByteBlockPool裏面的起始位置,通過textStarts[termID]可以快速找到termID對應的term


byteStarts存儲的是termByteBlockPool的結束位置的下一個位置。


IntStarts存儲的是termIntBlockPool的地址信息,而IntBlockPool則存儲着termByteBlockPool中的Slice位置信息。


         比如兩個詞”new term”,PostingArrayIntBlockPoolByteBlockPool的數據指示關係如下:(注下圖只表示各個部分的聯繫)


wKiom1PLo3zibBbiAAJhgwl01Z0958.jpg


Lucene在存儲倒排索引的時候默認的存儲選項是:wKioL1PLpKDQKkn1AABcF0ppXqg982.jpg


即需要存儲DOCID;Freq;Positions三種信息。這三種信息都是隨着Term存儲在ByteBlockPool中。其存儲的過程如下:


第一步:把term.length存儲到ByteBlockPool.buffer中。這會佔用1或者2byte,由term的大小決定。由於term的最大長度爲32766,所以term.length最多會佔用兩個byte


第二步:把termbyte數組形式存儲到ByteBlockPool.buffer中。


第三步:緊接着term開闢5byte大小的slice,用來存儲term在每個doc中的freq信息。


第四步,再開闢一塊Slice用來存儲positions信息.


         第三步和第四步開闢的Slice除了存儲的內容不同外,結構是沒有差別的。 如果一個Slice用完了,那麼按照ByteBlockPool設置的規則再開闢14byteslice.


如果slice又用完了,則再開闢20byteslice…..


         下圖的兩個數組代表了slice的開闢規則:一共有9種不同層次的slice,編號從1-9,每一種層次的slice大小都不相同,最小是5byte,最大是200byte


wKiom1PLo5XTmLbiAAEF8BdhPsM718.jpg


每個代碼塊實際可用的Bytes= Slice.length-1 。這是因爲slice的最後一個byte裏面存儲着該slice的結束標誌。5_Bytes_Slice的結束符是16,14Bytes_Slice的結束符是17,依次加1就可以了。

新開闢的slice會與前面用完的slice連接起來,像鏈表一樣。


wKiom1PLo6CBEruOAAB2vwwp9fk825.jpg


連接的方式比較特殊:把前一個Slice除結束符外最後的三個位置裏面存儲的數據轉移到新的Slice中,這樣前一個Slice的最後4個位置就用來存儲新的Slicebuffer中的地址信息。兩個Slice連接的代碼如下:

 /*@xh 傳入的參數slice 與函數體中的 buffer指向同一塊內存地址。這樣做的目的在於編碼上清晰。* */
 public int allocSlice(final byte[] slice, final int upto) {
        /*@xh
         * slice[upto]裏面存儲的是當前slice的結束標誌,slice[upto] & 15即得到當前層ID。
         * 通過數組NEXT_LEVEL_ARRAY 得到下一層的ID,
         * 通過數組LEVEL_SIZE_ARRAY 得到下一層的Slice大小
         * */
        final int level = slice[upto] & 15;
        final int newLevel = NEXT_LEVEL_ARRAY[level];
        final int newSize = LEVEL_SIZE_ARRAY[newLevel];
        // Maybe allocate another block
        if (byteUpto > BYTE_BLOCK_SIZE-newSize) {
          nextBuffer();
        }
        final int newUpto = byteUpto;
        final int offset = newUpto +byteOffset;
        byteUpto += newSize;
        // Copy forward the past 3 bytes (which we are about
        // to overwrite with the forwarding address):
        /*@xh 簡單翻譯就是:把當前Slice結束標誌位前面的存儲的內容移到下一層Slice的前三個位置
         * */
        buffer[newUpto] = slice[upto-3];
        buffer[newUpto+1] = slice[upto-2];
        buffer[newUpto+2] = slice[upto-1];
        /*@xh 然後用當前Slice空出來的三個位置連同結束標誌位,一共4個Byte,來存儲下一層Slice在buffer中起始位置。
         * 這樣的話就可以通過當前Slice定位到下一層的Slice
         * */
        // Write forwarding address at end of last slice:
        slice[upto-3] = (byte) (offset >>> 24);
        slice[upto-2] = (byte) (offset >>> 16);
        slice[upto-1] = (byte) (offset >>> 8);
        slice[upto] = (byte) offset;
        // Write new level:
        //@xh 把下一層的結束標誌寫入。
        buffer[byteUpto-1] = (byte) (16|newLevel);
        //@xh 返回下一層可用的起始位置(由於下一層的前三個位置已經被佔用<參看上面的代碼>,所以需要+3)
        return newUpto+3;
      }

    這種以鏈表的方式管理內存空間,是充分考慮了數據的特點。在文檔集中的詞分佈是zipf分佈。只有少量的詞頻很高,大量的詞詞頻其實很低。所以最小的Slice5byte,但是如果所有的Slice都是5byte的話,對於高頻詞彙,又太浪費空間,所以最大的Slice200byte。而且有9種不同的Slice,滿足了不同詞頻的存儲需求。


如果要從Slice中讀取數據,怎麼知道里面的byte是數據信息還是下一層的地址信息呢?通過ByteSliceReader就很容易了.在寫入數據到Slice時記錄ByteBlockPool.buffer中代表Slice塊鏈表的startIndexendIndex。接下來我們肯定是從5_Bytes_Slice塊開始讀取.如果startIndex+5>=endIndex,那麼就可以確定當前塊中存儲的內容只是整個Slice鏈表的一部分.就很自然得到5_Bytes_Slice中數據信息的終結位置limit.接下來用同樣的方法確定出下一層的limitOK啦。


    具體的實現細節可以參考ByteSliceReader類,代碼很容易讀懂。


         根據前面描述的ByteBlockPool存儲term的方式,如果document如下:


wKiom1PLo-zzdW2BAAB5Fek2yyU447.jpg


則在ByteBlockPool中,存儲的結構如下:


wKioL1PLpRCwZn3VAAIJSOqtK7Q208.jpg


可以看到每個term後面都跟了兩個5_Bytes_Slice,米***的塊用來存儲docDeltadocFreq信息;藍色的塊用來存儲position信息。這就需要爲每一個term分配兩個int來保存Slice的起始位置,IntBlockPool則正好實現了上面的要求。接下來就會出現新的問題了,IntBlockPool中的哪兩個位置是分配給給定termID的呢?IntStarts[termID]就正好指明瞭分配給term的位置起點。(注:兩個位置是連續的)。所以ParallelPostingsArrayIntBlockPool可以視爲整個倒排索引的藏寶路線圖,而ByteBlockPool則可視爲寶藏所在地。


 還有就是Lucene存儲在索引中的並非真正的docId,而是docDelta,即兩個docId的差值.這樣存儲能夠起到節約空間的作用.


正向信息在內存中的存儲


正向信息在Lucene中只有docId-document的映射,由CompressingStoredFieldsWriter類來完成。


wKioL1PLpR7ivoDbAAFOlLKOb7o962.jpg


Lucene的正向信息存儲比較簡單,按Field依次把內容寫入到bufferedDocs中,然後把偏移量寫入到endOffsets中就OK了。


   當滿足flush條件或者執行了IndexWriter.commit()方法,則會進行一次flush操作,把內存中緩存的document及倒排信息flush到硬盤中。


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