Lucene的索引文件,會包含很多個segments文件,每個segment中包含多個documents文件,一個segment中會有完整的正向索引和反向索引。
在搜索時,Lucene會遍歷這些segments,以segments為基本單位獨立搜索每個segments文件,而后再把搜索結果合并。
建立索引文件的過程,實際就是把documents文件一個個加入索引中,Lucene的做法是最開始為每個新加入的document獨立生成一個segment,放在內存中。而后,當內存中segments數量到達一個闕值時,合并這些segments,新生成一個segment加入文件系統的segments列表中。
而當文件系統的segments數量過多時,勢必影響搜索效率,因此需要不斷合并這些segments文件。
那么Lucene的合并策略是什么?如何保證合適的segments數量呢?
其實Lucene有兩套基本的策略:
第一種策略實現代碼位于IndexWriter#optimize()函數,其實就是把所有segments文件合并成一個文件。合并的算法是遞歸合并列表最后的mergeFactor個segments文件直到合并成一個文件。這兒mergeFactor是Lucene的一個參數。
btw: 從算法細節上看,其實我不是喜歡這段實現,因為列表的最后mergeFactor個文件內容實際被掃描了segmens_count/mergeFactor次。如果分段歸納合并的方式不知道是否更好,每個segment文件內容都將被掃描 ceil(Log_mergeFactor(segmens_count)) 或ceil(Log_mergeFactor(segmens_count)) +1次,是否更好?
第二種策略實現代碼位于IndexWriter#maybeMergeSegments()函數中,這個代碼就復雜了,它的基本策略就是要求確保合并后兩個公式的成立:
假定segments是個有序列表,B表示maxBufferedDocs,f(n)=ceil(log_M(ceil(n/B))),M表示mergeFactor,這兒maxBufferedDocs和mergeFactor是兩個參數
1. 如果第i個segment的documents數量為x,第i+1個segment的documents數量為y,那么f(x)>f(y)一定成立
2. f(n)值相同的segments的數量不得超過M。
那么maybeMergeSegments()函數是如何確保這兩個公式成立的呢?
1.首先,從代碼:
protected final void maybeFlushRamSegments() throws IOException {
// A flush is triggered if enough new documents are buffered or
// if enough delete terms are buffered
if (ramSegmentInfos.size() >= minMergeDocs
|| numBufferedDeleteTerms >= maxBufferedDeleteTerms) {
flushRamSegments();
}
}
這兒minMergeDocs=maxBufferedDocs, 因此可以看出,當內存中緩存的segments被合并寫回磁盤時生成的segment的document count等于或小于maxBufferedDocs(即minMergeDocs)。
補充:因為maybeMergeSegments()運行在同步代碼中,因此只要ramSegmentInfos.size==minMergerDocs(即maxBufferedDocs)就會寫回磁盤,因此應該不存在ramSegmentInfos.size>maxBufferedDocs才寫回的情況。而且,但如果是這種情況,因為合并后的segment文件的document count過大,后面的maybeMergeSegments()將不做合并處理直接退出,上述公式就可能不成立,那么算法將有錯。
----
2.
2.1 因此maybeMergeSegments()第一次執行時,所有segments的document count都小于等于maxBufferedDocs。此時,從i=0開始,合并i~i+mergeFactor-1個文件,如果合并后的doc count>maxBufferedDocs時,保留第i個segment,否則繼續合并改變后的i~i+mergeFactor-1,直到doc count>maxBufferedDocs或所有segments文件個數已經<mergeFactor了。經過這樣一輪的合并,除了最后<mergeFactor個的doc counts<=maxBufferedDocs文件外,其它文件的doc counts一定都>maxBufferedDocs并<maxBufferedDocs*mergeFactor了。
2.2 這時,不理會最后<mergeFactor個doc count<maxBufferedDocs的文件,而后按2.1的同理規則,合并之前的文件,讓這些文件的最后<mergerFactor個segment符合 maxBufferedDocs<doc counts<=maxBufferedDocs*mergeFactor,之前的segment文件都符合maxBufferedDocs*mergeFactor<doc counts<=maxBufferedDocs*mergeFactor^2
2.3 重復2.2,最后得到的列表就會滿足上述兩等式的成立
---
3
之后,每次從內存緩存中flush出一個新的segment時,也就是往這個segments列表的最后增加一個doc_count<=maxBufferedDocs的文件,同樣執行上述步驟2進行合并,能夠始終保證上述兩公式的成立。
----
4
4.1
IndexWriter#addIndexesNoOptimize同樣借鑒使用了maybeMergeSegments()算法,區別此時,實際是已有一個符合兩公式的segments序列T,在T之后追加上隨意順序的segments序列S。這時,我們先找到S中doc count值最大的那個segment,計算出它屬于的區間f(x),使得maxBufferedDocs*mergeFactor^x<doc counts<=maxBufferedDocs*mergeFactor^(x+1),而后按2.2的算法合并出除了最后<mergerFactor個segments外, 之前所有segments都符合 a. doc count>maxBufferedDocs*mergeFactor^(x+1) b.符合上述2等式。
btw: 因為這兒調用IndexWriter#addIndexesNoOptimize傳入的參數是maxBufferedDocs*mergeFactor^(x+1),因為S所有segment的doc count都一定小于maxBufferedDocs*mergeFactor^(x+1),因此S的所有元素都會參與收縮合并。
4.2 將最后<mergerFactor個doc count<maxBufferedDocs*mergeFactor^(x+1)的segments合并,如果合并后的segment的doc count大于maxBufferedDocs*mergeFactor^(x+1),就繼續2.2步驟,使得整個隊列符合上述兩公式
-----
上述兩種策略,最終確保了Lucene中的segments不會太多,確保效率。
BTW:實際上,如果documents太多時,Lucene還支持把docuements分成幾個組,每個組用獨立的進程或電腦進行索引,而后再多個目錄的索引合并起來,具體可參考IndexWriter#addIndexesNoOptimize和IndexWriter#addIndexes函數