Lucene学习总结之四:Lucene索引过程分析


对于Lucene的索引过程,除了将词(Term)写入倒排表并最终写入Lucene的索引文件外,还包括分词(Analyzer)和合并段(merge segments)的过程,本次不包括这两部分,将在以后的文章中进行分析。

Lucene的索引过程,很多的博客,文章都有介绍,推荐大家上网搜一篇文章:《Annotated Lucene》,好像中文名称叫《Lucene源码剖析》是很不错的。

想要真正了解Lucene索引文件过程,最好的办法是跟进代码调试,对着文章看代码,这样不但能够最详细准确的掌握索引过程(描述都是有偏差的,而代码是不会骗你的),而且还能够学习Lucene的一些优秀的实现,能够在以后的工作中为我所用,毕竟Lucene是比较优秀的开源项目之一。

由于Lucene已经升级到3.0.0了,本索引过程为Lucene 3.0.0的索引过程。

一、索引过程体系结构

Lucene 3.0的搜索要经历一个十分复杂的过程,各种信息分散在不同的对象中分析,处理,写入,为了支持多线程,每个线程都创建了一系列类似结构的对象集,为了提高效率,要复用一些对象集,这使得索引过程更加复杂。

其实索引过程,就是经历下图中所示的索引链的过程,索引链中的每个节点,负责索引文档的不同部分的信息 ,当经历完所有的索引链的时候,文档就处理完毕了。最初的索引链,我们称之基本索引链 。

为了支持多线程,使得多个线程能够并发处理文档,因而每个线程都要建立自己的索引链体系,使得每个线程能够独立工作,在基本索引链基础上建立起来的每个线程独立的索引链体系,我们称之线程索引链 。线程索引链的每个节点是由基本索引链中的相应的节点调用函数addThreads创建的。

为了提高效率,考虑到对相同域的处理有相似的过程,应用的缓存也大致相当,因而不必每个线程在处理每一篇文档的时候都重新创建一系列对象,而是复用这些对象。所以对每个域也建立了自己的索引链体系,我们称之域索引链 。域索引链的每个节点是由线程索引链中的相应的节点调用addFields创建的。

当完成对文档的处理后,各部分信息都要写到索引文件中,写入索引文件的过程是同步的,不是多线程的,也是沿着基本索引链将各部分信息依次写入索引文件的。

下面详细分析这一过程。

[图]索引过程体系结构

 

二、详细索引过程

1、创建IndexWriter对象

代码:

IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);

IndexWriter对象主要包含以下几方面的信息:

  • 用于索引文档
    • Directory directory;  指向索引文件夹
    • Analyzer analyzer;    分词器
    • Similarity similarity = Similarity.getDefault(); 影响打分的标准化因子(normalization factor)部分,对文档的打分分两个部分,一部分是索引阶段计算的,与查询语句无关,一部分是搜索阶段计算的,与查询语句相关。
    • SegmentInfos segmentInfos = new SegmentInfos(); 保存段信息,大家会发现,和segments_N中的信息几乎一一对应。
    • IndexFileDeleter deleter; 此对象不是用来删除文档的,而是用来管理索引文件的。
    • Lock writeLock; 每一个索引文件夹只能打开一个IndexWriter,所以需要锁。
    • Set segmentsToOptimize = new HashSet(); 保存正在最优化(optimize)的段信息。当调用optimize的时候,当前所有的段信息加入此Set,此后新生成的段并不参与此次最优化。
  • 用于合并段,在合并段的文章中将详细描述
    • SegmentInfos localRollbackSegmentInfos;
    • HashSet mergingSegments = new HashSet();
    • MergePolicy mergePolicy = new LogByteSizeMergePolicy(this);
    • MergeScheduler mergeScheduler = new ConcurrentMergeScheduler();
    • LinkedList pendingMerges = new LinkedList();
    • Set runningMerges = new HashSet();
    • List mergeExceptions = new ArrayList();
    • long mergeGen;
  • 为保持索引完整性,一致性和事务性
    • SegmentInfos rollbackSegmentInfos; 当IndexWriter对索引进行了添加,删除文档操作后,可以调用commit将修改提交到文件中去,也可以调用rollback取消从上次commit到此时的修改。
    • SegmentInfos localRollbackSegmentInfos; 此段信息主要用于将其他的索引文件夹合并到此索引文件夹的时候,为防止合并到一半出错可回滚所保存的原来的段信息。 
  • 一些配置
    • long writeLockTimeout; 获得锁的时间超时。当超时的时候,说明此索引文件夹已经被另一个IndexWriter打开了。
    • int termIndexInterval; 同tii和tis文件中的indexInterval。

 

有关SegmentInfos对象所保存的信息:

  • 当索引文件夹如下的时候,SegmentInfos对象如下表

[图]SegmentInfos对象


segmentInfos    SegmentInfos  (id=37)    
    capacityIncrement    0    
    counter    3    
    elementCount    3    
    elementData    Object[10]  (id=68)    
        [0]    SegmentInfo  (id=166)    
            delCount    0    
            delGen    -1    
            diagnostics    HashMap  (id=170)    
            dir    SimpleFSDirectory  (id=171)    
            docCount    2    
            docStoreIsCompoundFile    false    
            docStoreOffset    -1    
            docStoreSegment    null    
            files    ArrayList  (id=173)    
            hasProx    true    
            hasSingleNormFile    true    
            isCompoundFile    1    
            name    "_0"    
            normGen    null    
            preLockless    false    
            sizeInBytes    635    
        [1]    SegmentInfo  (id=168)    
            delCount    0    
            delGen    -1    
            diagnostics    HashMap  (id=177)    
            dir    SimpleFSDirectory  (id=171)    
            docCount    2    
            docStoreIsCompoundFile    false    
            docStoreOffset    -1    
            docStoreSegment    null    
            files    ArrayList  (id=178)    
            hasProx    true    
            hasSingleNormFile    true    
            isCompoundFile    1    
            name    "_1"    
            normGen    null    
            preLockless    false    
            sizeInBytes    635    
        [2]    SegmentInfo  (id=169)    
            delCount    0    
            delGen    -1    
            diagnostics    HashMap  (id=180)    
            dir    SimpleFSDirectory  (id=171)    
            docCount    2    
            docStoreIsCompoundFile    false    
            docStoreOffset    -1    
            docStoreSegment    null    
            files    ArrayList  (id=214)    
            hasProx    true    
            hasSingleNormFile    true    
            isCompoundFile    1    
            name    "_2"    
            normGen    null    
            preLockless    false    
            sizeInBytes    635     
    generation    4    
    lastGeneration    4    
    modCount    3    
    pendingSegnOutput    null    
    userData    HashMap  (id=146)    
    version    1263044890832   

有关IndexFileDeleter:

  • 其不是用来删除文档的,而是用来管理索引文件的。
  • 在对文档的添加,删除,对段的合并的处理过程中,会生成很多新的文件,并需要删除老的文件,因而需要管理。
  • 然而要被删除的文件又可能在被用,因而要保存一个引用计数,仅仅当引用计数为零的时候,才执行删除。
  • 下面这个例子能很好的说明IndexFileDeleter如何对文件引用计数并进行添加和删除的。

(1) 创建IndexWriter时    

IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); 
writer.setMergeFactor(3);

索引文件夹如下:

[图]IndexFileDeleter管理文件


引用计数如下:

refCounts    HashMap  (id=101)     
    size    1    
    table    HashMap$Entry[16]  (id=105)     
        [8]    HashMap$Entry  (id=110)     
            key    "segments_1"     
            value    IndexFileDeleter$RefCount  (id=38)    
                count    1

(2) 添加第一个段时

indexDocs(writer, docDir); 
writer.commit();

首先生成的不是compound文件

[图]IndexFileDeleter管理文件02


因而引用计数如下:

refCounts    HashMap  (id=101)     
    size    9    
    table    HashMap$Entry[16]  (id=105)     
        [1]    HashMap$Entry  (id=129)     
            key    "_0.tis"     
            value    IndexFileDeleter$RefCount  (id=138)    
                count    1     
        [3]    HashMap$Entry  (id=130)     
            key    "_0.fnm"     
            value    IndexFileDeleter$RefCount  (id=141)    
                count    1     
        [4]    HashMap$Entry  (id=134)     
            key    "_0.tii"     
            value    IndexFileDeleter$RefCount  (id=142)    
                count    1     
        [8]    HashMap$Entry  (id=135)     
            key    "_0.frq"     
            value    IndexFileDeleter$RefCount  (id=143)    
                count    1     
        [10]    HashMap$Entry  (id=136)     
            key    "_0.fdx"     
            value    IndexFileDeleter$RefCount  (id=144)    
                count    1     
        [13]    HashMap$Entry  (id=139)     
            key    "_0.prx"     
            value    IndexFileDeleter$RefCount  (id=145)    
                count    1     
        [14]    HashMap$Entry  (id=140)     
            key    "_0.fdt"     
            value    IndexFileDeleter$RefCount  (id=146)    
                count    1

然后会合并成compound文件,并加入引用计数

[图]IndexFileDeleter管理文件03


refCounts    HashMap  (id=101)     
    size    10    
    table    HashMap$Entry[16]  (id=105)     
        [1]    HashMap$Entry  (id=129)     
            key    "_0.tis"     
            value    IndexFileDeleter$RefCount  (id=138)    
                count    1     
        [2]    HashMap$Entry  (id=154)     
            key    "_0.cfs"     
            value    IndexFileDeleter$RefCount  (id=155)    
                count    1
      
        [3]    HashMap$Entry  (id=130)     
            key    "_0.fnm"     
            value    IndexFileDeleter$RefCount  (id=141)    
                count    1     
        [4]    HashMap$Entry  (id=134)     
            key    "_0.tii"     
            value    IndexFileDeleter$RefCount  (id=142)    
                count    1     
        [8]    HashMap$Entry  (id=135)     
            key    "_0.frq"     
            value    IndexFileDeleter$RefCount  (id=143)    
                count    1     
        [10]    HashMap$Entry  (id=136)     
            key    "_0.fdx"     
            value    IndexFileDeleter$RefCount  (id=144)    
                count    1     
        [13]    HashMap$Entry  (id=139)     
            key    "_0.prx"     
            value    IndexFileDeleter$RefCount  (id=145)    
                count    1     
        [14]    HashMap$Entry  (id=140)     
            key    "_0.fdt"     
            value    IndexFileDeleter$RefCount  (id=146)    
                count    1

然后会用IndexFileDeleter.decRef()来删除[_0.nrm, _0.tis, _0.fnm, _0.tii, _0.frq, _0.fdx, _0.prx, _0.fdt]文件

[图]IndexFileDeleter管理文件04


refCounts    HashMap  (id=101)     
    size    2    
    table    HashMap$Entry[16]  (id=105)     
        [2]    HashMap$Entry  (id=154)     
            key    "_0.cfs"     
            value    IndexFileDeleter$RefCount  (id=155)    
                count    1     
        [8]    HashMap$Entry  (id=110)     
            key    "segments_1"     
            value    IndexFileDeleter$RefCount  (id=38)    
                count    1

然后为建立新的segments_2

[图]IndexFileDeleter文件管理 

 

refCounts    HashMap  (id=77)     
    size    3    
    table    HashMap$Entry[16]  (id=84)     
        [2]    HashMap$Entry  (id=87)     
            key    "_0.cfs"     
            value    IndexFileDeleter$RefCount  (id=91)    
                count    3     
        [8]    HashMap$Entry  (id=89)     
            key    "segments_1"     
            value    IndexFileDeleter$RefCount  (id=62)    
                count    0     
        [9]    HashMap$Entry  (id=90)     
            key    "segments_2"    
            next    null    
            value    IndexFileDeleter$RefCount  (id=93)    
                count    1

然后IndexFileDeleter.decRef() 删除segments_1文件

[图]IndexFileDeleter文件管理

 

refCounts    HashMap  (id=77)     
    size    2    
    table    HashMap$Entry[16]  (id=84)     
        [2]    HashMap$Entry  (id=87)     
            key    "_0.cfs"     
            value    IndexFileDeleter$RefCount  (id=91)    
                count    2     
        [9]    HashMap$Entry  (id=90)     
            key    "segments_2"     
            value    IndexFileDeleter$RefCount  (id=93)    
                count    1

(3) 添加第二个段

indexDocs(writer, docDir); 
writer.commit();

[图]IndexFileDeleter文档管理

  

(4) 添加第三个段,由于MergeFactor为3,则会进行一次段合并。

indexDocs(writer, docDir); 
writer.commit();

首先和其他的段一样,生成_2.cfs以及segments_4

[图]IndexFileDeleter文件管理

 

同时创建了一个线程来进行背后进行段合并(ConcurrentMergeScheduler$MergeThread.run())

[图]IndexFileDeleter文件管理

 

这时候的引用计数如下

