Lucene源代码索引合并之field,term层

 上一篇文章中的mergeSegments方法中的第51行

    mergedDocCount = merger.merge();    // 通过SegmentMerger merger获取需要合并的索引段文件数量

调用了SegmentMerger的对象中的merge方法对索引进行更深入的field&term合并。

SegmentMerger的merge方法如下:

  1.   final int merge() throws IOException {
  2.     int value;
  3.     
  4.     value = mergeFields();
  5.     mergeTerms();
  6.     mergeNorms();
  7.     if (fieldInfos.hasVectors())
  8.       mergeVectors();
  9.     return value;
  10.   }

按顺序来,首先是mergeField方法:

  1.  private final int mergeFields() throws IOException {
  2.     fieldInfos = new FieldInfos();        // merge field names
  3.     int docCount = 0;
  4.     /**
  5.      * 重新构造field的信息,保存到filedInfos容器中
  6.      */
  7.     for (int i = 0; i < readers.size(); i++) {
  8.       IndexReader reader = (IndexReader) readers.elementAt(i);             @?
  9.       addIndexed(reader, fieldInfos, reader.getFieldNames(IndexReader.FieldOption.TERMVECTOR_WITH_POSITION_OFFSET), truetruetrue);
  10.       addIndexed(reader, fieldInfos, reader.getFieldNames(IndexReader.FieldOption.TERMVECTOR_WITH_POSITION), truetruefalse);
  11.       addIndexed(reader, fieldInfos, reader.getFieldNames(IndexReader.FieldOption.TERMVECTOR_WITH_OFFSET), truefalsetrue);
  12.       addIndexed(reader, fieldInfos, reader.getFieldNames(IndexReader.FieldOption.TERMVECTOR), truefalsefalse);
  13.       addIndexed(reader, fieldInfos, reader.getFieldNames(IndexReader.FieldOption.INDEXED), falsefalsefalse);
  14.       fieldInfos.add(reader.getFieldNames(IndexReader.FieldOption.UNINDEXED), false);
  15.     }
  16.     //重新写入field的.fnm文件。
  17.     fieldInfos.write(directory, segment + ".fnm");
  18.     FieldsWriter fieldsWriter = // merge field values
  19.             new FieldsWriter(directory, segment, fieldInfos);
  20.     
  21.     /**
  22.      * for merging we don't want to compress/uncompress the data, 
  23.      * so to tell the FieldsReader that we're in  merge mode,
  24.      * we use this FieldSelector
  25.      */
  26.     FieldSelector fieldSelectorMerge = new FieldSelector() {
  27.       public FieldSelectorResult accept(String fieldName) {
  28.         return FieldSelectorResult.LOAD_FOR_MERGE;
  29.       }        
  30.     };
  31.     
  32.     try {
  33.       for (int i = 0; i < readers.size(); i++) {
  34.         IndexReader reader = (IndexReader) readers.elementAt(i);
  35.         int maxDoc = reader.maxDoc();
  36.         for (int j = 0; j < maxDoc; j++)
  37.           if (!reader.isDeleted(j)) { // skip deleted docs,如果没有删除。
  38.             //重新写入.fdt文件及.fdx文件。
  39.             fieldsWriter.addDocument(reader.document(j, fieldSelectorMerge));
  40.             //统计各个segment中包含document的数量。
  41.             docCount++;
  42.           }
  43.       }
  44.     } finally {
  45.       fieldsWriter.close();
  46.     }
  47.     return docCount;
  48.   }

标记中@?的readers哪里来的呢?这个问题一直困扰着我。

那么首先看看SegmentMerger的属性

private Vector readers = new Vector();

SegmentMerger的add方法

  final void add(IndexReader reader) {
    readers.addElement(reader);
  }

