系列文章:

构造 IndexWriter 对象(三)

构造 IndexWriter 对象(二)

构造 IndexWriter 对象(一)

本文承接 构造 IndexWriter 对象(三),继续介绍调用 IndexWriter 的构造函数的流程。

调用 IndexWriter 的构造函数的流程图

图 1:

根据不同的 OpenMode 执行对应的工作

  在上一篇文章中,我们介绍了执行 CREATE 模式下的工作,故继续介绍执行 APPEND 模式下的工作。

执行 APPEND 模式下的工作的流程图

图 2:

StandardDirectoryReader 是否为空?

图 3:

  在图 1 的流程点 获取IndexCommit对应的StandardDirectoryReader,如果用户通过 IndexWriterConfig.setIndexCommit(IndexCommit commit) 设置了 IndexCommit,那么 Lucene 会尝试根据该 IndexCommit 获得一个 StandardDirectoryReader,它描述了 IndexCommit 中包含的索引信息(主要是 SegmentInfos 对象的信息,见 构造 IndexWriter 对象(三) 关于 SegmentInfos 对象的介绍)。

  StandardDirectoryReader 为空的情况有两种:

  • 用户没有设置 IndexCommit
  • 用户设置了 IndexCommit,但是 IndexCommit 中没有 StandardDirectoryReader 对象的信息

为什么 IndexCommit 中可能会没有 StandardDirectoryReader 对象的信息

  IndexCommit 类其实是一个抽象类,它的类图关系如下所示:

图 4:

ReaderCommit、CommitPoint

  ReaderCommit 类跟 CommitPoint 类中包含的内容如下图所示

图 5:

  故如果是 ReaderCommit 对象,那么就可以获得 StandardDirectoryReader 对象,而 ReaderCommit 对象则是通过 StandardDirectoryReader.getIndexCommit()方法获得,由于该方法的实现很简单,故直接给出:

图 6:

  图 6 为 StandardDirectoryReader 类中的方法,可以看出,StandardDirectoryReader 对象通过调用 getIndexCommit()方法,构造了一个新的 ReaderCommit 对象,并且将自己(this 指针)作为 ReaderCommit 的成员变量之一,即图 5 中红框标注。

  尽管无法通过 CommitPoint 对象中获得 StandardDirectoryReader 对象,但这里仍然要说下 CommitPoint 对象是什么生成的。

  CommitPoint 对象生成点有两处:

  • 生成 IndexFileDeleter 对象期间:这个时机点即图 1 中的流程点 生成对IndexFileDeleter,故我们这里先暂时不展开
  • 执行 commit()操作期间:图 7 是执行 IndexWriter.commit()的两阶段提交的流程图,并且红框标注的流程点为 CommitPoint 对象生成的时机,该流程点的具体介绍可以看 文档提交之 commit(二)

图 7:

SnapshotCommitPoint

  SnapshotCommitPoint 实际是采用装饰者模式来实现额外的功能,该类中的成员变量很少,故直接给出:

图 8:

  根据图 8 可以看出,它封装了另一个 IndexCommit 对象(见图 7 的红色标注的流程在文章 文档提交之 commit(二) 中的介绍),实际上该对象就是 CommitPoint。

  至于它实现了哪些额外的功能,在后面的流程中会展开介绍,这里我们只需要知道它同 CommitPoint 一样,无法提供 StandardDirectoryReader 对象。

  由于 SnapshotCommitPoint 封装了 CommitPoint 对象,所以它的生成时机同样在图 7 中的红色标注流程点。

配置检查 2

图 9:

  我们先介绍下能获得 StandardDirectoryReader 对象的情况,需要对用户的配置信息进行三项检查,如下所示

图 10:

  图 10 中,reader 即 StandardDirectoryReader 对象、commit 即 IndexCommit 对象、这里没什么好介绍的。

用 StandardDirectoryReader 初始化一个新的 SegmentInfos 对象

图 11:

  在文章 构造 IndexWriter 对象(三) 中我们介绍 执行CREATE模式下的工作 时说到,该模式下初始化一个新的 SegmentInfos 对象时,它不包含任何的索引信息,而在 APPEND 模式下,则是用 StandardDirectoryReader 中的索引信息来初始化一个新的 SegmentInfos 对象,即所谓的"追加"。

获得回滚(rollback)信息

图 12:

  上文中根据 IndexCommit 获得的 StandardDirectoryReader,它包含的 SegmentInfos 在后面的流程中将会作为回滚内容,而在这个流程中,最重要的一步是检查 SegmentInfos 中包含的索引信息对应的索引文件是否还在索引目录中。