refCounts    HashMap  (id=84)     
    size    5    
    table    HashMap$Entry[16]  (id=98)     
        [2]    HashMap$Entry  (id=112)     
            key    "_0.cfs"     
            value    IndexFileDeleter$RefCount  (id=117)    
                count    1     
        [4]    HashMap$Entry  (id=113)     
            key    "_3.cfs"     
            value    IndexFileDeleter$RefCount  (id=118)    
                count    1     
        [12]    HashMap$Entry  (id=114)     
            key    "_1.cfs"     
            value    IndexFileDeleter$RefCount  (id=119)    
                count    1     
        [13]    HashMap$Entry  (id=115)     
            key    "_2.cfs"     
            value    IndexFileDeleter$RefCount  (id=120)    
                count    1     
        [15]    HashMap$Entry  (id=116)     
            key    "segments_4"     
            value    IndexFileDeleter$RefCount  (id=121)    
                count    1   

(5) 关闭writer

writer.close();

通过IndexFileDeleter.decRef()删除被合并的段

[图]IndexFileDeleter文件管理

 

有关SimpleFSLock进行JVM之间的同步:

  • 有时候,我们写java程序的时候,也需要不同的JVM之间进行同步,来保护一个整个系统中唯一的资源。
  • 如果唯一的资源仅仅在一个进程中,则可以使用线程同步的机制
  • 然而如果唯一的资源要被多个进程进行访问,则需要进程间同步的机制,无论是Windows和Linux在操作系统层面都有很多的进程间同步的机制。
  • 但进程间的同步却不是Java的特长,Lucene的SimpleFSLock给我们提供了一种方式。
Lock的抽象类 

public abstract class Lock {

  public static long LOCK_POLL_INTERVAL = 1000;

  public static final long LOCK_OBTAIN_WAIT_FOREVER = -1;

  public abstract boolean obtain() throws IOException;

  public boolean obtain(long lockWaitTimeout) throws LockObtainFailedException, IOException {

    boolean locked = obtain();

    if (lockWaitTimeout < 0 && lockWaitTimeout != LOCK_OBTAIN_WAIT_FOREVER) 
      throw new IllegalArgumentException("...");

    long maxSleepCount = lockWaitTimeout / LOCK_POLL_INTERVAL;

    long sleepCount = 0;

    while (!locked) {

      if (lockWaitTimeout != LOCK_OBTAIN_WAIT_FOREVER && sleepCount++ >= maxSleepCount) { 
        throw new LockObtainFailedException("Lock obtain timed out."); 
      } 
      try { 
        Thread.sleep(LOCK_POLL_INTERVAL); 
      } catch (InterruptedException ie) { 
        throw new ThreadInterruptedException(ie); 
      } 
      locked = obtain(); 
    } 
    return locked; 
  }

  public abstract void release() throws IOException;

  public abstract boolean isLocked() throws IOException;

}

LockFactory的抽象类

public abstract class LockFactory {

  public abstract Lock makeLock(String lockName);

  abstract public void clearLock(String lockName) throws IOException; 
}

SimpleFSLock的实现类

class SimpleFSLock extends Lock {

  File lockFile; 
  File lockDir;

  public SimpleFSLock(File lockDir, String lockFileName) { 
    this.lockDir = lockDir; 
    lockFile = new File(lockDir, lockFileName); 
  }

  @Override 
  public boolean obtain() throws IOException {

    if (!lockDir.exists()) {

      if (!lockDir.mkdirs()) 
        throw new IOException("Cannot create directory: " + lockDir.getAbsolutePath());

    } else if (!lockDir.isDirectory()) {

      throw new IOException("Found regular file where directory expected: " + lockDir.getAbsolutePath()); 
    }

    return lockFile.createNewFile();

  }

  @Override 
  public void release() throws LockReleaseFailedException {

    if (lockFile.exists() && !lockFile.delete()) 
      throw new LockReleaseFailedException("failed to delete " + lockFile);

  }

  @Override 
  public boolean isLocked() {

    return lockFile.exists();

  }

}

SimpleFSLockFactory的实现类

public class SimpleFSLockFactory extends FSLockFactory {

  public SimpleFSLockFactory(String lockDirName) throws IOException {

    setLockDir(new File(lockDirName));

  }

  @Override 
  public Lock makeLock(String lockName) {

    if (lockPrefix != null) {

      lockName = lockPrefix + "-" + lockName;

    }

    return new SimpleFSLock(lockDir, lockName);

  }

  @Override 
  public void clearLock(String lockName) throws IOException {

    if (lockDir.exists()) {

      if (lockPrefix != null) {

        lockName = lockPrefix + "-" + lockName;

      }

      File lockFile = new File(lockDir, lockName);

      if (lockFile.exists() && !lockFile.delete()) {

        throw new IOException("Cannot delete " + lockFile);

      }

    }

  }

};

 

2、创建文档Document对象,并加入域(Field)

代码:

Document doc = new Document();

doc.add(new Field("path", f.getPath(), Field.Store.YES, Field.Index.NOT_ANALYZED));

doc.add(new Field("modified",DateTools.timeToString(f.lastModified(), DateTools.Resolution.MINUTE), Field.Store.YES, Field.Index.NOT_ANALYZED));

doc.add(new Field("contents", new FileReader(f)));

Document对象主要包括以下部分:

  • 此文档的boost,默认为1,大于一说明比一般的文档更加重要,小于一说明更不重要。
  • 一个ArrayList保存此文档所有的域
  • 每一个域包括域名,域值,和一些标志位,和fnm,fdx,fdt中的描述相对应。

doc    Document  (id=42)    
    boost    1.0    
    fields    ArrayList  (id=44)    
        elementData    Object[10]  (id=46)    
            [0]    Field  (id=48)    
                binaryLength    0    
                binaryOffset    0    
                boost    1.0    
                fieldsData    "exampledocs//file01.txt"    
                isBinary    false    
                isIndexed    true    
                isStored    true    
                isTokenized    false    
                lazy    false    
                name    "path"    
                omitNorms    false    
                omitTermFreqAndPositions    false    
                storeOffsetWithTermVector    false    
                storePositionWithTermVector    false    
                storeTermVector    false    
                tokenStream    null    
            [1]    Field  (id=50)    
                binaryLength    0    
                binaryOffset    0    
                boost    1.0    
                fieldsData    "200910240957"    
                isBinary    false    
                isIndexed    true    
                isStored    true    
                isTokenized    false    
                lazy    false    
                name    "modified"    
                omitNorms    false    
                omitTermFreqAndPositions    false    
                storeOffsetWithTermVector    false    
                storePositionWithTermVector    false    
                storeTermVector    false    
                tokenStream    null    
            [2]    Field  (id=52)    
                binaryLength    0    
                binaryOffset    0    
                boost    1.0    
                fieldsData    FileReader  (id=58)    
                isBinary    false    
                isIndexed    true    
                isStored    false    
                isTokenized    true    
                lazy    false    
                name    "contents"    
                omitNorms    false    
                omitTermFreqAndPositions    false    
                storeOffsetWithTermVector    false    
                storePositionWithTermVector    false    
                storeTermVector    false    
                tokenStream    null     
        modCount    3    
        size    3   


3、将文档加入IndexWriter

代码:

writer.addDocument(doc); 
-->IndexWriter.addDocument(Document doc, Analyzer analyzer) 
     -->doFlush = docWriter.addDocument(doc, analyzer); 
          --> DocumentsWriter.updateDocument(Document, Analyzer, Term) 
注:--> 代表一级函数调用

IndexWriter继而调用DocumentsWriter.addDocument,其又调用DocumentsWriter.updateDocument。

4、将文档加入DocumentsWriter

代码:

DocumentsWriter.updateDocument(Document doc, Analyzer analyzer, Term delTerm) 
-->(1) DocumentsWriterThreadState state = getThreadState(doc, delTerm); 
-->(2) DocWriter perDoc = state.consumer.processDocument(); 
-->(3) finishDocument(state, perDoc);

DocumentsWriter对象主要包含以下几部分:

  • 用于写索引文件
    • IndexWriter writer;
    • Directory directory;
    • Similarity similarity:分词器
    • String segment:当前的段名,每当flush的时候,将索引写入以此为名称的段。
IndexWriter.doFlushInternal() 
--> String segment = docWriter.getSegment();//return segment 
--> newSegment = new SegmentInfo(segment,……); 
--> docWriter.createCompoundFile(segment);//根据segment创建cfs文件。
  •  
    • String docStoreSegment:存储域所要写入的目标段。(在索引文件格式一文中已经详细描述)
    • int docStoreOffset:存储域在目标段中的偏移量。
    • int nextDocID:下一篇添加到此索引的文档ID号,对于同一个索引文件夹,此变量唯一,且同步访问。
    • DocConsumer consumer; 这是整个索引过程的核心,是IndexChain整个索引链的源头。

基本索引链:

对于一篇文档的索引过程,不是由一个对象来完成的,而是用对象组合的方式形成的一个处理链,链上的每个对象仅仅处理索引过程的一部分,称为索引链,由于后面还有其他的索引链,所以此处的索引链我称为基本索引链。

DocConsumer consumer 类型为DocFieldProcessor,是整个索引链的源头,包含如下部分:

  • 对索引域的处理
    • DocFieldConsumer consumer 类型为DocInverter,包含如下部分
      • InvertedDocConsumer consumer类型为TermsHash,包含如下部分
        • TermsHashConsumer consumer类型为FreqProxTermsWriter,负责写freq, prox信息
        • TermsHash nextTermsHash
          • TermsHashConsumer consumer类型为TermVectorsTermsWriter,负责写tvx, tvd, tvf信息
      • InvertedDocEndConsumer endConsumer 类型为NormsWriter,负责写nrm信息
  • 对存储域的处理
    • FieldInfos fieldInfos = new FieldInfos();
    • StoredFieldsWriter fieldsWriter负责写fnm, fdt, fdx信息
  • 删除文档
    • BufferedDeletes deletesInRAM = new BufferedDeletes();
    • BufferedDeletes deletesFlushed = new BufferedDeletes();

类BufferedDeletes包含了一下的成员变量:

  • HashMap terms = new HashMap();删除的词(Term)
  • HashMap queries = new HashMap();删除的查询(Query)
  • List docIDs = new ArrayList();删除的文档ID
  • long bytesUsed:用于判断是否应该对删除的文档写入索引文件。

由此可见,文档的删除主要有三种方式:

  • IndexWriter.deleteDocuments(Term term):所有包含此词的文档都会被删除。
  • IndexWriter.deleteDocuments(Query query):所有能满足此查询的文档都会被删除。
  • IndexReader.deleteDocument(int docNum):删除此文档ID

删除文档既可以用reader进行删除,也可以用writer进行删除,不同的是,reader进行删除后,此reader马上能够生效,而用writer删除后,会被缓存在deletesInRAM及deletesFlushed中,只有写入到索引文件中,当reader再次打开的时候,才能够看到。

那deletesInRAM和deletesFlushed各有什么用处呢?

此版本的Lucene对文档的删除是支持多线程的,当用IndexWriter删除文档的时候,都是缓存在deletesInRAM中的,直到flush,才将删除的文档写入到索引文件中去,我们知道flush是需要一段时间的,那么在flush的过程中,另一个线程又有文档删除怎么办呢?

一般过程是这个样子的,当flush的时候,首先在同步(synchornized)的方法pushDeletes中,将deletesInRAM全部加到deletesFlushed中,然后将deletesInRAM清空,退出同步方法,于是flush的线程程就向索引文件写deletesFlushed中的删除文档的过程,而与此同时其他线程新删除的文档则添加到新的deletesInRAM中去,直到下次flush才写入索引文件。

  • 缓存管理
    • 为了提高索引的速度,Lucene对很多的数据进行了缓存,使一起写入磁盘,然而缓存需要进行管理,何时分配,何时回收,何时写入磁盘都需要考虑。
    • ArrayList freeCharBlocks = new ArrayList();将用于缓存词(Term)信息的空闲块
    • ArrayList freeByteBlocks = new ArrayList();将用于缓存文档号(doc id)及词频(freq),位置(prox)信息的空闲块。
    • ArrayList freeIntBlocks = new ArrayList();将存储某词的词频(freq)和位置(prox)分别在byteBlocks中的偏移量
    • boolean bufferIsFull;用来判断缓存是否满了,如果满了,则应该写入磁盘
    • long numBytesAlloc;分配的内存数量
    • long numBytesUsed;使用的内存数量
    • long freeTrigger;应该开始回收内存时的内存用量。
    • long freeLevel;回收内存应该回收到的内存用量。
    • long ramBufferSize;用户设定的内存用量。
