Lucene的索引過程分兩個階段,第一階段把文檔索引到內存中;第二階段,即內存滿了,就把內存中的數據刷新到硬盤上。
倒排索引信息在內存存儲方式
Lucene有各種Field,比如StringField,TextField,IntField,FloatField,DoubleField…,Lucene在處理的過程中把各種Field都處理成相應的byte[],以最本質的方式來看待各種Field的內容,統一了數據的存儲形式。
在寫入內存階段,第一步就是需要理清各個類之間的關係。
在索引的過程中,需要有ByteBlockPool,IntBlockPool, ParallelPostingsArray三個類來協調配合存儲數據. ByteBlockPool存儲Term信息/Freq信息/Prox信息,IntBlockPool起着協調控制的作用; ParallelPostingsArray同時起着協調控制和統計docFreq的作用.三者緊密結合,構成了Lucene索引內存階段的鐵三角.
在Lucene的設計裏,IntBlockPool和ByteBlockPool的作用域是IndexChain,即每個IndexChain都會生成獨立的ByteBlockPool和IntBlockPool ,這樣就不會出現多線程間可變數據共享的問題,這種做法實際上是一種約定方式的線程封閉,即ByteBlockPool本身並不是線程安全的,不像ThreadLocal或者棧封閉。由於每個IndexChain都需要處理多個Field,所以IntBlockPool和ByteBlockPool是Field所共享的。需要注意的是ParallelPostingsArray的作用域是Field,即每個Field都有一個postingsArray。
從IndexChain的TermHash開始,各個類的協調關係如下圖所示:
第一次看這幅圖會有錯綜複雜的感覺,的確如此。有以下幾點需要注意:
1. TermsHash創建了IntBlockPool和ByteBlockPool。其中bytePool和termBytePool指向同一個對象。而且整個圖中所用到的intPool和bytePool都是共享TermsHash創建的對象。
2. BytesRefHash中的bytesStart和ParallelPostingsArray中的textStarts共享同一個對象。
3. IntBlockPool管理着ByteBlockPool的Slice塊信息的寫入起始位置
把目光專注到ParallelPostingsArray的三個成員變量上面:
textStarts存儲的是每一個term在ByteBlockPool裏面的起始位置,通過textStarts[termID]可以快速找到termID對應的term 。
byteStarts存儲的是term在ByteBlockPool的結束位置的下一個位置。
IntStarts存儲的是term在IntBlockPool的地址信息,而IntBlockPool則存儲着term在ByteBlockPool中的Slice位置信息。
比如兩個詞”new term”,PostingArray和IntBlockPool及ByteBlockPool的數據指示關係如下:(注下圖只表示各個部分的聯繫)
Lucene在存儲倒排索引的時候默認的存儲選項是:
即需要存儲DOCID;Freq;Positions三種信息。這三種信息都是隨着Term存儲在ByteBlockPool中。其存儲的過程如下:
第一步:把term.length存儲到ByteBlockPool.buffer中。這會佔用1或者2個byte,由term的大小決定。由於term的最大長度爲32766,所以term.length最多會佔用兩個byte。
第二步:把term的byte數組形式存儲到ByteBlockPool.buffer中。
第三步:緊接着term開闢5個byte大小的slice,用來存儲term在每個doc中的freq信息。
第四步,再開闢一塊Slice用來存儲positions信息.
第三步和第四步開闢的Slice除了存儲的內容不同外,結構是沒有差別的。 如果一個Slice用完了,那麼按照ByteBlockPool設置的規則再開闢14個byte的slice.
如果slice又用完了,則再開闢20個byte的slice…..
下圖的兩個數組代表了slice的開闢規則:一共有9種不同層次的slice,編號從1-9,每一種層次的slice大小都不相同,最小是5byte,最大是200byte。
每個代碼塊實際可用的Bytes= Slice.length-1 。這是因爲slice的最後一個byte裏面存儲着該slice的結束標誌。5_Bytes_Slice的結束符是16,14Bytes_Slice的結束符是17,依次加1就可以了。
新開闢的slice會與前面用完的slice連接起來,像鏈表一樣。
連接的方式比較特殊:把前一個Slice除結束符外最後的三個位置裏面存儲的數據轉移到新的Slice中,這樣前一個Slice的最後4個位置就用來存儲新的Slice在buffer中的地址信息。兩個Slice連接的代碼如下:
這種以鏈表的方式管理內存空間,是充分考慮了數據的特點。在文檔集中的詞分佈是zipf分佈。只有少量的詞頻很高,大量的詞詞頻其實很低。所以最小的Slice是5byte,但是如果所有的Slice都是5byte的話,對於高頻詞彙,又太浪費空間,所以最大的Slice是200byte。而且有9種不同的Slice,滿足了不同詞頻的存儲需求。
如果要從Slice中讀取數據,怎麼知道里面的byte是數據信息還是下一層的地址信息呢?通過ByteSliceReader就很容易了.在寫入數據到Slice時記錄ByteBlockPool.buffer中代表Slice塊鏈表的startIndex和endIndex。接下來我們肯定是從5_Bytes_Slice塊開始讀取.如果startIndex+5>=endIndex,那麼就可以確定當前塊中存儲的內容只是整個Slice鏈表的一部分.就很自然得到5_Bytes_Slice中數據信息的終結位置limit.接下來用同樣的方法確定出下一層的limit就OK啦。
具體的實現細節可以參考ByteSliceReader類,代碼很容易讀懂。
根據前面描述的ByteBlockPool存儲term的方式,如果document如下:
則在ByteBlockPool中,存儲的結構如下:
可以看到每個term後面都跟了兩個5_Bytes_Slice,米***的塊用來存儲docDelta和docFreq信息;藍色的塊用來存儲position信息。這就需要爲每一個term分配兩個int來保存Slice的起始位置,IntBlockPool則正好實現了上面的要求。接下來就會出現新的問題了,IntBlockPool中的哪兩個位置是分配給給定termID的呢?IntStarts[termID]就正好指明瞭分配給term的位置起點。(注:兩個位置是連續的)。所以ParallelPostingsArray和IntBlockPool可以視爲整個倒排索引的藏寶路線圖,而ByteBlockPool則可視爲寶藏所在地。
還有就是Lucene存儲在索引中的並非真正的docId,而是docDelta,即兩個docId的差值.這樣存儲能夠起到節約空間的作用.
正向信息在內存中的存儲
正向信息在Lucene中只有docId-document的映射,由CompressingStoredFieldsWriter類來完成。
Lucene的正向信息存儲比較簡單,按Field依次把內容寫入到bufferedDocs中,然後把偏移量寫入到endOffsets中就OK了。
當滿足flush條件或者執行了IndexWriter.commit()方法,則會進行一次flush操作,把內存中緩存的document及倒排信息flush到硬盤中。