又要追溯到上一篇的IndexWriter里面的mergeSegment方法

  1.  if (doMerge) {
  2.         if (infoStream != null) infoStream.print("merging segments");
  3.         merger = new SegmentMerger(this, mergedName);
  4.         for (int i = minSegment; i < end; i++) {
  5.           SegmentInfo si = sourceSegments.info(i);
  6.           if (infoStream != null)
  7.             infoStream.print(" " + si.name + " (" + si.docCount + " docs)");
  8.           IndexReader reader = SegmentReader.get(si); // no need to set deleter (yet)
  9.           merger.add(reader);
  10.           if ((reader.directory() == this.directory) || // if we own the directory
  11.               (reader.directory() == this.ramDirectory))
  12.             segmentsToDelete.addElement(reader);   // queue segment for deletion
  13.         }
  14.       }

term信息合并过程

term信息合并过程由SegmentMerger的mergeTerms方法来执行。

  1.  private final void mergeTerms() throws IOException {
  2.     try {
  3.       freqOutput = directory.createOutput(segment + ".frq");//创建合并后的.frq文件。
  4.       proxOutput = directory.createOutput(segment + ".prx");//创建合并后的.frx文件。
  5.       /**
  6.        * 重新构建termInfosWriter,建立一组.tis、.tii文件
  7.        */
  8.       termInfosWriter =
  9.               new TermInfosWriter(directory, segment, fieldInfos,
  10.                                   termIndexInterval);
  11.       //获取跳跃跨度值。
  12.       skipInterval = termInfosWriter.skipInterval;
  13.       //带有优先级的队列,队列中所有的成员都是索引的读取类(reader)
  14.       queue = new SegmentMergeQueue(readers.size());
  15.       //合并词条term
  16.       mergeTermInfos();
  17.     } finally {
  18.       if (freqOutput != null) freqOutput.close();
  19.       if (proxOutput != null) proxOutput.close();
  20.       if (termInfosWriter != null) termInfosWriter.close();
  21.       if (queue != null) queue.close();
  22.     }
  23.   }

可能会出现了问题:skipInterval是什么意思?

【最佳答案】

skipInterval是对频率与位置文件信息查询时,快速定位的跳跃跨度数值。

举一个建立skip层次信息的例子(某个单词在27个文档中频率信息,跳跃跨度为3,会出现3个层次)如下:

skipInterval = 3:
* c (skip level 2)
* c c c (skip level 1)
* x x x x x x x x x x (skip level 0)
* d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d

号 3 6 9 12 15 18 21 24 27 30

* d - document
* x - skip data
* c - skip data with child pointer

如同有27个房间,每3个房间出现一个控制间,在lognN n=skipInterval的情况下,出现3层(level0,level1,level2)查找时,level2保存在内存中减少占用内存的大小,定位某个元素只是需要查找lognN次便可定位到底层的skipinterval个元素集合中,内部最多遍历n次即可找到该元素。


合并TermInfo

SegmentMerger的mergeTermInfos方法代码如下:

  1.  private final void mergeTermInfos() throws IOException {
  2.     int base = 0;
  3.     for (int i = 0; i < readers.size(); i++) {
  4.       IndexReader reader = (IndexReader) readers.elementAt(i);
  5.       TermEnum termEnum = reader.terms();
  6.       SegmentMergeInfo smi = new SegmentMergeInfo(base, termEnum, reader);
  7.       base += reader.numDocs();
  8.       if (smi.next())
  9.         queue.put(smi);               // initialize queue
  10.       else
  11.         smi.close();
  12.     }
  13.     SegmentMergeInfo[] match = new SegmentMergeInfo[readers.size()];
  14.     while (queue.size() > 0) {
  15.       int matchSize = 0;              // pop matching terms
  16.       match[matchSize++] = (SegmentMergeInfo) queue.pop();
  17.       Term term = match[0].term;
  18.       SegmentMergeInfo top = (SegmentMergeInfo) queue.top();
  19.       /**
  20.        * match数组里面存储了相同的term,match数组的大小即为term在
  21.        * 文集中的docfreq(文集中共有多少个document包含这个term)。
  22.        */
  23.       while (top != null && term.compareTo(top.term) == 0) {
  24.         match[matchSize++] = (SegmentMergeInfo) queue.pop();
  25.         top = (SegmentMergeInfo) queue.top();
  26.       }
  27.       mergeTermInfo(match, matchSize);  //add new TermInfo 核心     @
  28.       //合并后重新存储队列中的元素。
  29.       while (matchSize > 0) {
  30.         SegmentMergeInfo smi = match[--matchSize];
  31.         if (smi.next())
  32.           queue.put(smi);             // restore queue
  33.         else
  34.           smi.close();                // done with a segment
  35.       }
  36.     }
  37.   }