缓存用量之间的关系如下: 
DocumentsWriter.setRAMBufferSizeMB(double mb){ 

    ramBufferSize = (long) (mb*1024*1024);//用户设定的内存用量,当使用内存大于此时,开始写入磁盘 
    waitQueuePauseBytes = (long) (ramBufferSize*0.1); 
    waitQueueResumeBytes = (long) (ramBufferSize*0.05); 
    freeTrigger = (long) (1.05 * ramBufferSize);//当分配的内存到达105%的时候开始释放freeBlocks中的内存 
    freeLevel = (long) (0.95 * ramBufferSize);//一直释放到95%



DocumentsWriter.balanceRAM(){ 
    if (numBytesAlloc+deletesRAMUsed > freeTrigger) { 
    //当分配的内存加删除文档所占用的内存大于105%的时候,开始释放内存 
        while(numBytesAlloc+deletesRAMUsed > freeLevel) { 
        //一直进行释放,直到95% 

            //释放free blocks

            byteBlockAllocator.freeByteBlocks.remove(byteBlockAllocator.freeByteBlocks.size()-1); 
            numBytesAlloc -= BYTE_BLOCK_SIZE;

            freeCharBlocks.remove(freeCharBlocks.size()-1); 
            numBytesAlloc -= CHAR_BLOCK_SIZE * CHAR_NUM_BYTE;

            freeIntBlocks.remove(freeIntBlocks.size()-1); 
            numBytesAlloc -= INT_BLOCK_SIZE * INT_NUM_BYTE; 
        } 
    } else {

        if (numBytesUsed+deletesRAMUsed > ramBufferSize){

        //当使用的内存加删除文档占有的内存大于用户指定的内存时,可以写入磁盘

              bufferIsFull = true;

        }

    } 
}

当判断是否应该写入磁盘时:

  • 如果使用的内存大于用户指定内存时,bufferIsFull = true
  • 当使用的内存加删除文档所占的内存加正在写入的删除文档所占的内存大于用户指定内存时 deletesInRAM.bytesUsed + deletesFlushed.bytesUsed + numBytesUsed) >= ramBufferSize
  • 当删除的文档数目大于maxBufferedDeleteTerms时

DocumentsWriter.timeToFlushDeletes(){

    return (bufferIsFull || deletesFull()) && setFlushPending();

}

DocumentsWriter.deletesFull(){

    return (ramBufferSize != IndexWriter.DISABLE_AUTO_FLUSH && 
        (deletesInRAM.bytesUsed + deletesFlushed.bytesUsed + numBytesUsed) >= ramBufferSize) || 
        (maxBufferedDeleteTerms != IndexWriter.DISABLE_AUTO_FLUSH && 
        ((deletesInRAM.size() + deletesFlushed.size()) >= maxBufferedDeleteTerms));

}

  • 多线程并发索引
    • 为了支持多线程并发索引,对每一个线程都有一个DocumentsWriterThreadState,其为每一个线程根据DocConsumer consumer的索引链来创建每个线程的索引链(XXXPerThread),来进行对文档的并发处理。
    • DocumentsWriterThreadState[] threadStates = new DocumentsWriterThreadState[0];
    • HashMap threadBindings = new HashMap();
    • 虽然对文档的处理过程可以并行,但是将文档写入索引文件却必须串行进行,串行写入的代码在DocumentsWriter.finishDocument中
    • WaitQueue waitQueue = new WaitQueue()
    • long waitQueuePauseBytes
    • long waitQueueResumeBytes

在Lucene中,文档是按添加的顺序编号的,DocumentsWriter中的nextDocID就是记录下一个添加的文档id。 当Lucene支持多线程的时候,就必须要有一个synchornized方法来付给文档id并且将nextDocID加一,这些是在DocumentsWriter.getThreadState这个函数里面做的。

虽然给文档付ID没有问题了。但是由Lucene索引文件格式我们知道,文档是要按照ID的顺序从小到大写到索引文件中去的,然而不同的文档处理速度不同,当一个先来的线程一处理一篇需要很长时间的大文档时,另一个后来的线程二可能已经处理了很多小的文档了,但是这些后来小文档的ID号都大于第一个线程所处理的大文档,因而不能马上写到索引文件中去,而是放到waitQueue中,仅仅当大文档处理完了之后才写入索引文件。

waitQueue中有一个变量nextWriteDocID表示下一个可以写入文件的ID,当付给大文档ID=4时,则nextWriteDocID也设为4,虽然后来的小文档5,6,7,8等都已处理结束,但是如下代码,

WaitQueue.add(){

    if (doc.docID == nextWriteDocID){ 
       ………… 
    } else { 
        waiting[loc] = doc; 
        waitingBytes += doc.sizeInBytes(); 
   }

   doPause()

}

则把5, 6, 7, 8放入waiting队列,并且记录当前等待的文档所占用的内存大小waitingBytes。

当大文档4处理完毕后,不但写入文档4,把原来等待的文档5, 6, 7, 8也一起写入。

WaitQueue.add(){

    if (doc.docID == nextWriteDocID) {

       writeDocument(doc);

       while(true) {

           doc = waiting[nextWriteLoc];

           writeDocument(doc);

       }

   } else {

      …………

   }

   doPause()

}

但是这存在一个问题:当大文档很大很大,处理的很慢很慢的时候,后来的线程二可能已经处理了很多的小文档了,这些文档都是在waitQueue中,则占有了越来越多的内存,长此以往,有内存不够的危险。

因而在finishDocuments里面,在WaitQueue.add最后调用了doPause()函数

DocumentsWriter.finishDocument(){

    doPause = waitQueue.add(docWriter);

    if (doPause) 
        waitForWaitQueue();

    notifyAll();

}

WaitQueue.doPause() { 
    return waitingBytes > waitQueuePauseBytes; 
}

当waitingBytes足够大的时候(为用户指定的内存使用量的10%),doPause返回true,于是后来的线程二会进入wait状态,不再处理另外的文档,而是等待线程一处理大文档结束。

当线程一处理大文档结束的时候,调用notifyAll唤醒等待他的线程。

DocumentsWriter.waitForWaitQueue() { 
  do { 
    try { 
      wait(); 
    } catch (InterruptedException ie) { 
      throw new ThreadInterruptedException(ie); 
    } 
  } while (!waitQueue.doResume()); 
}

WaitQueue.doResume() { 
     return waitingBytes <= waitQueueResumeBytes; 
}

当waitingBytes足够小的时候,doResume返回true, 则线程二不用再wait了,可以继续处理另外的文档。

  • 一些标志位
    • int maxFieldLength:一篇文档中,一个域内可索引的最大的词(Term)数。
    • int maxBufferedDeleteTerms:可缓存的最大的删除词(Term)数。当大于这个数的时候,就要写到文件中了。

此过程又包含如下三个子过程:

4.1、得到当前线程对应的文档集处理对象(DocumentsWriterThreadState)

代码为:

DocumentsWriterThreadState state = getThreadState(doc, delTerm);

在Lucene中,对于同一个索引文件夹,只能够有一个IndexWriter打开它,在打开后,在文件夹中,生成文件write.lock,当其他IndexWriter再试图打开此索引文件夹的时候,则会报org.apache.lucene.store.LockObtainFailedException错误。

这样就出现了这样一个问题,在同一个进程中,对同一个索引文件夹,只能有一个IndexWriter打开它,因而如果想多线程向此索引文件夹中添加文档,则必须共享一个IndexWriter,而且在以往的实现中,addDocument函数是同步的(synchronized),也即多线程的索引并不能起到提高性能的效果。

于是为了支持多线程索引,不使IndexWriter成为瓶颈,对于每一个线程都有一个相应的文档集处理对象(DocumentsWriterThreadState),这样对文档的索引过程可以多线程并行进行,从而增加索引的速度。

getThreadState函数是同步的(synchronized),DocumentsWriter有一个成员变量threadBindings,它是一个HashMap,键为线程对象(Thread.currentThread()),值为此线程对应的DocumentsWriterThreadState对象。

DocumentsWriterThreadState DocumentsWriter.getThreadState(Document doc, Term delTerm)包含如下几个过程:

  • 根据当前线程对象,从HashMap中查找相应的DocumentsWriterThreadState对象,如果没找到,则生成一个新对象,并添加到HashMap中
