復合索引文件格式(.cfs)是如何產生的?從這個問題出發,研究索引文件是如何合并的,這都是IndexWriter類中定義的一些重要的方法。
在建立索引過程中,生成的索引文件的格式有很多種。
在文章 Lucene-2.2.0 源代碼閱讀學習(4) 中測試的那個例子,沒有對IndexWriter進行任何的客戶化設置,完全使用Lucene 2.2.0默認的設置(以及,對Field的設置使用了Lucene自帶的Demo中的設置)。
運行程序以后,在本地磁盤的索引目錄中生成了一些.擴展名為.cfs的索引文件,即復合索引格式文件。如圖(該圖在文章 Lucene-2.2.0 源代碼閱讀學習(4) 中介紹過)所示:
從上面生成的那些.cfs復合索引文件可以看出,Lucene 2.2.0版本,IndexWriter索引的一個成員useCompoundFile的設置起了作用,可以在IndexWriter類的內部看到定義和默認設置:
private boolean useCompoundFile = true;
即,默認使用復合索引文件格式來存儲索引文件。
在IndexWriter類的addDocument(Document doc, Analyzer analyzer)方法中可以看到,最后調用了 maybeFlushRamSegments()方法,這個方法的作用可是很大的,看它的定義:
protected final void
maybeFlushRamSegments
() throws CorruptIndexException, IOException {
??? if (ramSegmentInfos.size() >= minMergeDocs || numBufferedDeleteTerms >= maxBufferedDeleteTerms) {
?????
flushRamSegments();
??? }
}
這里,minMergeDocs是指:決定了合并索引段文件時指定的最小的Document的數量,在IndexWriter類中默認值為10,可以在IndexWriter類中查看到:
???? private int minMergeDocs = DEFAULT_MAX_BUFFERED_DOCS;
?? public final static int DEFAULT_MAX_BUFFERED_DOCS = 10;
其中SegmentInfos ramSegmentInfos中保存了Document的數量的信息,如果Document的數量小于10,則調用flushRamSegments()方法進行處理,flushRamSegments()方法的定義如下所示:
private final synchronized void flushRamSegments() throws CorruptIndexException, IOException {
???
flushRamSegments
(true);
}
在flushRamSegments()方法中又調用到了該方法的一個重載的方法,帶一個boolean型參數。該重載的方法定義如下:
protected final synchronized void flushRamSegments(boolean triggerMerge)
????? throws CorruptIndexException, IOException {
??? if (ramSegmentInfos.size() > 0 || bufferedDeleteTerms.size() > 0) {
?????
mergeSegments
(ramSegmentInfos, 0, ramSegmentInfos.size());
????? if (triggerMerge)
maybeMergeSegments
(minMergeDocs);
??? }
}
同樣,如果Document的數量小于10,則調用mergeSegments()方法,先看一下該方法的參數:
private final int mergeSegments(SegmentInfos sourceSegments, int minSegment, int end)
第一個參數指定了一個SegmentInfos(上面調用傳遞了ramSegmentInfos) ;第二個參數是minSegment是最小的索引段數量(上面調用傳遞了0,說明如果存在>=0個索引段文件時就開始合并索引文件);第三個參數是end,指要合并索引段文件的個數(上面調用傳遞了ramSegmentInfos.size(),即對所有的索引段文件都執行合并操作)。
繼續看mergeSegments()方法的實現:
private final int mergeSegments(SegmentInfos sourceSegments, int minSegment, int end)
??? throws CorruptIndexException, IOException {
??? // doMerge決定了是否執行合并操作,根據end的值,如果end為0說明要合并的索引段文件為0個,即不需要合并,doMerge=false
??? boolean doMerge = end > 0;
/*??? 生成合并的索引段文件名稱,即根據SegmentInfos的counter值,如果counter=0,則返回的文件名為_0(沒有指定擴展名)
??? final synchronized String newSegmentName() {
??????? return "_" + Integer.toString(segmentInfos.counter++, Character.MAX_RADIX);
??? }
*/
??? final String mergedName = newSegmentName();
??? SegmentMerger merger = null;??
// 聲明一個SegmentMerger變量
??? final List ramSegmentsToDelete = new ArrayList();??? // ramSegmentsToDelete列表用于存放可能要在合并結束后刪除的索引段文件,因為合并的過程中需要刪除掉合并完以后存在于內存中的這些索引段文件
??? SegmentInfo newSegment = null;
??? int mergedDocCount = 0;
??? boolean anyDeletes = (bufferedDeleteTerms.size() != 0);
???
// This is try/finally to make sure merger's readers are closed:
??? try {
????? if (doMerge) {???
// 如果doMerge=true,即end>0,也就是說至少有1個以上的索引段文件存在,才能談得上合并
??????? if (infoStream != null) infoStream.print("merging segments");???
// infoStream是一個PrintStream輸出流對象,合并完成后要向索引目錄中寫入合并后的索引段文件,必須有一個打開的輸出流
??????? merger = new SegmentMerger(this, mergedName);???
// 構造一個SegmentMerger對象,通過參數:當前的打開的索引器this和合并后的索引段名稱mergedName(形如_N,其中N為數)關于SegmentMerger類會在后面文章學習
??????? for (int i = minSegment; i < end; i++) {????
// 循環遍歷,從SegmentInfos sourceSegments中迭代出每個SegmentInfo對象
????????? SegmentInfo si = sourceSegments.info(i);
????????? if (infoStream != null)
??????????? infoStream.print(" " + si.name + " (" + si.docCount + " docs)");???
// SegmentInfo si的name在索引目錄中是唯一的;這里打印出每個 SegmentInfo si的名稱和在這個索引段文件中Document的數量
????????? IndexReader reader = SegmentReader.get(si, MERGE_READ_BUFFER_SIZE);
?? // 調用SegmentReader類的靜態方法get(),根據每個SegmentInfo si獲取一個索引輸入流對象;在IndexWriter類中定義了成員MERGE_READ_BUFFER_SIZE=4096
????????? merger.add(reader);???
//?? 將獲取到的SegmentReader reader加入到SegmentMerger merger中
????????? if (reader.directory() == this.ramDirectory) {???
// 如果SegmentReader
reader是當前的索引目錄,與當前的RAMDirectory ramDirectory是同一個索引目錄
??????????? ramSegmentsToDelete.add(si);???
// 將該SegmentInfo si加入到待刪除的列表ramSegmentsToDelete中
????????? }
??????? }
????? }
????? SegmentInfos rollback = null;
????? boolean success = false;
????? // This is try/finally to rollback our internal state
????? // if we hit exception when doing the merge:
????? try {
??????? if (doMerge) {????
// 如果doMerge=true
????????? mergedDocCount = merger.merge();???
// 通過SegmentMerger merger獲取需要合并的索引段文件數量
????????? if (infoStream != null) {???
// 打印出合并后的索引段文件的名稱,及其合并了索引段文件的數量
??????????? infoStream.println(" into "+mergedName+" ("+mergedDocCount+" docs)");
????????? }
????????? newSegment = new SegmentInfo(mergedName, mergedDocCount,
?????????????????????????????????????? directory, false, true);???
// 實例化一個SegmentInfo對象
??????? }
???????
??????? if (sourceSegments != ramSegmentInfos || anyDeletes) {
?????????
// 通過克隆,存儲一個用來回滾用的SegmentInfos實例,以防合并過程中發生異常
????????? rollback = (SegmentInfos) segmentInfos.clone();
??????? }
??????? if (doMerge) {??
// 如果doMerge=true
????????? if (sourceSegments == ramSegmentInfos) {??
// 如果傳進來的sourceSegments和內存中的ramSegmentInfos是同一個
??????????? segmentInfos.addElement(newSegment);???
// 將合并后的新的SegmentInfo newSegment加入到segmentInfos中進行管理,以便之后再對其操作
????????? } else {
// 如果傳進來的sourceSegments和內存中的ramSegmentInfos不是同一個
??????????? for (int i = end-1; i > minSegment; i--)????
// 刪除舊的信息,同時添加新的信息
????????????? sourceSegments.remove(i);
??????????? segmentInfos.set(minSegment, newSegment);
????????? }
??????? }
??????? if (sourceSegments == ramSegmentInfos) {???
// 如果傳進來的sourceSegments和內存中的ramSegmentInfos是同一個,因為參數設置的原因,可能需要刪除合并以后原來舊的索引段文件
????????? maybeApplyDeletes(doMerge);???
// 調用 maybeApplyDeletes()方法執行合并后的刪除處理
????????? doAfterFlush();
??????? }
???????
??????? checkpoint();??
// 調用該方法 checkpoint()檢查,確認并提交更新
??????? success = true;??? // 如果檢查沒有發現異常,則置success=true
????? } finally {
??????? if (success) {???
// 如果success
=true,表示提交成功,要清理內存
????????? if (sourceSegments == ramSegmentInfos) {
??????????? ramSegmentInfos.removeAllElements();
????????? }
??????? } else {???
// 如果發生異常,則需要回滾操作
????????? if (sourceSegments == ramSegmentInfos && !anyDeletes) {
??????????? if (newSegment != null &&
??????????????? segmentInfos.size() > 0 &&
??????????????? segmentInfos.info(segmentInfos.size()-1) == newSegment) {
????????????? segmentInfos.remove(segmentInfos.size()-1);
??????????? }
????????? } else if (rollback != null) {
??????????? segmentInfos.clear();
??????????? segmentInfos.addAll(rollback);
????????? }
?????????
// Delete any partially created and now unreferenced files:
????????? deleter.refresh();
??????? }
????? }
??? } finally {
?????
// 關閉所有的輸入流(readers),嘗試刪除過時的廢棄文件
????? if (doMerge) merger.closeReaders();
??? }
???
// 刪除RAM中的索引段文件
??? deleter.deleteDirect(ramDirectory, ramSegmentsToDelete);
???
// 一個檢查點,允許一個IndexFileDeleter deleter有機會在該時間點上去刪除文件
??? deleter.checkpoint(segmentInfos, autoCommit);
??? if (useCompoundFile && doMerge) {?? // 如果IndexWriter索引器設置了useCompoundFile=true
????? boolean success = false;
????? try {
??????? merger.createCompoundFile(mergedName + ".cfs");???
//?? 創建復合索引文件(.cfs),即_N.cfs文件
??????? newSegment.setUseCompoundFile(true);???
// 設置SegmentInfo newSegment為復合索引文件的信息
??????? checkpoint();?????
// 調用該方法 checkpoint()檢查,確認并提交更新
??????? success = true;
????? } finally {???
// 如果檢查過程中發生異常,則回滾
??????? if (!success) {??
????????? newSegment.setUseCompoundFile(false);
????????? deleter.refresh();
??????? }
????? }
??????
?????
// 一個檢查點,允許一個IndexFileDeleter deleter有機會在該時間點上去刪除文件
????? deleter.checkpoint(segmentInfos, autoCommit);
??? }
??? return mergedDocCount;
// 返回需合并的索引段文件數量
}
?
?
?
?
?
?
?
?
?
在不帶參數的flushRamSegments()方法中,調用了帶參數的flushRamSegments(boolean triggerMerge),也就是說,默認情況下,Lucene指定triggerMerge=true,可以在不帶參數的flushRamSegments()方法中看到對該參數的設置:
private final synchronized void flushRamSegments() throws CorruptIndexException, IOException {
???
flushRamSegments(true);
}
所以,在帶參數的flushRamSegments(boolean triggerMerge)方法中,一定會執行maybeMergeSegments()這個合并索引的方法,如下所示:
if (triggerMerge) maybeMergeSegments(minMergeDocs);
這里,傳遞的參數minMergeDocs=10(Lucene默認值),那么就應該有一個maxMergeDocs的成員與之對應,在Lucene 2.2.0版本中,在IndexWriter類中定義了該maxMergeDocs成員的默認值:
??? private int
maxMergeDocs
= DEFAULT_MAX_MERGE_DOCS;
??? public final static int DEFAULT_MAX_MERGE_DOCS = Integer.MAX_VALUE;
??? public static final int?? MAX_VALUE = 0x7fffffff;
maxMergeDocs是合并的最大的Document的數量,定義為最大的Integer。
因為一個索引目錄中的索引段文件的數量可能大于minMergeDocs=10,如果也要對所有的索引段文件進行合并,則指定合并最小數量minMergeDocs的Docment是不能滿足要求的,即使用mergeSegments()方法。
因此,maybeMergeSegments()就能實現合并性能的改善,它的聲明就是需要一個起始的參數,從而進行增量地合并索引段文件。該方法的實現如下所示:
/** Incremental segment merger. */
private final void maybeMergeSegments(int startUpperBound) throws CorruptIndexException, IOException {
??? long lowerBound = -1;
??? long upperBound = startUpperBound;???
// 使用upperBound存放傳遞進來的startUpperBound
??? while (upperBound < maxMergeDocs) {???
// 如果upperBound < maxMergeDocs,一般來說,這個應該總成立的
????? int minSegment = segmentInfos.size();???
//?? 設置minSegment的值為當前的SegmentInfos segmentInfos 的大小
????? int maxSegment = -1;
?????
// 查找能夠合并的索引段文件
????? while (--minSegment >= 0) {????
// 就是遍歷SegmentInfos segmentInfos中的每個SegmentInfo si
??????? SegmentInfo si = segmentInfos.info(minSegment);???
// 從索引位置號最大的開始往外取
??????? if (maxSegment == -1 && si.docCount > lowerBound && si.docCount <= upperBound) {???
// maxSegment == -1;同時滿足-1=lowerBound <(一個索引段文件中Dcoment的數量si.docCount)<=upperBound = startUpperBound
?????????
// start from the rightmost* segment whose doc count is in bounds
????????? maxSegment = minSegment;???
//?? 設置maxSegment的值為當前SegmentInfos的大小
??????? } else if (si.docCount > upperBound) {
?????????
// 直到segment中Document的數量超過了上限upperBound,則退出循環
????????? break;
??????? }
????? }
??? // 該while循環只執行了一次,執行過程中,將maxSegment賦值為segmentInfos.size()-1
????? minSegment++;???
// 上面循環中一直執行--minSegment,則到這里minSegment=-1,設置其值為0
????? maxSegment++;???
// 因為maxSegment=segmentInfos.size()-1,則設置為maxSegment=segmentInfos.size()
????? int numSegments = maxSegment - minSegment;???
// numSegments = maxSegment - minSegment = segmentInfos.size()
??
????? if (numSegments < mergeFactor) {
???
/* mergeFactor是合并因子,IndexWriter的成員,默認設置為10,mergeFactor的值越大,則內存中駐留的Document就越多,向索引目錄中寫入segment的次數就越少,雖然占用內存較多,但是速度應該很快的。每向索引文件中加入mergeFactor=10個Document的時候,就會在索引目錄中生成一個索引段文件(segment) */
??????? break;???
// numSegments < mergeFactor則沒有達到合并所需要的數量,不需要合并,直接退出
????? } else {
??????? boolean exceedsUpperLimit = false;???
// 設置一個沒有超過上限的boolean型標志(false)
???????
// 能夠合并的segments的數量>=mergeFactor時
??????? while (numSegments >= mergeFactor) {
?????????
// 調用mergeSegments(即上面的學習到的那個合并的方法)方法,
合并從minSegment開始的mergeFactor個segment
????????? int docCount = mergeSegments(segmentInfos, minSegment, minSegment + mergeFactor);
????????? numSegments -= mergeFactor;
// mergeFactor個segment已經合并完成,剩下需要合并的數量要減去mergeFactor,在下一次循環的時候繼續合并
????????? if (docCount > upperBound) {???
// 如果上次合并返回的合并后的Document的數量大于上限
???????????
// 繼續在該層次合并剩余的segment
??????????? minSegment++;
??????????? exceedsUpperLimit = true;???
//?? 設置已經超過上限,不能再進行深一層次的的合并,即本輪合并就是最深層次的合并了
????????? } else {
// 如果上次合并返回的合并后的Document的數量沒有超過上限
???????????
// 考慮進行更深層次的合并
??????????? numSegments++;
????????? }
??????? }
??????? if (!exceedsUpperLimit) {
// 如果上次合并返回的合并后的Document的數量大于上限,則終止執行本層次合并
????????? break;
??????? }
????? }
????? lowerBound = upperBound;
????? upperBound *= mergeFactor;???
// 如果一個層次的合并成功后,還可以進一步合并,則,上限變為原來的10倍
??? }
}
合并索引段文件就是這樣實現的,并非只是在一個層次上合并:
第一層次合并時,每次只能將10個segment索引段文件合并為1個新的segment,假設在這一層生成了500個經過合并以后生成的索引段文件;
第二層次合并時,每次能合并10*mergeFactor=10*10=100個segment,經判斷,上一層次生成了500個segment還可以進行第二層次的合并,現在每次100個segment文件才可能合并為1個,可見,只能合并生成5個新的segment;
第三層次合并時,每次能合并10*mergeFactor*mergeFactor=10*10*10=1000個segment,但是上一層次只是生成了5個,不夠數量(1000個),不能繼續合并了,到此終止。
就是上面的那種原理,實現索引段文件的合并。如果希望進行更深層次的合并,把mergeFactor的值設置的非常小就可以了,但是I/O操作過于頻繁,速度會很慢很慢的。
提高合并的速度,是以內存空間開銷為代價的。
通過第一個合并的方法可以看出,只有當為一個IndexWriter索引器設置了useCompoundFile=true的時候,才能生成復合索引文件_N.cfs,如下所示:
??? if (useCompoundFile && doMerge) {?? // 如果IndexWriter索引器設置了useCompoundFile=true
????? boolean success = false;
????? try {
??????? merger.createCompoundFile(mergedName + ".cfs");???
//?? 創建復合索引文件(.cfs),即_N.cfs文件
??????? newSegment.setUseCompoundFile(true);???
// 設置SegmentInfo newSegment為復合索引文件的信息
??????? checkpoint();?????
// 調用該方法 checkpoint()檢查,確認并提交更新
??????? success = true;
????? } finally {???
// 如果檢查過程中發生異常,則回滾
??????? if (!success) {??
????????? newSegment.setUseCompoundFile(false);
????????? deleter.refresh();
??????? }
????? }
??????
?????
// 一個檢查點,允許一個IndexFileDeleter deleter有機會在該時間點上去刪除文件
????? deleter.checkpoint(segmentInfos, autoCommit);
??? }
現在知道了,那些_N.cfs文件是合并的索引段文件。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