核心代码@,继续细化到mergeTermInfo方法:

  1.  private final void mergeTermInfo(SegmentMergeInfo[] smis, int n)
  2.           throws IOException {
  3.     long freqPointer = freqOutput.getFilePointer();
  4.     long proxPointer = proxOutput.getFilePointer();
  5.     int df = appendPostings(smis, n);    //append posting data 核心
  6.     long skipPointer = writeSkip(); //应该是写入skip跳跃跨度信息。
  7.     if (df > 0) {
  8.       // add an entry to the dictionary with pointers to prox and freq files
  9.       // 提供了字典文件的入口地址。
  10.       termInfo.set(df, freqPointer, proxPointer, (int) (skipPointer - freqPointer));
  11.       termInfosWriter.add(smis[0].term, termInfo);
  12.     }
  13.   }

上述的核心为appendPostings方法,主要作用:重新写入频率文件和位置文件,即合并后的term的posting信心重新填写到索引中。代码如下:

  1. /** Process postings from multiple(多个) segments all positioned on the
  2.    *  same term. Writes out merged entries into freqOutput and
  3.    *  the proxOutput streams.
  4.    *
  5.    * @param smis array of segments
  6.    * @param n number of cells in the array actually occupied
  7.    * @return number of documents across all segments where this term was found
  8.    */
  9.   private final int appendPostings(SegmentMergeInfo[] smis, int n)
  10.           throws IOException {
  11.     int lastDoc = 0;
  12.     int df = 0;                   // number of docs w/ term
  13.     resetSkip();
  14.     for (int i = 0; i < n; i++) {
  15.       SegmentMergeInfo smi = smis[i];
  16.       TermPositions postings = smi.getPositions();
  17.       int base = smi.base;
  18.       int[] docMap = smi.getDocMap();
  19.       postings.seek(smi.termEnum);
  20.       while (postings.next()) {
  21.         int doc = postings.doc();
  22.         if (docMap != null)
  23.           doc = docMap[doc];                // map around deletions
  24.         doc += base;                       // convert to merged space
  25.         if (doc < 0 || (df > 0 && doc <= lastDoc))
  26.           throw new IllegalStateException("docs out of order (" + doc +
  27.               " <= " + lastDoc + " )");
  28.   
  29.         /**
  30.          * 记录有多少个document中含有相同的term,因为本posting里面存储的
  31.          * 都是相同的term,而这些term又是从每个document中采集来的不重复的
  32.          * 词条。
  33.          */
  34.         df++;
  35.         /**
  36.          * 如果多个document包含这个term并且频率数值超过skipInterval这个
  37.          * 阀值,需要在频率与位置文件加入跳跃指针。
  38.          */
  39.         if ((df % skipInterval) == 0) {
  40.           bufferSkip(lastDoc);
  41.         }
  42.         /**
  43.          * 计算docCode,当前document编号与前document的编号之间的差值乘以2.
  44.          */
  45.         int docCode = (doc - lastDoc) << 1;   // use low bit to flag freq=1
  46.         lastDoc = doc;
  47.         /**
  48.          * term在document中出现的次数就是代码freq的数值。
  49.          */
  50.         int freq = postings.freq();
  51.         if (freq == 1) {
  52.           //用docCode的数值加上1(必为奇数)保存到频率文件中。
  53.           freqOutput.writeVInt(docCode | 1);  // write doc & freq=1
  54.         } else {
  55.           freqOutput.writeVInt(docCode);      // write doc 偶数
  56.           freqOutput.writeVInt(freq);       // write frequency in doc
  57.         }
  58.         int lastPosition = 0;      // write position deltas
  59.         for (int j = 0; j < freq; j++) {
  60.           int position = postings.nextPosition();
  61.           //计算位置的差值delta
  62.           proxOutput.writeVInt(position - lastPosition);
  63.           lastPosition = position;
  64.         }
  65.       }
  66.     }
  67.     return df;
  68.   }