DocumentsWriterThreadState state = (DocumentsWriterThreadState) threadBindings.get(Thread.currentThread()); 
if (state == null) { 
    …… 
    state = new DocumentsWriterThreadState(this); 
    …… 
    threadBindings.put(Thread.currentThread(), state); 
  • 如果此线程对象正在用于处理上一篇文档,则等待,直到此线程的上一篇文档处理完。
DocumentsWriter.getThreadState() { 
    waitReady(state); 
    state.isIdle = false; 


waitReady(state) { 
    while (!state.isIdle) {wait();} 
}  

显然如果state.isIdle为false,则此线程等待。 
在一篇文档处理之前,state.isIdle = false会被设定,而在一篇文档处理完毕之后,DocumentsWriter.finishDocument(DocumentsWriterThreadState perThread, DocWriter docWriter)中,会首先设定perThread.isIdle = true; 然后notifyAll()来唤醒等待此文档完成的线程,从而处理下一篇文档。
  • 如果IndexWriter刚刚commit过,则新添加的文档要加入到新的段中(segment),则首先要生成新的段名。
initSegmentName(false); 
--> if (segment == null) segment = writer.newSegmentName();
  • 将此线程的文档处理对象设为忙碌:state.isIdle = false;

4.2、用得到的文档集处理对象(DocumentsWriterThreadState)处理文档

代码为:

DocWriter perDoc = state.consumer.processDocument();

每一个文档集处理对象DocumentsWriterThreadState都有一个文档及域处理对象DocFieldProcessorPerThread,它的成员函数processDocument()被调用来对文档及域进行处理。

线程索引链(XXXPerThread):

由于要多线程进行索引,因而每个线程都要有自己的索引链,称为线程索引链。

线程索引链同基本索引链有相似的树形结构,由基本索引链中每个层次的对象调用addThreads进行创建的,负责每个线程的对文档的处理。

DocFieldProcessorPerThread是线程索引链的源头,由DocFieldProcessor.addThreads(…)创建

DocFieldProcessorPerThread对象结构如下:

  • 对索引域进行处理
    • DocFieldConsumerPerThread consumer 类型为 DocInverterPerThread,由DocInverter.addThreads创建
      • InvertedDocConsumerPerThread consumer 类型为TermsHashPerThread,由TermsHash.addThreads创建
        • TermsHashConsumerPerThread consumer类型为FreqProxTermsWriterPerThread,由FreqProxTermsWriter.addThreads创建,负责每个线程的freq,prox信息处理
        • TermsHashPerThread nextPerThread
          • TermsHashConsumerPerThread consumer类型TermVectorsTermsWriterPerThread,由TermVectorsTermsWriter创建,负责每个线程的tvx,tvd,tvf信息处理
      • InvertedDocEndConsumerPerThread endConsumer 类型为NormsWriterPerThread,由NormsWriter.addThreads创建,负责nrm信息的处理
  • 对存储域进行处理
    • StoredFieldsWriterPerThread fieldsWriter由StoredFieldsWriter.addThreads创建,负责fnm,fdx,fdt的处理。
    • FieldInfos fieldInfos;

DocumentsWriter.DocWriter DocFieldProcessorPerThread.processDocument()包含以下几个过程:

4.2.1、开始处理当前文档

consumer(DocInverterPerThread).startDocument(); 
fieldsWriter(StoredFieldsWriterPerThread).startDocument();

在此版的Lucene中,几乎所有的XXXPerThread的类,都有startDocument和finishDocument两个函数,因为对同一个线程,这些对象都是复用的,而非对每一篇新来的文档都创建一套,这样也提高了效率,也牵扯到数据的清理问题。一般在startDocument函数中,清理处理上篇文档遗留的数据,在finishDocument中,收集本次处理的结果数据,并返回,一直返回到DocumentsWriter.updateDocument(Document, Analyzer, Term) 然后根据条件判断是否将数据刷新到硬盘上。

4.2.2、逐个处理文档的每一个域

由于一个线程可以连续处理多个文档,而在普通的应用中,几乎每篇文档的域都是大致相同的,为每篇文档的每个域都创建一个处理对象非常低效,因而考虑到复用域处理对象DocFieldProcessorPerField,对于每一个域都有一个此对象。

那当来到一个新的域的时候,如何更快的找到此域的处理对象呢?Lucene创建了一个DocFieldProcessorPerField[] fieldHash哈希表来方便更快查找域对应的处理对象。

当处理各个域的时候,按什么顺序呢?其实是按照域名的字典顺序。因而Lucene创建了DocFieldProcessorPerField[] fields的数组来方便按顺序处理域。

因而一个域的处理对象被放在了两个地方。

对于域的处理过程如下:

4.2.2.1、首先:对于每一个域,按照域名,在fieldHash中查找域处理对象DocFieldProcessorPerField,代码如下:

final int hashPos = fieldName.hashCode() & hashMask;//计算哈希值 
DocFieldProcessorPerField fp = fieldHash[hashPos];//找到哈希表中对应的位置 
while(fp != null && !fp.fieldInfo.name.equals(fieldName)) fp = fp.next;//链式哈希表

如果能够找到,则更新DocFieldProcessorPerField中的域信息fp.fieldInfo.update(field.isIndexed()…)

如果没有找到,则添加域到DocFieldProcessorPerThread.fieldInfos中,并创建新的DocFieldProcessorPerField,且将其加入哈希表。代码如下:

fp = new DocFieldProcessorPerField(this, fi); 
fp.next = fieldHash[hashPos]; 
fieldHash[hashPos] = fp;

如果是一个新的field,则将其加入fields数组fields[fieldCount++] = fp;

并且如果是存储域的话,用StoredFieldsWriterPerThread将其写到索引中:

if (field.isStored()) { 
  fieldsWriter.addField(field, fp.fieldInfo); 
}

4.2.2.1.1、处理存储域的过程如下:

StoredFieldsWriterPerThread.addField(Fieldable field, FieldInfo fieldInfo) 
--> localFieldsWriter.writeField(fieldInfo, field);

FieldsWriter.writeField(FieldInfo fi, Fieldable field)代码如下:

请参照fdt文件的格式,则一目了然:

fieldsStream.writeVInt(fi.number);//文档号 
byte bits = 0; 
if (field.isTokenized()) 
    bits |= FieldsWriter.FIELD_IS_TOKENIZED; 
if (field.isBinary()) 
    bits |= FieldsWriter.FIELD_IS_BINARY; 
if (field.isCompressed()) 
    bits |= FieldsWriter.FIELD_IS_COMPRESSED;

fieldsStream.writeByte(bits); //域的属性位

if (field.isCompressed()) {//对于压缩域 
    // compression is enabled for the current field 
    final byte[] data; 
    final int len; 
    final int offset; 
    // check if it is a binary field 
    if (field.isBinary()) { 
        data = CompressionTools.compress(field.getBinaryValue(), field.getBinaryOffset(), field.getBinaryLength()); 
    } else { 
        byte x[] = field.stringValue().getBytes("UTF-8"); 
        data = CompressionTools.compress(x, 0, x.length); 
    } 
    len = data.length; 
    offset = 0; 
    fieldsStream.writeVInt(len);//写长度 
    fieldsStream.writeBytes(data, offset, len);//写二进制内容 
} else {//对于非压缩域 
    // compression is disabled for the current field 
    if (field.isBinary()) {//如果是二进制域 
        final byte[] data; 
        final int len; 
        final int offset; 
        data = field.getBinaryValue(); 
        len = field.getBinaryLength(); 
        offset = field.getBinaryOffset();

        fieldsStream.writeVInt(len);//写长度 
        fieldsStream.writeBytes(data, offset, len);//写二进制内容 
    } else { 
        fieldsStream.writeString(field.stringValue());//写字符内容 
    } 
}

4.2.2.2、然后:对fields数组进行排序,是域按照名称排序。quickSort(fields, 0, fieldCount-1);

4.2.2.3、最后:按照排序号的顺序,对域逐个处理,此处处理的仅仅是索引域,代码如下:

for(int i=0;i      fields[i].consumer.processFields(fields[i].fields, fields[i].fieldCount);

域处理对象(DocFieldProcessorPerField)结构如下:

域索引链:

每个域也有自己的索引链,称为域索引链,每个域的索引链也有同线程索引链有相似的树形结构,由线程索引链中每个层次的每个层次的对象调用addField进行创建,负责对此域的处理。

和基本索引链及线程索引链不同的是,域索引链仅仅负责处理索引域,而不负责存储域的处理。

DocFieldProcessorPerField是域索引链的源头,对象结构如下:

  • DocFieldConsumerPerField consumer类型为DocInverterPerField,由DocInverterPerThread.addField创建
    • InvertedDocConsumerPerField consumer 类型为TermsHashPerField,由TermsHashPerThread.addField创建
      • TermsHashConsumerPerField consumer 类型为FreqProxTermsWriterPerField,由FreqProxTermsWriterPerThread.addField创建,负责freq, prox信息的处理
      • TermsHashPerField nextPerField
        • TermsHashConsumerPerField consumer 类型为TermVectorsTermsWriterPerField,由TermVectorsTermsWriterPerThread.addField创建,负责tvx, tvd, tvf信息的处理
    • InvertedDocEndConsumerPerField endConsumer 类型为NormsWriterPerField,由NormsWriterPerThread.addField创建,负责nrm信息的处理。

4.2.2.3.1、处理索引域的过程如下:

DocInverterPerField.processFields(Fieldable[], int) 过程如下:

  • 判断是否要形成倒排表,代码如下:
boolean doInvert = consumer.start(fields, count); 
--> TermsHashPerField.start(Fieldable[], int)  
      --> for(int i=0;i             if (fields[i].isIndexed()) 
                 return true; 
            return false;

读到这里,大家可能会发生困惑,既然XXXPerField是对于每一个域有一个处理对象的,那为什么参数传进来的是Fieldable[]数组, 并且还有域的数目count呢?

其实这不经常用到,但必须得提一下,由上面的fieldHash的实现我们可以看到,是根据域名进行哈希的,所以准确的讲,XXXPerField并非对于每一个域有一个处理对象,而是对每一组相同名字的域有相同的处理对象。

对于同一篇文档,相同名称的域可以添加多个,代码如下:

doc.add(new Field("contents", "the content of the file.", Field.Store.NO, Field.Index.NOT_ANALYZED)); 
doc.add(new Field("contents", new FileReader(f)));

则传进来的名为"contents"的域如下:

fields    Fieldable[2]  (id=52)    
    [0]    Field  (id=56)    
        binaryLength    0    
        binaryOffset    0    
        boost    1.0    
        fieldsData    "the content of the file."    
        isBinary    false    
        isCompressed    false    
        isIndexed    true    
        isStored    false    
        isTokenized    false    
        lazy    false    
        name    "contents"    
        omitNorms    false    
        omitTermFreqAndPositions    false    
        storeOffsetWithTermVector    false    
        storePositionWithTermVector    false    
        storeTermVector    false    
        tokenStream    null    
    [1]    Field  (id=58)    
        binaryLength    0    
        binaryOffset    0    
        boost    1.0    
        fieldsData    FileReader  (id=131)    
        isBinary    false    
        isCompressed    false    
        isIndexed    true    
        isStored    false    
        isTokenized    true    
        lazy    false    
        name    "contents"    
        omitNorms    false    
        omitTermFreqAndPositions    false    
        storeOffsetWithTermVector    false    
        storePositionWithTermVector    false    
        storeTermVector    false    
        tokenStream    null   

  • 对传进来的同名域逐一处理,代码如下

for(int i=0;i

    final Fieldable field = fields[i];

    if (field.isIndexed() && doInvert) {

        //仅仅对索引域进行处理

        if (!field.isTokenized()) {

            //如果此域不分词,见(1)对不分词的域的处理

        } else {

            //如果此域分词,见(2)对分词的域的处理

        }

    }

}

(1) 对不分词的域的处理

(1-1) 得到域的内容,并构建单个Token形成的SingleTokenAttributeSource。因为不进行分词,因而整个域的内容算做一个Token.

String stringValue = field.stringValue(); //stringValue    "200910240957"  
final int valueLength = stringValue.length(); 
perThread.singleToken.reinit(stringValue, 0, valueLength);

对于此域唯一的一个Token有以下的属性:

  • Term:文字信息。在处理过程中,此值将保存在TermAttribute的实现类实例化的对象TermAttributeImp里面。
  • Offset:偏移量信息,是按字或字母的起始偏移量和终止偏移量,表明此Token在文章中的位置,多用于加亮。在处理过程中,此值将保存在OffsetAttribute的实现类实例化的对象OffsetAttributeImp里面。

在SingleTokenAttributeSource里面,有一个HashMap来保存可能用于保存属性的类名(Key,准确的讲是接口)以及保存属性信息的对象(Value):

singleToken    DocInverterPerThread$SingleTokenAttributeSource  (id=150)    
    attributeImpls    LinkedHashMap  (id=945)    
    attributes    LinkedHashMap  (id=946)     
        size    2    
        table    HashMap$Entry[16]  (id=988)    
            [0]    LinkedHashMap$Entry  (id=991)     
                key    Class (org.apache.lucene.analysis.tokenattributes.TermAttribute) (id=755)     
                value    TermAttributeImpl  (id=949)    
                    termBuffer    char[19]  (id=954)    //[2, 0, 0, 9, 1, 0, 2, 4, 0, 9, 5, 7] 
                    termLength    12
     
            [7]    LinkedHashMap$Entry  (id=993)     
                key    Class (org.apache.lucene.analysis.tokenattributes.OffsetAttribute) (id=274)     
                value    OffsetAttributeImpl  (id=948)    
                    endOffset    12    
                    startOffset    0
     
    factory    AttributeSource$AttributeFactory$DefaultAttributeFactory  (id=947)    
    offsetAttribute    OffsetAttributeImpl  (id=948)    
    termAttribute    TermAttributeImpl  (id=949)   

(1-2) 得到Token的各种属性信息,为索引做准备。

consumer.start(field)做的主要事情就是根据各种属性的类型来构造保存属性的对象(HashMap中有则取出,无则构造),为索引做准备。

consumer(TermsHashPerField).start(…)

--> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的就是上述HashMap中的TermAttributeImpl   

--> consumer(FreqProxTermsWriterPerField).start(f);

      --> if (fieldState.attributeSource.hasAttribute(PayloadAttribute.class)) {

                payloadAttribute = fieldState.attributeSource.getAttribute(PayloadAttribute.class); 
                存储payload信息则得到payload的属}

--> nextPerField(TermsHashPerField).start(f);

      --> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的还是上述HashMap中的TermAttributeImpl

      --> consumer(TermVectorsTermsWriterPerField).start(f);

            --> if (doVectorOffsets) {

                      offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class); 
                      如果存储词向量则得到的是上述HashMap中的OffsetAttributeImp }

(1-3) 将Token加入倒排表

consumer(TermsHashPerField).add();

加入倒排表的过程,无论对于分词的域和不分词的域,过程是一样的,因而放到对分词的域的解析中一起说明。

(2) 对分词的域的处理

(2-1) 构建域的TokenStream

final TokenStream streamValue = field.tokenStreamValue();

//用户可以在添加域的时候,应用构造函数public Field(String name, TokenStream tokenStream) 直接传进一个TokenStream过来,这样就不用另外构建一个TokenStream了。

if (streamValue != null) 
  stream = streamValue; 
else {

  ……

  stream = docState.analyzer.reusableTokenStream(fieldInfo.name, reader);

}

此时TokenStream的各项属性值还都是空的,等待一个一个被分词后得到,此时的TokenStream对象如下:

stream    StopFilter  (id=112)    
    attributeImpls    LinkedHashMap  (id=121)    
    attributes    LinkedHashMap  (id=122)     
        size    4    
        table    HashMap$Entry[16]  (id=146)     
            [2]    LinkedHashMap$Entry  (id=148)     
                key    Class (org.apache.lucene.analysis.tokenattributes.TypeAttribute) (id=154)     
                value    TypeAttributeImpl  (id=157)    
                    type    "word"     
            [8]    LinkedHashMap$Entry  (id=150)    
                after    LinkedHashMap$Entry  (id=156)     
                    key    Class (org.apache.lucene.analysis.tokenattributes.OffsetAttribute) (id=163)     
                    value    OffsetAttributeImpl  (id=164)    
                        endOffset    0    
                        startOffset    0
     
                key    Class (org.apache.lucene.analysis.tokenattributes.TermAttribute) (id=142)     
                value    TermAttributeImpl  (id=133)    
                    termBuffer    char[17]  (id=173)    
                    termLength    0
     
            [10]    LinkedHashMap$Entry  (id=151)     
                key    Class (org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute) (id=136)     
                value    PositionIncrementAttributeImpl  (id=129)    
                    positionIncrement    1     
    currentState    AttributeSource$State  (id=123)    
    enablePositionIncrements    true    
    factory    AttributeSource$AttributeFactory$DefaultAttributeFactory  (id=125)    
    input    LowerCaseFilter  (id=127)     
        input    StandardFilter  (id=213)     
            input    StandardTokenizer  (id=218)     
                input    FileReader  (id=93)    //从文件中读出来的文本,将经过分词器分词,并一层层的Filter的处理,得到一个个Token 
    stopWords    CharArraySet$UnmodifiableCharArraySet  (id=131)    
    termAtt    TermAttributeImpl  (id=133)   

(2-2) 得到第一个Token,并初始化此Token的各项属性信息,并为索引做准备(start)。

boolean hasMoreTokens = stream.incrementToken();//得到第一个Token

OffsetAttribute offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class);//得到偏移量属性

offsetAttribute    OffsetAttributeImpl  (id=164)    
    endOffset    8    
    startOffset    0   

PositionIncrementAttribute posIncrAttribute = fieldState.attributeSource.addAttribute(PositionIncrementAttribute.class);//得到位置属性

posIncrAttribute    PositionIncrementAttributeImpl  (id=129)    
    positionIncrement    1   

consumer.start(field);//其中得到了TermAttribute属性,如果存储payload则得到PayloadAttribute属性,如果存储词向量则得到OffsetAttribute属性。

(2-3) 进行循环,不断的取下一个Token,并添加到倒排表

for(;;) {

    if (!hasMoreTokens) break;

    …… 
    consumer.add();

    …… 
    hasMoreTokens = stream.incrementToken(); 
}

(2-4) 添加Token到倒排表的过程consumer(TermsHashPerField).add()

TermsHashPerField对象主要包括以下部分:

  • CharBlockPool charPool; 用于存储Token的文本信息,如果不足时,从DocumentsWriter中的freeCharBlocks分配
  • ByteBlockPool bytePool;用于存储freq, prox信息,如果不足时,从DocumentsWriter中的freeByteBlocks分配
  • IntBlockPool intPool; 用于存储分别指向每个Token在bytePool中freq和prox信息的偏移量。如果不足时,从DocumentsWriter的freeIntBlocks分配
  • TermsHashConsumerPerField consumer类型为FreqProxTermsWriterPerField,用于写freq, prox信息到缓存中。
  • RawPostingList[] postingsHash = new RawPostingList[postingsHashSize];存储倒排表,每一个Term都有一个RawPostingList (PostingList),其中包含了int textStart,也即文本在charPool中的偏移量,int byteStart,即此Term的freq和prox信息在bytePool中的起始偏移量,int intStart,即此term的在intPool中的起始偏移量。

形成倒排表的过程如下:

//得到token的文本及文本长度

final char[] tokenText = termAtt.termBuffer();//[s, t, u, d, e, n, t, s]

final int tokenTextLen = termAtt.termLength();//tokenTextLen 8

//按照token的文本计算哈希值,以便在postingsHash中找到此token对应的倒排表

int downto = tokenTextLen; 
int code = 0; 
while (downto > 0) { 
  char ch = tokenText[—downto]; 
  code = (code*31) + ch; 
}

int hashPos = code & postingsHashMask;

//在倒排表哈希表中查找此Token,如果找到相应的位置,但是不是此Token,说明此位置存在哈希冲突,采取重新哈希rehash的方法。

p = postingsHash[hashPos];

if (p != null && !postingEquals(tokenText, tokenTextLen)) {  
  final int inc = ((code>>8)+code)|1; 
  do { 
    code += inc; 
    hashPos = code & postingsHashMask; 
    p = postingsHash[hashPos]; 
  } while (p != null && !postingEquals(tokenText, tokenTextLen)); 
}

//如果此Token之前从未出现过

if (p == null) {

    if (textLen1 + charPool.charUpto > DocumentsWriter.CHAR_BLOCK_SIZE) {

        //当charPool不足的时候,在freeCharBlocks中分配新的buffer

        charPool.nextBuffer();

    }

    //从空闲的倒排表中分配新的倒排表

    p = perThread.freePostings[--perThread.freePostingsCount];

    //将文本复制到charPool中

    final char[] text = charPool.buffer; 
    final int textUpto = charPool.charUpto; 
    p.textStart = textUpto + charPool.charOffset; 
    charPool.charUpto += textLen1; 
    System.arraycopy(tokenText, 0, text, textUpto, tokenTextLen); 
    text[textUpto+tokenTextLen] = 0xffff;

    //将倒排表放入哈希表中

    postingsHash[hashPos] = p; 
    numPostings++;

    if (numPostingInt + intPool.intUpto > DocumentsWriter.INT_BLOCK_SIZE) intPool.nextBuffer();

    //当intPool不足的时候,在freeIntBlocks中分配新的buffer。

    if (DocumentsWriter.BYTE_BLOCK_SIZE - bytePool.byteUpto < numPostingInt*ByteBlockPool.FIRST_LEVEL_SIZE)

        bytePool.nextBuffer();

    //当bytePool不足的时候,在freeByteBlocks中分配新的buffer。

    //此处streamCount为2,表明在intPool中,每两项表示一个词,一个是指向bytePool中freq信息偏移量的,一个是指向bytePool中prox信息偏移量的。

    intUptos = intPool.buffer; 
    intUptoStart = intPool.intUpto; 
    intPool.intUpto += streamCount;

    p.intStart = intUptoStart + intPool.intOffset;

    //在bytePool中分配两个空间,一个放freq信息,一个放prox信息的。  
    for(int i=0;i

        final int upto = bytePool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE); 
        intUptos[intUptoStart+i] = upto + bytePool.byteOffset; 
    } 
    p.byteStart = intUptos[intUptoStart];

    //当Term原来没有出现过的时候,调用newTerm

    consumer(FreqProxTermsWriterPerField).newTerm(p);

}

//如果此Token之前曾经出现过,则调用addTerm。

else {

    intUptos = intPool.buffers[p.intStart >> DocumentsWriter.INT_BLOCK_SHIFT]; 
    intUptoStart = p.intStart & DocumentsWriter.INT_BLOCK_MASK; 
    consumer(FreqProxTermsWriterPerField).addTerm(p);

}

(2-5) 添加新Term的过程,consumer(FreqProxTermsWriterPerField).newTerm

final void newTerm(RawPostingList p0) { 
  FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0; 
  p.lastDocID = docState.docID; //当一个新的term出现的时候,包含此Term的就只有本篇文档,记录其ID 
  p.lastDocCode = docState.docID << 1; //docCode是文档ID左移一位,为什么左移,请参照索引文件格式(1)中的或然跟随规则。 
  p.docFreq = 1; //docFreq这里用词可能容易引起误会,docFreq这里指的是此文档所包含的此Term的次数,并非包含此Term的文档的个数。 
  writeProx(p, fieldState.position); //写入prox信息到bytePool中,此时freq信息还不能写入,因为当前的文档还没有处理完,尚不知道此文档包含此Term的总数。 
}

writeProx(FreqProxTermsWriter.PostingList p, int proxCode) {

  termsHashPerField.writeVInt(1, proxCode<<1);//第一个参数所谓1,也就是写入此文档在intPool中的第1项——prox信息。为什么左移一位呢?是因为后面可能跟着payload信息,参照索引文件格式(1)中或然跟随规则。 
  p.lastPosition = fieldState.position;//总是要记录lastDocID, lastPostion,是因为要计算差值,参照索引文件格式(1)中的差值规则。

}

(2-6) 添加已有Term的过程

final void addTerm(RawPostingList p0) {

  FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0;

  if (docState.docID != p.lastDocID) {

      //当文档ID变了的时候,说明上一篇文档已经处理完毕,可以写入freq信息了。

      //第一个参数所谓0,也就是写入上一篇文档在intPool中的第0项——freq信息。至于信息为何这样写,参照索引文件格式(1)中的或然跟随规则,及tis文件格式。

      if (1 == p.docFreq) 
        termsHashPerField.writeVInt(0, p.lastDocCode|1); 
      else { 
        termsHashPerField.writeVInt(0, p.lastDocCode); 
        termsHashPerField.writeVInt(0, p.docFreq); 
      } 
      p.docFreq = 1;//对于新的文档,freq还是为1. 
      p.lastDocCode = (docState.docID - p.lastDocID) << 1;//文档号存储差值 
      p.lastDocID = docState.docID; 
      writeProx(p, fieldState.position);  
    } else {

      //当文档ID不变的时候,说明此文档中这个词又出现了一次,从而freq加一,写入再次出现的位置信息,用差值。 
      p.docFreq++; 
      writeProx(p, fieldState.position-p.lastPosition); 
  } 
}

(2-7) 结束处理当前域

consumer(TermsHashPerField).finish();

--> FreqProxTermsWriterPerField.finish()

--> TermVectorsTermsWriterPerField.finish()

endConsumer(NormsWriterPerField).finish();

--> norms[upto] = Similarity.encodeNorm(norm);//计算标准化因子的值。

--> docIDs[upto] = docState.docID;

4.2.3、结束处理当前文档

final DocumentsWriter.DocWriter one = fieldsWriter(StoredFieldsWriterPerThread).finishDocument();

存储域返回结果:一个写成了二进制的存储域缓存。

one    StoredFieldsWriter$PerDoc  (id=322)    
    docID    0    
    fdt    RAMOutputStream  (id=325)    
        bufferLength    1024    
        bufferPosition    40    
        bufferStart    0    
        copyBuffer    null    
        currentBuffer    byte[1024]  (id=332)    
        currentBufferIndex    0    
        file    RAMFile  (id=333)    
        utf8Result    UnicodeUtil$UTF8Result  (id=335)    
    next    null    
    numStoredFields    2    
    this$0    StoredFieldsWriter  (id=327)   

final DocumentsWriter.DocWriter two = consumer(DocInverterPerThread).finishDocument();

--> NormsWriterPerThread.finishDocument()

--> TermsHashPerThread.finishDocument()

索引域的返回结果为null

4.3、用DocumentsWriter.finishDocument结束本次文档添加

代码:

DocumentsWriter.updateDocument(Document, Analyzer, Term)

--> DocumentsWriter.finishDocument(DocumentsWriterThreadState, DocumentsWriter$DocWriter)

      --> doPause = waitQueue.add(docWriter);//有关waitQueue,在DocumentsWriter的缓存管理中已作解释

            --> DocumentsWriter$WaitQueue.writeDocument(DocumentsWriter$DocWriter)

                  --> StoredFieldsWriter$PerDoc.finish()

                        --> fieldsWriter.flushDocument(perDoc.numStoredFields, perDoc.fdt);将存储域信息真正写入文件。


5、DocumentsWriter对CharBlockPool,ByteBlockPool,IntBlockPool的缓存管理

  • 在索引的过程中,DocumentsWriter将词信息(term)存储在CharBlockPool中,将文档号(doc ID),词频(freq)和位置(prox)信息存储在ByteBlockPool中。
  • 在ByteBlockPool中,缓存是分块(slice)分配的,块(slice)是分层次的,层次越高,此层的块越大,每一层的块大小事相同的。
    • nextLevelArray表示的是当前层的下一层是第几层,可见第9层的下一层还是第9层,也就是说最高有9层。
    • levelSizeArray表示每一层的块大小,第一层是5个byte,第二层是14个byte以此类推。

ByteBlockPool类中有以下静态变量:

final static int[] nextLevelArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9}; 
final static int[] levelSizeArray = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200};

  • 在ByteBlockPool中分配一个块的代码如下:

 