为什么要执行这个检查

  我们首先根据下面两张图来介绍下 SegmentInfos 对象跟 索引文件 segments_N索引文件。si 以及其他 索引文件 的关系:

图 14:

  图 14 为索引目录中的索引文件,并且存在两个段。

图 15:

  SegmentInfos 对象是索引文件 segments_N 和索引文件。si 在内存中的表示,图 14 中的提交中包含了两个段,即以_0 跟_1 开头的两个段,所以索引文件 segments_1 中有两个 SegmentCommitInfo 字段,接着根据 SegmentCommitInfo 中的 SegName 字段,该字段的值描述的是该段对应的所有索引文件的前缀值(见文章 索引文件。si 中的介绍),即_0,那么就可以在索引目录中找到 索引文件_0.si,而在 索引文件_0.si 的 Files 字段(图 15 中红框标注)中存储了其他索引文件的名字,同样地根据这些索引文件的名字在索引目录中读取到所有的索引信息。

  另外图 15 中 SegmentCommitInfo 中的两个字段 FieldInfosFies、UpdatesFiles 也是存储了索引文件的名字,当一个段中的 DocValues 发生变更时,变更的信息也用索引文件描述,并且索引文件的名字存储在这两个字段里。

  从上文的描述可以看出,尽管我们通过 IndexCommit 可以获得 SegmentInfos 信息,但是该对象只是描述了它对应的索引文件有哪些,并不具有这些索引文件真正的数据,故可能在获得 IndexCommit 之后,索引又发生了变化,例如又出现了新的提交,那么根据 默认的索引删除策略(见 文档提交之 commit(二) 中关于 执行检查点(checkPoint)工作 d 的介绍),segments_1 文件就会被删除,当执行回滚操作时就无法获得真正的索引数据。如果出现在这个情况,那么在当前流程点会抛出如下的异常:

      throw new IllegalArgumentException("the provided reader is stale: its prior commit file \"" + segmentInfos.getSegmentsFileName() + "\" is missing from index");  其中 segmentInfos.getSegmentsFileName()指的就是 segment_1 文件。

  感兴趣的同学可以执行这个 demo: https://github.com/LuXugang/Lucene-7.5.0/blob/master/LuceneDemo/src/main/java/lucene/index/StaleIndexCommit.java ,该 demo 模拟了上述异常的抛出。

同步 SegmentInfos 以及回滚信息中 SegmentInfos 中的部分信息

图 16:

为什么 StandardDirectoryReader 中可能没有 IndexWriter 对象

  在 近实时搜索 NRT 系列文章中我们说到可以通过下面四种 open()方法获得一个 StandardDirectoryReader,其中:

  • 方法一:DirectoryReader.open(final Directory directory)
  • 方法二:DirectoryReader.open(final IndexCommit indexCommit)
  • 方法三:DirectoryReader.open(final IndexWriter indexWriter)
  • 方法四:DirectoryReader.open(final IndexWriter indexWriter, boolean applyAllDeletes, boolean writeAllDeletes)

  其中通过方法一根方法二获得的 StandardDirectoryReader 对象中是没有 IndexWriter 对象的,即使方法二的参数 indexCommit 对象中有 IndexWriter 对象。

为什么持有(引用)IndexWriter 对象的 StandardDirectoryReader 需要执行图 16 中的两个同步操作

  源码中是这么说的:

// In case the old writer wrote further segments (which we are now dropping)  其中 old Writer 指的就是 StandardDirectoryReader 中的 IndexWriter 对象,上述注释的意思是为了能处理 old writer 可能生成的新提交(一个或多个),并且该提交是需要丢弃的。

  该注释的详细意思就是:我们使用的 IndexCommit 参数对应的索引信息可能不是 old writer 最新的提交对应的索引信息,那么比 IndexCommit 更加新的的提交(一个或多个)都应该丢弃(dropping),为了能正确的处理那些应该被丢弃的段,我们需要上面图 16 中的两个更新操作。

为什么执行图 16 的两个更新操作就能正确的处理那些应该被丢弃的段:

  处理的时机点在后面的流程中,到时候再介绍。

  另外图 16 中两个同步的内容即 version、counter、generation,在 构造 IndexWriter 对象(三) 文章中介绍了这三个值,这里不赘述。

设置回滚

  该流程跟 构造 IndexWriter 对象(三) 文章中的中的 设置回滚 是相同的意思,将流程点 获得回滚(rollback)信息 的信息更新到 rollbackSegments。