索引文件的读取(十三)(Lucene 8.4.0)

  本文承接文章索引文件的读取(十二)之doc&&pos&&pay,继续介绍剩余的内容。索引文件.doc、.pos、.pay的读取过程相比索引文件.tim&&.tip较为简单,核心部分为如何通过读取这三个索引文件,生成一个PostingsEnum对象,该对象中描述了term在一篇文档中的词频frequency、位置position、在文档中的偏移offset负载payload以及该文档的文档号docId,其中docId和frequency通过索引文件.doc获得、position通过索引文件.pos获得、offset和payload通过索引文件.pay获得。

PostingsEnum

  PostingsEnum是一个抽象类,其子类的实现有很多,本文中仅仅介绍在Lucene84PostingsReader类中的子类,如下所示:

图1:

  图1中共有5个子类实现,用红框标注,在搜索阶段,通过下面两个条件来选择其中一种实现:

Flag

  Flag描述了在搜索阶段中,我们需要获取term在文档中的哪些信息。这里的信息即上文中提到的frequency、position、offset以及payload。由于不是所有的查询都需要所有的这些信息,选择性(optional)的获取这些信息能降低搜索阶段的内存开销,同时减少读取索引文件时产生的磁盘I/O,下文中会详细介绍。

  Flag的可选值如下所示:

图2:

打分模式ScoreMode

  ScoreMode描述的是搜索模式,正如源码中的注释:

图3:

  本文中我们不展开ScoreMode的详细介绍,我们仅仅看下图3中红框标注的TOP_SCORES,该值影响了上文中PostingsEnum子类的选择,该注释源码大意为:在搜索阶段,不会匹配所有满足查询条件的文档,会跳过那些不具竞争力的文档。其原理就是利用了索引文件.doc中的Impacts字段实现的,这里简单提下,在以后介绍WAND(weak and)算法时候再详细介绍。

选择PostingsEnum的实现类

图4:

  图4的流程图描述了如何根据Flag跟打分模式选择PostingsEnum的实现类。是否有跳表信息的判断依据为:如果包含term的文档数量小于128,即一个block的大小,那么在生成索引文件.doc阶段就不会有跳表信息(不懂?见文章索引文件的生成(三)之跳表SkipList);在读取阶段由于是先读取索引文件.tim,故通过该索引文件中的DocFreq字段来获取包含term的文档数量,如下图红框标注的字段:

图5:

  图5索引文件.tim的字段介绍以及生成过程可以分别阅读文章索引文件tim&&tip索引文件的读取(七)之tim&&tip

PostingsEnum中的信息

  我们先直接列出每个实现类中包含的数据,随后介绍获取这些信息的方式。

  无论哪一种PostingsEnum的实现类,在读取过程中,每次总是从索引文件.doc、pos、pay只读取一个block的信息,并把block中的信息写入到多个数组中(下文会介绍这些数组),这些数组用来描述上文中docId集合、frequency集合、position集合、offset集合、payload集合、impactData信息。

如何读取一个block中的信息

  为了便于描述,我们不考虑没有生成跳表以及不满128条信息的block(见文章索引文件之doc中VIntBlocks的介绍)的读取,只介绍生成跳表后的读取方式。

  在读取了跳表SkipList之后(读取过程见文章索引文件的生成(四)之跳表SkipList),我们就获得了一个SkipDatum的信息。

  这里需要简单说明下,在生成跳表SkipList的过程中,在第0层中每当处理skipInterval(默认值为128)篇文档就生成一个SkipDatum,另外每生成skipMultiplier(默认值为8)个SkipDatum就在上一层,即第1层,生成一个SkipDatum。注意的是第1层的该SkipDatum中包含的指针信息是指向第0层中最后一个SKipDatum的结束读取位置,同时意味着指针信息指向了第0层的最后一个SkipDatum的下一个待写入的SkipDatum的起始读取位置,如果不了解这段描述,请阅读文章索引文件的生成(三)之跳表SkipList

  那么此时问题来了,在读取阶段,最高层的第一个跳表是如何读取的;term的第一个docId信息、frequency信息、position信息、offset信息、payload信息是如何获得的呢