//此函数仅仅在upto已经是当前块的结尾的时候方才调用来分配新块。

public int allocSlice(final byte[] slice, final int upto) {

  //可根据块的结束符来得到块所在的层次。从而我们可以推断,每个层次的块都有不同的结束符,第1层为16,第2层位17,第3层18,依次类推。

  final int level = slice[upto] & 15;

  //从数组总得到下一个层次及下一层块的大小。

  final int newLevel = nextLevelArray[level];

  final int newSize = levelSizeArray[newLevel];

  // 如果当前缓存总量不够大,则从DocumentsWriter的freeByteBlocks中分配。

  if (byteUpto > DocumentsWriter.BYTE_BLOCK_SIZE-newSize)

    nextBuffer();

  final int newUpto = byteUpto;

  final int offset = newUpto + byteOffset;

  byteUpto += newSize;

  //当分配了新的块的时候,需要有一个指针从本块指向下一个块,使得读取此信息的时候,能够在此块读取结束后,到下一个块继续读取。

  //这个指针需要4个byte,在本块中,除了结束符所占用的一个byte之外,之前的三个byte的数据都应该移到新的块中,从而四个byte连起来形成一个指针。

  buffer[newUpto] = slice[upto-3];

  buffer[newUpto+1] = slice[upto-2];

  buffer[newUpto+2] = slice[upto-1];

  // 将偏移量(也即指针)写入到连同结束符在内的四个byte

  slice[upto-3] = (byte) (offset >>> 24);

  slice[upto-2] = (byte) (offset >>> 16);

  slice[upto-1] = (byte) (offset >>> 8);

  slice[upto] = (byte) offset;

  // 在新的块的结尾写入新的结束符,结束符和层次的关系就是(endbyte = 16 | level)

  buffer[byteUpto-1] = (byte) (16|newLevel);

  return newUpto+3;

}

  • 在ByteBlockPool中,文档号和词频(freq)信息是应用或然跟随原则写到一个块中去的,而位置信息(prox)是写入到另一个块中去的,对于同一个词,这两块的偏移量保存在IntBlockPool中。因而在IntBlockPool中,每一个词都有两个int,第0个表示docid + freq在ByteBlockPool中的偏移量,第1个表示prox在ByteBlockPool中的偏移量。
  • 在写入docid + freq信息的时候,调用termsHashPerField.writeVInt(0, p.lastDocCode),第一个参数表示向此词的第0个偏移量写入;在写入prox信息的时候,调用termsHashPerField.writeVInt(1, (proxCode<<1)|1),第一个参数表示向此词的第1个偏移量写入。
  • CharBlockPool是按照出现的先后顺序保存词(term)
  • 在TermsHashPerField中,有一个成员变量RawPostingList[] postingsHash,为每一个term分配了一个RawPostingList,将上述三个缓存关联起来。

 

abstract class RawPostingList {

  final static int BYTES_SIZE = DocumentsWriter.OBJECT_HEADER_BYTES + 3*DocumentsWriter.INT_NUM_BYTE;

  int textStart; //此词在CharBlockPool中的偏移量,由此可以知道是哪个词。

  int intStart; //此词在IntBlockPool中的偏移量,在指向的位置有两个int,一个是docid + freq信息的偏移量,一个是prox信息的偏移量。

  int byteStart; //此词在ByteBlockPool中的起始偏移量

}

static final class PostingList extends RawPostingList {

  int docFreq;                                    // 此词在此文档中出现的次数

  int lastDocID;                                  // 上次处理完的包含此词的文档号。

  int lastDocCode;                                // 文档号和词频按照或然跟随原则形成的编码

  int lastPosition;                               // 上次处理完的此词的位置

}

这里需要说明的是,在IntBlockPool中保存了两个在ByteBlockPool中的偏移量,而在RawPostingList的byteStart又保存了在ByteBlockPool中的偏移量,这两者有什么区别呢?

在IntBlockPool中保存的分别指向docid+freq及prox信息在ByteBlockPool中的偏移量是主要用来写入信息的,它记录的偏移量是下一个要写入的docid+freq或者prox在ByteBlockPool中的位置,随着信息的不断写入,IntBlockPool中的两个偏移量是不断改变的,始终指向下一个可以写入的位置。