private RAMOutputStream skipBuffer = new RAMOutputStream();

bufferSkip的函数实现如下:

  1.  private void bufferSkip(int doc) throws IOException {
  2.     long freqPointer = freqOutput.getFilePointer();
  3.     long proxPointer = proxOutput.getFilePointer();
  4.     //写入当前document编号与连续document编号之间的差值delta
  5.     skipBuffer.writeVInt(doc - lastSkipDoc);
  6.     //在位置文件和频率文件中写入skip信息,写入skip信息后的指针偏移量。
  7.     skipBuffer.writeVInt((int) (freqPointer - lastSkipFreqPointer));
  8.     skipBuffer.writeVInt((int) (proxPointer - lastSkipProxPointer));
  9.     lastSkipDoc = doc;
  10.     lastSkipFreqPointer = freqPointer;
  11.     lastSkipProxPointer = proxPointer;
  12.   }

所得到的bufferSkip现在只能保存在内存中,写入磁盘则需要调用writeSkip方法:

  1.  private long writeSkip() throws IOException {
  2.     long skipPointer = freqOutput.getFilePointer();
  3.     skipBuffer.writeTo(freqOutput);
  4.     return skipPointer;
  5.   }

RAMOutputStream的writeTo方法:

  1. /** Copy the current contents of this buffer to the named output. */
  2.   public void writeTo(IndexOutput out) throws IOException {
  3.     flush();
  4.     final long end = file.length;
  5.     long pos = 0;
  6.     int buffer = 0;
  7.     while (pos < end) {
  8.       int length = BUFFER_SIZE;
  9.       long nextPos = pos + length;
  10.       if (nextPos > end) {                        // at the last buffer
  11.         length = (int)(end - pos);
  12.       }
  13.       out.writeBytes((byte[])file.buffers.get(buffer++), length);
  14.       pos = nextPos;
  15.     }
  16.   }

【小问题】

如何把bufferSkip中的内容写入到freqOut中的呢?难道file就是bufferSkip?有待深究……

最后写入合并后的规格化文件Norms,mergeNorms方法如下:

  1.   private void mergeNorms() throws IOException {
  2.     byte[] normBuffer = null;
  3.     IndexOutput output = null;
  4.     try {
  5.       for (int i = 0; i < fieldInfos.size(); i++) {
  6.         FieldInfo fi = fieldInfos.fieldInfo(i);
  7.         if (fi.isIndexed && !fi.omitNorms) {
  8.           if (output == null) { 
  9.             output = directory.createOutput(segment + "." + IndexFileNames.NORMS_EXTENSION);
  10.             output.writeBytes(NORMS_HEADER,NORMS_HEADER.length);
  11.           }
  12.           for (int j = 0; j < readers.size(); j++) {
  13.             IndexReader reader = (IndexReader) readers.elementAt(j);
  14.             int maxDoc = reader.maxDoc();
  15.             if (normBuffer == null || normBuffer.length < maxDoc) {
  16.               // the buffer is too small for the current segment
  17.               normBuffer = new byte[maxDoc];
  18.             }
  19.             reader.norms(fi.name, normBuffer, 0);
  20.             if (!reader.hasDeletions()) {   //如果不是将要被删除。
  21.               //optimized case for segments without deleted docs
  22.               output.writeBytes(normBuffer, maxDoc);
  23.             } else {
  24.               // this segment has deleted docs, so we have to
  25.               // check for every doc if it is deleted or not
  26.               for (int k = 0; k < maxDoc; k++) {
  27.                 if (!reader.isDeleted(k)) { //如果没有被删除
  28.                   output.writeByte(normBuffer[k]);
  29.                 }
  30.               }
  31.             }
  32.           }
  33.         }
  34.       }
  35.     } finally {
  36.       if (output != null) { 
  37.         output.close();
  38.       }
  39.     }
  40.   }

关于output.writeByte(normBuffer[k]);作用应该是把normBuffer[k]按字节写入output流中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值