图6:

  在SkipDatum字段包含的信息中,DocFPSkip描述了存储docId跟frequency的block在索引文件.doc中的起始读取位置,PosFPSkip描述了存储position的block在索引文件.pos中的起始读取位置,PosBlockOffset描述了block中的块内偏移(不明白的话,请阅读文章索引文件的生成(一)之doc&&pay&&pos索引文件的生成(二)之doc&&pay&&pos)剩余的字段同理,下文中会进一步介绍,最终读取后的信息写入到上文提到的各种集合中:

图7:

点击查看大图

docId集合、frequency集合

  在源码中,使用docBuffer[ ]、freqBuffer[ ]两个数组来描述在内存中一个block中的的docId、frequency集合信息,从下面的定义也可以看出这两个数组都只存储一个block大小的信息:

图8:

  通过读取索引文件.doc的TermFreqs字段中的PackedDocDeltaBlock跟PackedFreqBlock字段,就可以获得docId集合跟frequency集合,并将这两个字段的信息分别写入到docBuffer[ ]、freqBuffer[ ]中:

图9:

  这里要特别说明的是,在读取block时,总是会将PackedDocDeltaBlock的信息,即文档号信息,写入到docBuffer[ ]中,而PackeddFreqBlock的信息,即frequency词频信息,则是采用read lazily,它描述的是在索引阶段存储了文档的frequency信息(基于IndexOptions选项),但是在搜索阶段,某种查询并不需要frequency信息,那么只有在需要frequency信息时候才读取PackedFreqBlock,并且写入到freqBuffer[ ]中。

position集合、offset集合、payload集合

  在源码中,使用posDeltaBuffer[ ]描述position信息;使用offsetStartDeltaBuffer[ ]、offsetLengthBuffer[ ]描述offset信息;使用payloadLengthBuffer[ ]、payloadBytes[ ]描述payload信息:

图10:

  对于这三个信息的读取相比较读取文档号跟词频稍微复杂,原因在于term在一篇文档中的这三个信息可能分布在一个或多个block(图7中的PackedPosBlock、packedPayBlock)中、甚至有可能在同一个PackedPosBlock中,保存term在两篇文档或更多篇文档的position信息,并且这些文档的属于不同的SkipDatum管理。例如term在一篇文档中的词频为386次,由于每128个位置信息就生成一个PackedPosBlock,故需要3个block存储。

  对于上述的情况,我们以position为例,通过SkipDatum中的PosFPSkip先从索引文件.pos中找到block,即PackedPosBlock,的起始读取位置,然后将PosBlockOffset作为块内偏移找到在block中的起始读取位置即可。

  由于position信息、offset信息、payload信息的数量总是保持一致的,即term在文档中的某个位置,必定对应有一个offset以及payload(可以为空),所以存储这些信息的posDeltaBuffer[ ]、offsetStartDeltaBuffer[ ]、offsetLengthBuffer[ ]、payloadLengthBuffer[ ]这四个数组的数组下标是保持一致的,注意的是payloadLengthBuffer[ ]描述的是在某个位置的term的payload长度length,根据这个长度去payloadBytes[ ]读取payload数据:

图11:

impactData信息

  最后剩余读取impactData信息就相对简单了,在源码中,用二维数组impactData[ ] [ ]、impactDataLength[ ]来描述所有层的Impacts信息(Impacts的概念见文章索引文件的读取(十二)之doc&&pos&&pay)、图6中,先读取ImpactLength字段,确定Impacts字段的读取区间,然后将Impacts字段的信息写入到这两个数组中,这两个数组跟存储payload信息的两个数组用法一致,不赘述。

结语

  关于索引文件.doc、.pos、.pay的一些基本的读取逻辑就暂时介绍到这里,在后面的文章介绍WAND(weak and)算法时,我们再进一步展开几个方法,例如advance( )、nextDoc( )、slowAdvance( )、advanceShallow( )等等,这些方法更细节的实现了读取索引文件.doc、.pos、.pay的过程。

点击下载附件