RawPostingList中byteStart主要是用来读取docid及prox信息的,当索引过程基本结束,所有的信息都写入在缓存中了,那么如何找到此词对应的文档号偏移量及位置信息,然后写到索引文件中去呢?自然是通过RawPostingList找到byteStart,然后根据byteStart在ByteBlockPool中找到docid+freq及prox信息的起始位置,从起始位置开始的两个大小为5的块,第一个就是docid+freq信息的源头,第二个就是prox信息的源头,如果源头的块中包含了所有的信息,读出来就可以了,如果源头的块中有指针,则沿着指针寻找到下一个块,从而可以找到所有的信息。

  • 下面举一个实例来表明如果进行缓存管理的:

此例子中,准备添加三个文件:

file01: common common common common common term

file02: common common common common common term term

file03: term term term common common common common common

file04: term

(1) 添加第一篇文档第一个common

  • 在CharBlockPool中分配6个char来存放"common"字符串
  • 在ByteBlockPool中分配两个块,每个块大小为5,以16结束,第一个块用来存放docid+freq信息,第二个块用来存放prox信息。此时docid+freq信息没有写入,docid+freq信息总是在下一篇文档的处理过程出现了"common"的时候方才写入,因为当一篇文档没有处理完毕的时候,freq也即词频是无法知道的。而prox信息存放0,是因为第一个common的位置为0,但是此0应该左移一位,最后一位置0表示没有payload存储,因而0<<1 + 0 = 0。
  • 在IntBlockPool中分配两个int,一个指向第0个位置,是因为当前没有docid+freq信息写入,第二个指向第6个位置,是因为第5个位置写入了prox信息。所以IntBlockPool中存放的是下一个要写入的位置。

[图]添加第一篇文档第一个common

 

(2) 添加第四个common

  • 在ByteBlockPool中,prox信息已经存放了4个,第一个0是代表第一个位置为0,后面不跟随payload。第二个2表示,位置增量(差值原则)为1,后面不跟随payload(或然跟随原则),1<<1 + 0 =2。第三个第四个同第二个。

添加第四个common

 

(3) 添加第五个common

  • ByteBlockPool中,存放prox信息的块已经全部填满,必须重新分配新的块。
  • 新的块层次为2,大小为14,在缓存的最后追加分配。
  • 原块中连同结束位在内的四个byte作为指针(绿色部分),指向新的块的其实地址,在此为10.
  • 被指针占用的结束位之前的三位移到新的块中,也即6, 7, 8移到10, 11, 12处,13处是第五个common的prox信息。
  • 指针的值并不都是四个byte的最后一位,当缓存很大的时候,指针的值也会很大。比如指针出现[0, 0, 0, -56],最后一位为负,并不表示指向的负位置,而是最后一个byte的第一位为1,显示为有符号数为负,-56的二进制是11001000,和前三个byte拼称int,大小为200也即指向第200个位置。比如指针出现[0, 0, 1, 2],其转换为二进制的int为100000010,大小为258,也即指向第258个位置。比如指针出现 
    [0, 0, 1, -98],转换为二进制的int为110011110,大小为414,也即指向第414个位置。

[图]添加第五个common

 

(4) 添加第一篇文档,第一个term

  • CharBlockPool中分配了5个char来存放"term"
  • ByteBlockPool中分配了两个块来分别存放docid+freq信息和prox信息。第一个块没有信息写入,第二个块写入了"term"的位置信息,即出现在第5个位置,并且后面没有payload,5<<1 + 0 = 10。
  • IntBlockPool中分配了两个int来指向"term"的两个块中下一个写入的位置。

[图]添加第一篇文档第一个term

 

(5) 添加第二篇文档第一个common

  • 第一篇文档的common的docid+freq信息写入。在第一篇文档中,"common"出现了5次,文档号为0,按照或然跟随原则,存放的信息为[docid<<1 + 0, 5] = [0, 5],docid左移一位,最后一位为0表示freq大于1。
  • 第二篇文档第一个common的位置信息也写入了,位置为0,0<<1 + 0 = 0。

[图]添加第二篇文档第一个common

 

(6) 添加第二篇文档第一个term

  • 第一篇文档中的term的docid+freq信息写入,在第一篇文档中,"term"出现了1次,文档号为0,所以存储信息为[docid<<1 + 1] = [1],文档号左移一位,最后一位为1表示freq为1。
  • 第二篇文档第一个term的prox信息也写入了,在第5个位置,5<<1 + 0 = 10。
  • 第二篇文档中的5个common的prox信息也写入了,分别为从14到18的[0, 2, 2, 2, 2]

[图]添加第二篇文档第一个term

 

(7) 添加第三篇文档的第一个term

  • 第二篇文档的term的docid+freq信息写入,在第二篇文档中,文档号为1,"term"出现了2次,所以存储为[docid<<1 + 0, freq] = [2, 2],存储在25, 26两个位置。
  • 第二篇文档中两个term的位置信息也写入了,为30, 31的[10, 2],也即出现在第5个,第6个位置,后面不跟随payload。
  • 第三篇文档的第一个term的位置信息也写入了,在第0个位置,不跟payload,为32保存的[0]

[图]添加第三篇文档的第一个term

 

(8) 添加第三篇文档第二个term

  • term的位置信息已经填满了,必须分配新的块,层次为2,大小为14,结束符为17,也即图中34到47的位置。
  • 30到33的四个byte组成一个int指针,指向第34个位置
  • 原来30到32的三个prox信息移到34到36的位置。
  • 在37处保存第三篇文档第二个term的位置信息,位置为1,不跟随payload,1<<1 + 0 = 2。

[图]添加第三篇文档第二个term

 

(9) 添加第三篇文档第四个common

  • 第二篇文档中"common"的docid+freq信息写入,文档号为1,出现了5次,存储为[docid << 1 + 0, freq],docid取差值为1,因而存储为 [2, 5],即2,3的位置。
  • 第三篇文档中前四个common的位置信息写入,即从19到22的[6, 2, 2, 2],即出现在第3个,第4个,第5个,第6个位置。
  • 第三篇文档中的第三个"term"的位置信息也写入,为38处的[2]。

[图]添加第三篇文档第四个common

 

(10) 添加第三篇文档的第五个common

  • 虽然common已经分配了层次为2,大小为14的第二个块(从10到23),不过还是用完了,需要在缓存的最后分配新的块,层次为3,大小为20,结束符为18,也即从48到67的位置。
  • 从20到23的四个byte组成一个int指针指向新分配的块。
  • 原来20到22的数据移到48至50的位置。
  • 第三篇文档的第五个common的位置信息写入,为第51个位置的[2],也即紧跟上个common,后面没有payload信息。

[图]添加第三篇文档的第五个common

 

(11) 添加第四篇文档的第一个term

  • 写入第三篇文档的term的docid+freq信息,文档号为2,出现了三次,存储为[docid<<1+0, freq],docid取差值为1,因而存储为[2, 3]。
  • 然而存储term的docid+freq信息的块已经满了,需要在缓存的最后追加新的块,层次为2,大小为14,结束符为17,即从68到81的位置。
  • 从25到28的四个byte组成一个int指针指向新分配的块。
  • 原来25到26的信息移到68, 69处,在70, 71处写入第三篇文档的docid+freq信息[2, 3]

[图]添加第四篇文档的第一个term

 

(12) 最终PostingList, CharBlockPool, IntBlockPool,ByteBlockPool的关系如下图:



 

[图]整体关系图

 


 

[图]整体关系图

6、关闭IndexWriter对象

代码:

writer.close();

--> IndexWriter.closeInternal(boolean)

      --> (1) 将索引信息由内存写入磁盘: flush(waitForMerges, true, true); 
      --> (2) 进行段合并: mergeScheduler.merge(this);

对段的合并将在后面的章节进行讨论,此处仅仅讨论将索引信息由写入磁盘的过程。

代码:

IndexWriter.flush(boolean triggerMerge, boolean flushDocStores, boolean flushDeletes)

--> IndexWriter.doFlush(boolean flushDocStores, boolean flushDeletes)

      --> IndexWriter.doFlushInternal(boolean flushDocStores, boolean flushDeletes)

将索引写入磁盘包括以下几个过程:

  • 得到要写入的段名:String segment = docWriter.getSegment();
  • DocumentsWriter将缓存的信息写入段:docWriter.flush(flushDocStores);
  • 生成新的段信息对象:newSegment = new SegmentInfo(segment, flushedDocCount, directory, false, true, docStoreOffset, docStoreSegment, docStoreIsCompoundFile, docWriter.hasProx());
  • 准备删除文档:docWriter.pushDeletes();
  • 生成cfs段:docWriter.createCompoundFile(segment);
  • 删除文档:applyDeletes();

6.1、得到要写入的段名

代码:

SegmentInfo newSegment = null;

final int numDocs = docWriter.getNumDocsInRAM();//文档总数

String docStoreSegment = docWriter.getDocStoreSegment();//存储域和词向量所要要写入的段名,"_0"   

int docStoreOffset = docWriter.getDocStoreOffset();//存储域和词向量要写入的段中的偏移量

String segment = docWriter.getSegment();//段名,"_0"

在Lucene的索引文件结构一章做过详细介绍,存储域和词向量可以和索引域存储在不同的段中。

6.2、将缓存的内容写入段

代码:

flushedDocCount = docWriter.flush(flushDocStores);

此过程又包含以下两个阶段;

  • 按照基本索引链关闭存储域和词向量信息
  • 按照基本索引链的结构将索引结果写入段

6.2.1、按照基本索引链关闭存储域和词向量信息

代码为:

closeDocStore();

flushState.numDocsInStore = 0;

其主要是根据基本索引链结构,关闭存储域和词向量信息:

  • consumer(DocFieldProcessor).closeDocStore(flushState);
    • consumer(DocInverter).closeDocStore(state);
      • consumer(TermsHash).closeDocStore(state);
        • consumer(FreqProxTermsWriter).closeDocStore(state);
        • if (nextTermsHash != null) nextTermsHash.closeDocStore(state);
          • consumer(TermVectorsTermsWriter).closeDocStore(state);
      • endConsumer(NormsWriter).closeDocStore(state);
    • fieldsWriter(StoredFieldsWriter).closeDocStore(state);

其中有实质意义的是以下两个closeDocStore:

  • 词向量的关闭:TermVectorsTermsWriter.closeDocStore(SegmentWriteState)

void closeDocStore(final SegmentWriteState state) throws IOException {

                     if (tvx != null) { 
            //为不保存词向量的文档在tvd文件中写入零。即便不保存词向量,在tvx, tvd中也保留一个位置 
            fill(state.numDocsInStore - docWriter.getDocStoreOffset()); 
            //关闭tvx, tvf, tvd文件的写入流 
            tvx.close(); 
            tvf.close(); 
            tvd.close(); 
            tvx = null; 
            //记录写入的文件名,为以后生成cfs文件的时候,将这些写入的文件生成一个统一的cfs文件。 
            state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_INDEX_EXTENSION); 
            state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_FIELDS_EXTENSION); 
            state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_DOCUMENTS_EXTENSION); 
            //从DocumentsWriter的成员变量openFiles中删除,未来可能被IndexFileDeleter删除 
            docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_INDEX_EXTENSION); 
            docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_FIELDS_EXTENSION); 
            docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_DOCUMENTS_EXTENSION); 
            lastDocID = 0; 
        }     
}
  • 存储域的关闭:StoredFieldsWriter.closeDocStore(SegmentWriteState)

public void closeDocStore(SegmentWriteState state) throws IOException {

    //关闭fdx, fdt写入流

    fieldsWriter.close(); 
    --> fieldsStream.close(); 
    --> indexStream.close(); 
    fieldsWriter = null; 
    lastDocID = 0;

    //记录写入的文件名 
    state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_EXTENSION); 
    state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_INDEX_EXTENSION); 
    state.docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_EXTENSION); 
    state.docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_INDEX_EXTENSION); 
}


6.2.2、按照基本索引链的结构将索引结果写入段

代码为:

consumer(DocFieldProcessor).flush(threads, flushState);

    //回收fieldHash,以便用于下一轮的索引,为提高效率,索引链中的对象是被复用的。

    Map> childThreadsAndFields = new HashMap>(); 
    for ( DocConsumerPerThread thread : threads) { 
        DocFieldProcessorPerThread perThread = (DocFieldProcessorPerThread) thread; 
        childThreadsAndFields.put(perThread.consumer, perThread.fields()); 
        perThread.trimFields(state); 
    }

    //写入存储域

    --> fieldsWriter(StoredFieldsWriter).flush(state);

    //写入索引域

    --> consumer(DocInverter).flush(childThreadsAndFields, state);

    //写入域元数据信息,并记录写入的文件名,以便以后生成cfs文件

    --> final String fileName = state.segmentFileName(IndexFileNames.FIELD_INFOS_EXTENSION);

    --> fieldInfos.write(state.directory, fileName);

    --> state.flushedFiles.add(fileName);

此过程也是按照基本索引链来的:

  • consumer(DocFieldProcessor).flush(…);
    • consumer(DocInverter).flush(…);
      • consumer(TermsHash).flush(…);
        • consumer(FreqProxTermsWriter).flush(…);
        • if (nextTermsHash != null) nextTermsHash.flush(…);
          • consumer(TermVectorsTermsWriter).flush(…);
      • endConsumer(NormsWriter).flush(…);
    • fieldsWriter(StoredFieldsWriter).flush(…);

6.2.2.1、写入存储域

代码为:

StoredFieldsWriter.flush(SegmentWriteState state) { 
    if (state.numDocsInStore > 0) { 
      initFieldsWriter(); 
      fill(state.numDocsInStore - docWriter.getDocStoreOffset()); 
    } 
    if (fieldsWriter != null) 
      fieldsWriter.flush(); 
  }

从代码中可以看出,是写入fdx, fdt两个文件,但是在上述的closeDocStore已经写入了,并且把state.numDocsInStore置零,fieldsWriter设为null,在这里其实什么也不做。

6.2.2.2、写入索引域

代码为:

DocInverter.flush(Map>, SegmentWriteState)

    //写入倒排表及词向量信息

    --> consumer(TermsHash).flush(childThreadsAndFields, state);

    //写入标准化因子

    --> endConsumer(NormsWriter).flush(endChildThreadsAndFields, state);

6.2.2.2.1、写入倒排表及词向量信息

代码为:

TermsHash.flush(Map>, SegmentWriteState)

    //写入倒排表信息

    --> consumer(FreqProxTermsWriter).flush(childThreadsAndFields, state);

   //回收RawPostingList

    --> shrinkFreePostings(threadsAndFields, state);

    //写入词向量信息

    --> if (nextTermsHash != null) nextTermsHash.flush(nextThreadsAndFields, state);

          --> consumer(TermVectorsTermsWriter).flush(childThreadsAndFields, state);

6.2.2.2.1.1、写入倒排表信息

代码为:

FreqProxTermsWriter.flush(Map                                       Collection>, SegmentWriteState)

    (a) 所有域按名称排序,使得同名域能够一起处理

    Collections.sort(allFields);

    final int numAllFields = allFields.size();

    (b) 生成倒排表的写对象

    final FormatPostingsFieldsConsumer consumer = new FormatPostingsFieldsWriter(state, fieldInfos);

    int start = 0;

    (c) 对于每一个域

    while(start < numAllFields) {

        (c-1) 找出所有的同名域

        final FieldInfo fieldInfo = allFields.get(start).fieldInfo;

        final String fieldName = fieldInfo.name;

        int end = start+1;

        while(end < numAllFields && allFields.get(end).fieldInfo.name.equals(fieldName))

            end++;

        FreqProxTermsWriterPerField[] fields = new FreqProxTermsWriterPerField[end-start];

        for(int i=start;i

            fields[i-start] = allFields.get(i);

            fieldInfo.storePayloads |= fields[i-start].hasPayloads;

        }

        (c-2) 将同名域的倒排表添加到文件

        appendPostings(fields, consumer);

       (c-3) 释放空间

        for(int i=0;i

            TermsHashPerField perField = fields[i].termsHashPerField;

            int numPostings = perField.numPostings;

            perField.reset();

            perField.shrinkHash(numPostings);

            fields[i].reset();

        }

        start = end;

    }

    (d) 关闭倒排表的写对象

    consumer.finish();

(b) 生成倒排表的写对象

代码为:

public FormatPostingsFieldsWriter(SegmentWriteState state, FieldInfos fieldInfos) throws IOException { 
    dir = state.directory; 
    segment = state.segmentName; 
    totalNumDocs = state.numDocs; 
    this.fieldInfos = fieldInfos; 
    //用于写tii,tis 
    termsOut = new TermInfosWriter(dir, segment, fieldInfos, state.termIndexInterval); 
    //用于写freq, prox的跳表  
    skipListWriter = new DefaultSkipListWriter(termsOut.skipInterval, termsOut.maxSkipLevels, totalNumDocs, null, null); 
    //记录写入的文件名, 
    state.flushedFiles.add(state.segmentFileName(IndexFileNames.TERMS_EXTENSION)); 
    state.flushedFiles.add(state.segmentFileName(IndexFileNames.TERMS_INDEX_EXTENSION));  
    //用以上两个写对象,按照一定的格式写入段 
    termsWriter = new FormatPostingsTermsWriter(state, this); 
}

对象结构如下:

consumer    FormatPostingsFieldsWriter  (id=119)  //用于处理一个域 
    dir    SimpleFSDirectory  (id=126)   //目标索引文件夹 
    totalNumDocs    8   //文档总数 
    fieldInfos    FieldInfos  (id=70)  //域元数据信息   
    segment    "_0"   //段名 
    skipListWriter    DefaultSkipListWriter  (id=133)  //freq, prox中跳表的写对象   
    termsOut    TermInfosWriter  (id=125)  //tii, tis文件的写对象 
    termsWriter    FormatPostingsTermsWriter  (id=135)  //用于添加词(Term) 
        currentTerm    null    
        currentTermStart    0    
        fieldInfo    null    
        freqStart    0    
        proxStart    0    
        termBuffer    null    
        termsOut    TermInfosWriter  (id=125)    
        docsWriter    FormatPostingsDocsWriter  (id=139)  //用于写入此词的docid, freq信息 
            df    0    
            fieldInfo    null    
            freqStart    0    
            lastDocID    0    
            omitTermFreqAndPositions    false    
            out    SimpleFSDirectory$SimpleFSIndexOutput  (id=144)    
            skipInterval    16    
            skipListWriter    DefaultSkipListWriter  (id=133)    
            storePayloads    false    
            termInfo    TermInfo  (id=151)    
            totalNumDocs    8     
            posWriter    FormatPostingsPositionsWriter  (id=146)  //用于写入此词在此文档中的位置信息   
                lastPayloadLength    -1    
                lastPosition    0    
                omitTermFreqAndPositions    false    
                out    SimpleFSDirectory$SimpleFSIndexOutput  (id=157)    
                parent    FormatPostingsDocsWriter  (id=139)    
                storePayloads    false   
  • FormatPostingsFieldsWriter.addField(FieldInfo field)用于添加索引域信息,其返回FormatPostingsTermsConsumer用于添加词信息
  • FormatPostingsTermsConsumer.addTerm(char[] text, int start)用于添加词信息,其返回FormatPostingsDocsConsumer用于添加freq信息
  • FormatPostingsDocsConsumer.addDoc(int docID, int termDocFreq)用于添加freq信息,其返回FormatPostingsPositionsConsumer用于添加prox信息
  • FormatPostingsPositionsConsumer.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength)用于添加prox信息

(c-2) 将同名域的倒排表添加到文件

代码为:

 

FreqProxTermsWriter.appendPostings(FreqProxTermsWriterPerField[], FormatPostingsFieldsConsumer) {

    int numFields = fields.length;

    final FreqProxFieldMergeState[] mergeStates = new FreqProxFieldMergeState[numFields];

    for(int i=0;i

      FreqProxFieldMergeState fms = mergeStates[i] = new FreqProxFieldMergeState(fields[i]);

      boolean result = fms.nextTerm(); //对所有的域,取第一个词(Term)

    }

    (1) 添加此域,虽然有多个域,但是由于是同名域,只取第一个域的信息即可。返回的是用于添加此域中的词的对象。

    final FormatPostingsTermsConsumer termsConsumer = consumer.addField(fields[0].fieldInfo);

    FreqProxFieldMergeState[] termStates = new FreqProxFieldMergeState[numFields];

    final boolean currentFieldOmitTermFreqAndPositions = fields[0].fieldInfo.omitTermFreqAndPositions;

    (2) 此while循环是遍历每一个尚有未处理的词的域,依次按照词典顺序处理这些域所包含的词。当一个域中的所有的词都被处理过后,则numFields减一,并从mergeStates数组中移除此域。直到所有的域的所有的词都处理完毕,方才退出此循环。

    while(numFields > 0) {

       (2-1) 找出所有域中按字典顺序的下一个词。可能多个同名域中,都包含同一个term,因而要遍历所有的numFields,得到所有的域里的下一个词,numToMerge即为有多少个域包含此词。

      termStates[0] = mergeStates[0];

      int numToMerge = 1;

      for(int i=1;i

        final char[] text = mergeStates[i].text;

        final int textOffset = mergeStates[i].textOffset;

        final int cmp = compareText(text, textOffset, termStates[0].text, termStates[0].textOffset);

        if (cmp < 0) {

          termStates[0] = mergeStates[i];

          numToMerge = 1;

        } else if (cmp == 0)

          termStates[numToMerge++] = mergeStates[i];

      }

      (2-2) 添加此词,返回FormatPostingsDocsConsumer用于添加文档号(doc ID)及词频信息(freq)

      final FormatPostingsDocsConsumer docConsumer = termsConsumer.addTerm(termStates[0].text, termStates[0].textOffset);

      (2-3) 由于共numToMerge个域都包含此词,每个词都有一个链表的文档号表示包含这些词的文档。此循环遍历所有的包含此词的域,依次按照从小到大的循序添加包含此词的文档号及词频信息。当一个域中对此词的所有文档号都处理过了,则numToMerge减一,并从termStates数组中移除此域。当所有包含此词的域的所有文档号都处理过了,则结束此循环。

      while(numToMerge > 0) {

        (2-3-1) 找出最小的文档号

        FreqProxFieldMergeState minState = termStates[0];

        for(int i=1;i

          if (termStates[i].docID < minState.docID)

            minState = termStates[i];

        final int termDocFreq = minState.termFreq;

        (2-3-2) 添加文档号及词频信息,并形成跳表,返回FormatPostingsPositionsConsumer用于添加位置(prox)信息

        final FormatPostingsPositionsConsumer posConsumer = docConsumer.addDoc(minState.docID, termDocFreq);

        //ByteSliceReader是用于读取bytepool中的prox信息的。

        final ByteSliceReader prox = minState.prox;

        if (!currentFieldOmitTermFreqAndPositions) {

          int position = 0;

          (2-3-3) 此循环对包含此词的文档,添加位置信息

          for(int j=0;j

            final int code = prox.readVInt();

            position += code >> 1;

            final int payloadLength;

            // 如果此位置有payload信息,则从bytepool中读出,否则设为零。

            if ((code & 1) != 0) {

              payloadLength = prox.readVInt();

              if (payloadBuffer == null || payloadBuffer.length < payloadLength)

                payloadBuffer = new byte[payloadLength];

              prox.readBytes(payloadBuffer, 0, payloadLength);

            } else

              payloadLength = 0;

              //添加位置(prox)信息

              posConsumer.addPosition(position, payloadBuffer, 0, payloadLength);

          }

          posConsumer.finish();

        }

       (2-3-4) 判断退出条件,上次选中的域取得下一个文档号,如果没有,则说明此域包含此词的文档已经处理完毕,则从termStates中删除此域,并将numToMerge减一。然后此域取得下一个词,当循环到(2)的时候,表明此域已经开始处理下一个词。如果没有下一个词,说明此域中的所有的词都处理完毕,则从mergeStates中删除此域,并将numFields减一,当numFields为0的时候,循环(2)也就结束了。

        if (!minState.nextDoc()) {//获得下一个docid

          //如果此域包含此词的文档已经没有下一篇docid,则从数组termStates中移除,numToMerge减一。

          int upto = 0;

          for(int i=0;i

            if (termStates[i] != minState)

              termStates[upto++] = termStates[i];

          numToMerge--;

          //此域则取下一个词(term),在循环(2)处来参与下一个词的合并

          if (!minState.nextTerm()) {

            //如果此域没有下一个词了,则此域从数组mergeStates中移除,numFields减一。

            upto = 0;

            for(int i=0;i

              if (mergeStates[i] != minState)

                mergeStates[upto++] = mergeStates[i];

            numFields--;

          }

        }

      }

      (2-4) 经过上面的过程,docid和freq信息虽已经写入段文件,而跳表信息并没有写到文件中,而是写入skip buffer里面了,此处真正写入文件。并且词典(tii, tis)也应该写入文件。

      docConsumer(FormatPostingsDocsWriter).finish();

    }

    termsConsumer.finish();

  }

(2-3-4) 获得下一篇文档号代码如下:

 

public boolean nextDoc() {//如何获取下一个docid

  if (freq.eof()) {//如果bytepool中的freq信息已经读完

    if (p.lastDocCode != -1) {//由上述缓存管理,PostingList里面还存着最后一篇文档的文档号及词频信息,则将最后一篇文档返回

      docID = p.lastDocID;

      if (!field.omitTermFreqAndPositions)

        termFreq = p.docFreq;

      p.lastDocCode = -1;

      return true;

    } else

      return false;//没有下一篇文档

  }

  final int code = freq.readVInt();//如果bytepool中的freq信息尚未读完

  if (field.omitTermFreqAndPositions)

    docID += code;

  else {

    //读出文档号及词频信息。

    docID += code >>> 1;

    if ((code & 1) != 0)

      termFreq = 1;

    else

      termFreq = freq.readVInt();

  }

  return true;

}

(2-3-2) 添加文档号及词频信息代码如下:

 

FormatPostingsPositionsConsumer FormatPostingsDocsWriter.addDoc(int docID, int termDocFreq) {

    final int delta = docID - lastDocID;

    //当文档数量达到skipInterval倍数的时候,添加跳表项。

    if ((++df % skipInterval) == 0) {

      skipListWriter.setSkipData(lastDocID, storePayloads, posWriter.lastPayloadLength);

      skipListWriter.bufferSkip(df);

   }

   lastDocID = docID;

   if (omitTermFreqAndPositions)

     out.writeVInt(delta);

   else if (1 == termDocFreq)

     out.writeVInt((delta<<1) | 1);

   else {

     //写入文档号及词频信息。

     out.writeVInt(delta<<1);

     out.writeVInt(termDocFreq);

   }

   return posWriter;

}

(2-3-3) 添加位置信息:

 

FormatPostingsPositionsWriter.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength) {

    final int delta = position - lastPosition;

    lastPosition = position;

    if (storePayloads) {

        //保存位置及payload信息

        if (payloadLength != lastPayloadLength) {

            lastPayloadLength = payloadLength;

            out.writeVInt((delta<<1)|1);

            out.writeVInt(payloadLength);

        } else

            out.writeVInt(delta << 1);

            if (payloadLength > 0)

                out.writeBytes(payload, payloadLength);

    } else

        out.writeVInt(delta);

}

(2-4) 将跳表和词典(tii, tis)写入文件

FormatPostingsDocsWriter.finish() {

    //将跳表缓存写入文件

    long skipPointer = skipListWriter.writeSkip(out);

    if (df > 0) {

      //将词典(terminfo)写入tii,tis文件

      parent.termsOut(TermInfosWriter).add(fieldInfo.number, utf8.result, utf8.length, termInfo);

    }

  }

将跳表缓存写入文件:

DefaultSkipListWriter(MultiLevelSkipListWriter).writeSkip(IndexOutput)  {

    long skipPointer = output.getFilePointer();

    if (skipBuffer == null || skipBuffer.length == 0) return skipPointer;

    //正如我们在索引文件格式中分析的那样, 高层在前,低层在后,除最低层外,其他的层都有长度保存。

    for (int level = numberOfSkipLevels - 1; level > 0; level--) {

      long length = skipBuffer[level].getFilePointer();

      if (length > 0) {

        output.writeVLong(length);

        skipBuffer[level].writeTo(output);

      }

    }

    //写入最低层

    skipBuffer[0].writeTo(output);

    return skipPointer;

  }

将词典(terminfo)写入tii,tis文件:

  • tii文件是tis文件的类似跳表的东西,是在tis文件中每隔indexInterval个词提取出一个词放在tii文件中,以便很快的查找到词。
  • 因而TermInfosWriter类型中有一个成员变量other也是TermInfosWriter类型的,还有一个成员变量isIndex来表示此对象是用来写tii文件的还是用来写tis文件的。
  • 如果一个TermInfosWriter对象的isIndex=false则,它是用来写tis文件的,它的other指向的是用来写tii文件的TermInfosWriter对象
  • 如果一个TermInfosWriter对象的isIndex=true则,它是用来写tii文件的,它的other指向的是用来写tis文件的TermInfosWriter对象

TermInfosWriter.add (int fieldNumber, byte[] termBytes, int termBytesLength, TermInfo ti) {

    //如果词的总数是indexInterval的倍数,则应该写入tii文件

    if (!isIndex && size % indexInterval == 0)

      other.add(lastFieldNumber, lastTermBytes, lastTermBytesLength, lastTi);

    //将词写入tis文件

    writeTerm(fieldNumber, termBytes, termBytesLength);

    output.writeVInt(ti.docFreq);                       // write doc freq

    output.writeVLong(ti.freqPointer - lastTi.freqPointer); // write pointers

    output.writeVLong(ti.proxPointer - lastTi.proxPointer);

    if (ti.docFreq >= skipInterval) {

      output.writeVInt(ti.skipOffset);

    }

    if (isIndex) {

      output.writeVLong(other.output.getFilePointer() - lastIndexPointer);

      lastIndexPointer = other.output.getFilePointer(); // write pointer

    }

    lastFieldNumber = fieldNumber;

    lastTi.set(ti);

    size++;

  }

6.2.2.2.1.2、写入词向量信息

代码为:

TermVectorsTermsWriter.flush (Map> 
                                              threadsAndFields, final SegmentWriteState state) {

    if (tvx != null) {

      if (state.numDocsInStore > 0)

        fill(state.numDocsInStore - docWriter.getDocStoreOffset());

      tvx.flush();

      tvd.flush();

      tvf.flush();

    }

    for (Map.Entry> entry : 
                                                                                                                                      threadsAndFields.entrySet()) {

      for (final TermsHashConsumerPerField field : entry.getValue() ) {

        TermVectorsTermsWriterPerField perField = (TermVectorsTermsWriterPerField) field;

        perField.termsHashPerField.reset();

        perField.shrinkHash();

      }

      TermVectorsTermsWriterPerThread perThread = (TermVectorsTermsWriterPerThread) entry.getKey();

      perThread.termsHashPerThread.reset(true);

    }

  }

从代码中可以看出,是写入tvx, tvd, tvf三个文件,但是在上述的closeDocStore已经写入了,并且把tvx设为null,在这里其实什么也不做,仅仅是清空postingsHash,以便进行下一轮索引时重用此对象。

6.2.2.2.2、写入标准化因子

代码为:

NormsWriter.flush(Map> threadsAndFields,

                           SegmentWriteState state) {

    final Map> byField = new HashMap>();

    for (final Map.Entry> entry : 
                                                                                                                           threadsAndFields.entrySet()) {

     //遍历所有的域,将同名域对应的NormsWriterPerField放到同一个链表中。

      final Collection fields = entry.getValue();

      final Iterator fieldsIt = fields.iterator();

      while (fieldsIt.hasNext()) {

        final NormsWriterPerField perField = (NormsWriterPerField) fieldsIt.next();

        List l = byField.get(perField.fieldInfo);

        if (l == null) {

            l = new ArrayList();

            byField.put(perField.fieldInfo, l);

        }

        l.add(perField);

    }

    //记录写入的文件名,方便以后生成cfs文件。

    final String normsFileName = state.segmentName + "." + IndexFileNames.NORMS_EXTENSION;

    state.flushedFiles.add(normsFileName);

    IndexOutput normsOut = state.directory.createOutput(normsFileName);

    try {

      //写入nrm文件头

      normsOut.writeBytes(SegmentMerger.NORMS_HEADER, 0, SegmentMerger.NORMS_HEADER.length);

      final int numField = fieldInfos.size();

      int normCount = 0;

      //对每一个域进行处理

      for(int fieldNumber=0;fieldNumber

        final FieldInfo fieldInfo = fieldInfos.fieldInfo(fieldNumber);

        //得到同名域的链表

        List toMerge = byField.get(fieldInfo);

        int upto = 0;

        if (toMerge != null) {

          final int numFields = toMerge.size();

          normCount++;

          final NormsWriterPerField[] fields = new NormsWriterPerField[numFields];

          int[] uptos = new int[numFields];

          for(int j=0;j

            fields[j] = toMerge.get(j);

          int numLeft = numFields;

          //处理同名的多个域

          while(numLeft > 0) {

            //得到所有的同名域中最小的文档号

            int minLoc = 0;

            int minDocID = fields[0].docIDs[uptos[0]];

            for(int j=1;j

              final int docID = fields[j].docIDs[uptos[j]];

              if (docID < minDocID) {

                minDocID = docID;

                minLoc = j;

              }

            }

            // 在nrm文件中,每一个文件都有一个位置,没有设定的,放入默认值

            for (;upto<minDocID;upto++)

              normsOut.writeByte(defaultNorm);

            //写入当前的nrm值

            normsOut.writeByte(fields[minLoc].norms[uptos[minLoc]]);

            (uptos[minLoc])++;

            upto++;

            //如果当前域的文档已经处理完毕,则numLeft减一,归零时推出循环。

            if (uptos[minLoc] == fields[minLoc].upto) {

              fields[minLoc].reset();

              if (minLoc != numLeft-1) {

                fields[minLoc] = fields[numLeft-1];

                uptos[minLoc] = uptos[numLeft-1];

              }

              numLeft--;

            }

          }

          // 对所有的未设定nrm值的文档写入默认值。

          for(;upto

            normsOut.writeByte(defaultNorm);

        } else if (fieldInfo.isIndexed && !fieldInfo.omitNorms) {

          normCount++;

          // Fill entire field with default norm:

          for(;upto

            normsOut.writeByte(defaultNorm);

        }

      }

    } finally {

      normsOut.close();

    }

  }

6.2.2.3、写入域元数据

代码为:

FieldInfos.write(IndexOutput) {

    output.writeVInt(CURRENT_FORMAT);

    output.writeVInt(size());

    for (int i = 0; i < size(); i++) {

      FieldInfo fi = fieldInfo(i);

      byte bits = 0x0;

      if (fi.isIndexed) bits |= IS_INDEXED;

      if (fi.storeTermVector) bits |= STORE_TERMVECTOR;

      if (fi.storePositionWithTermVector) bits |= STORE_POSITIONS_WITH_TERMVECTOR;

      if (fi.storeOffsetWithTermVector) bits |= STORE_OFFSET_WITH_TERMVECTOR;

      if (fi.omitNorms) bits |= OMIT_NORMS;

      if (fi.storePayloads) bits |= STORE_PAYLOADS;

      if (fi.omitTermFreqAndPositions) bits |= OMIT_TERM_FREQ_AND_POSITIONS;

      output.writeString(fi.name);

      output.writeByte(bits);

    }

}

此处基本就是按照fnm文件的格式写入的。

6.3、生成新的段信息对象

代码:

newSegment = new SegmentInfo(segment, flushedDocCount, directory, false, true, docStoreOffset, docStoreSegment, docStoreIsCompoundFile, docWriter.hasProx());

segmentInfos.add(newSegment);

 

6.4、准备删除文档

代码:

docWriter.pushDeletes();

    --> deletesFlushed.update(deletesInRAM);

此处将deletesInRAM全部加到deletesFlushed中,并把deletesInRAM清空。原因上面已经阐明。

6.5、生成cfs段

代码:

docWriter.createCompoundFile(segment);

newSegment.setUseCompoundFile(true);

代码为:

DocumentsWriter.createCompoundFile(String segment) {

    CompoundFileWriter cfsWriter = new CompoundFileWriter(directory, segment + "." + IndexFileNames.COMPOUND_FILE_EXTENSION);

    //将上述中记录的文档名全部加入cfs段的写对象。

    for (final String flushedFile : flushState.flushedFiles)

      cfsWriter.addFile(flushedFile);

    cfsWriter.close();

  }

6.6、删除文档

代码:

applyDeletes();

代码为:

boolean applyDeletes(SegmentInfos infos) {

  if (!hasDeletes())

    return false;

  final int infosEnd = infos.size();

  int docStart = 0;

  boolean any = false;

  for (int i = 0; i < infosEnd; i++) {

    assert infos.info(i).dir == directory;

    SegmentReader reader = writer.readerPool.get(infos.info(i), false);

    try {

      any |= applyDeletes(reader, docStart);

      docStart += reader.maxDoc();

    } finally {

      writer.readerPool.release(reader);

    }

  }

  deletesFlushed.clear();

  return any;

}

  • Lucene删除文档可以用reader,也可以用writer,但是归根结底还是用reader来删除的。
  • reader的删除有以下三种方式:
    • 按照词删除,删除所有包含此词的文档。
    • 按照文档号删除。
    • 按照查询对象删除,删除所有满足此查询的文档。
  • 但是这三种方式归根结底还是按照文档号删除,也就是写.del文件的过程。

 

private final synchronized boolean applyDeletes(IndexReader reader, int docIDStart)

  throws CorruptIndexException, IOException {

  final int docEnd = docIDStart + reader.maxDoc();

  boolean any = false;

  //按照词删除,删除所有包含此词的文档。

  TermDocs docs = reader.termDocs();

  try {

    for (Entry entry: deletesFlushed.terms.entrySet()) {

      Term term = entry.getKey();

      docs.seek(term);

      int limit = entry.getValue().getNum();

      while (docs.next()) {

        int docID = docs.doc();

        if (docIDStart+docID >= limit)

          break;

        reader.deleteDocument(docID);

        any = true;

      }

    }

  } finally {

    docs.close();

  }

  //按照文档号删除。

  for (Integer docIdInt : deletesFlushed.docIDs) {

    int docID = docIdInt.intValue();

    if (docID >= docIDStart && docID < docEnd) {

      reader.deleteDocument(docID-docIDStart);

      any = true;

    }

  }

  //按照查询对象删除,删除所有满足此查询的文档。

  IndexSearcher searcher = new IndexSearcher(reader);

  for (Entry entry : deletesFlushed.queries.entrySet()) {

    Query query = entry.getKey();

    int limit = entry.getValue().intValue();

    Weight weight = query.weight(searcher);

    Scorer scorer = weight.scorer(reader, true, false);

    if (scorer != null) {

      while(true)  {

        int doc = scorer.nextDoc();

        if (((long) docIDStart) + doc >= limit)

          break;

        reader.deleteDocument(doc);

        any = true;

      }

    }

  }

  searcher.close();

  return any;

}


更多0

posted @ 2014-06-25 14:18  eagleGeek  阅读(448)  评论(0编辑  收藏  举报