Lucene (v4.5) 深入学习 (三) —— 文件结构 续
补充了.tim,.tip部分的内容。虽然还是不怎么样,但比之前好多了。
关于.tim,.tip,.doc,.pos,.pay的一些前置说明
关于Packed Blocks and VInt Blocks
在packed block中,整数使用同样长度的width编码(详情见Class PackedInts):block的大小(即block中整数的数目)固定为128。此外,同样大小的块可以优化编码。
在VInt blocks中,整数用VInt格式编码,块的大小是多样的。
关于Block structure
当Posting(这里应该是指写入内存的数据)足够长,Lucene41PostingsFormat会尝试将最多的整数编码为packed block。以一个出现在259个文档中的term为例,前面的256个文档被编码为两个packed blocks,剩下的三个则被编码为一个VInt block。不同类型的数据总是分别编码为不同的packed block,但有可能被交错的写入同样的VInt block。这一策略被用于下列变量:, <position, payload="" length="">, <position, offset="" start,="" length="">, and<position, payload="" length,="" offsetstart,="" offset="" length="">
关于Skipdata settings
跳跃表的结构以前版本的Lucene。跳跃表的间隔和block的大小是一样的,并且每个跳跃如果指向每个block的开始。当然,无论如何,对于第一个block,跳跃表的数据是忽略的。
关于Positions, Payloads, and Offsets
位置信息是一个整数,用于说明一个term在一篇文档中出现的位置。Payload是一段与当前位置信息相关的元数据信息。offsets是一对整数,用于记录term在当前位置的起始偏移量,特定的payload需要这个值。
当pyaloads和offsets没有被忽略,有numPositions==numPayloads==numOffsets(假设一个空值的payload也计数一次)。正如上文提到的,这三者可能被一同编码,也可能分开编码。
在左右情况下,payloads和offsets被一同储存。当它们被编码为packed block,位置信息被分离为.pos文件,而payloads和offsets被写入.pay(payload的元数据信息也会直接储存在.pay中)。当被编码入VInt blocks时,三者交错地写入.pos(也包括payload的元数据)。
通过这一策略,大部分payload和offset数据会存储于.pos文件之外。所以当请求仅需要位置信息的时候,相比于一个包括payloads和offsets信息的完整索引,这种策略会减少硬盘的预取操作。
.tim
.tim记录了由域(field)到term的信息。
.tim文件记录了每个域中的term的统计信息(如docfreq)所形成的列表,还包括了指向频率,位置,Payload,跳跃表信息的指针,这些信息在.doc,.pos,.pay文件中。
具体格式参考类lockTreeTermsWriter。
.tim在blocks中排列。Blocks包括不同数量(默认为25到48个)的入口,每个入口对应一个term或者指向一个次级block。
.tim结构见下图:
Term Metadata和Postings Metadata补充在下:
- DirOffset指向FieldSummary部分的指针。
- DocFreq表示包含这个term的文档的总数。
- TotalTermFreq表示这个term总共出现的次数,用和DocFreq的差值记录(?)。
- FieldNumber记录了域的数量,来自于.fnm文件。
- NumTerms记录了域中不同的term的数量。
- RootCode指向了作为域的根的块(root block for the field)。
- SumDocFreq是整个域中term-document对(就是posting?)的数量。
- DocCount那些包括这个域并且域中至少有一对term-document的文档的数量。
- PostingsMetadata和TermMetadata使用特殊的方式存储:它们包含任意的每个文件的数据(如参数或版本信息)和每个term的信息(如指向倒排索引的指针)。
- PackedBlockSize是packed blocks的固定的块大小。在packed block中,位宽(?)由最大的整数确定。较小的块大小导致较少变化的整数宽度以及较小的索引。较大的块大小导致更加有效的大量i/o,也因此有更好的加速。这个值应当总是64的倍数,作为折中当前固定为128。这同时也作为skip interval (跳跃间隔)用于加速DocIdSetIterator.advance(int)。
- DocFPDelta确定了term的TermFreqs数据的在.doc文件中的位置。具体来说,它等于一个term的数据与前一个term的数据的文件位置偏移量的差值(如果等于0,则说明是block内的第一个term)。在硬盘上,它储存了与前面值的差值。
- PosFPDelta确定了term的TermPositions数据的在.pos文件中的位置。同时PayFPDelta定义了term的<termpayloads, termoffsets?=""> 数据在.pay文件中的位置。 类似于DocFPDelta ,它也用两个文件的差值编码(如果这个值被忽略了,那么说明当前域不记录payload和offsets数据)。
- PosVIntBlockFPDelta确定了这个term最后的TermPosition数据在.pos文件中最后的block中的位置(这里用“剩下的”应该更准确。应该就是除以block的大小后的余数)。它和PayVIntBlockFPDelta或OffsetVIntBlockFPDelta是同义词(数值是一样的)。它事实上用于表明是否需要从.pos文件代替.pay文件载入接下来的payloads和offsets数据。每一次新的存储位置数据的block被载入时, PostingsReader 会用这个值去检查这个block的格式是packed format或者VInt。如果是Packed format,payloadsh呃offsets数据会从.pay搜索,否则则从.pos。(当位置的总数也就是totalTermFreq小于或等于PackedBlockSize时这个值被忽略。)
- SkipFPDelta确定了term的SkipData 在.doc文件中的位置。具体来说,它的值等于TermFreq 的长度。只有在DocFreq 不小于SkipMinimum (当前版本中是128)时SkipDelta 才会存储下来。
- SingletonDocID用于优化当一个term仅出现在一篇文档中的情况。在这种情况下,这个唯一的文档号会写入term字典,以取代写入一个指向.doc文件的指针(DocFPDelta)以及在那个位置上的一个VInt Block。
.tip
.tip 文件对每个域储存了分离的FST。FSP映射了一个term的前缀到一个位于硬盘上的block,这个block包括所有相同前缀的term。每个域的IndexStartFP 指向了他自己的FST。
其中,类FST的简单说明如下:
public final class FST extends Object
表示一种有限状态机,使用压缩的字节数组格式。
(前缀有了,但后缀是怎么来的?在一个块里只有一个SuffixLength,有一个长度为SuffixLength的字节数组。而每个block里有entrycount个term(或次级block),应该有entrycount个后缀才是。看了下源码——暂时还没通过调试一步步看——的确应该有多个SuffixLength和对应的字节数组才是。官方文档失误了。)
下面是部分代码截图:
.tip结构见下图
- DirOffset是所有域用来指向IndexStartFPs的开始的指针。
- 在硬盘上的block可能存储过多的term(超过所允许的上限:48)。当这一情况发生时,block会分裂为新的多个block(被称为“floor blocks”),之后为每个分裂出的block的前缀编码为leading byte(应该就是位于block头的字节,大概)并输出到FST,同时还要生成文件的指针。
.doc
- PackedDocDeltaBlock理论上通过两步创建:
- 计算每一篇文档的编号和其前一篇文档的编号之间的差,获得一个差值(d-gap)列表(对于第一篇文档,直接使用它的编号。);
- 对于这些d-gap,从第一个到第PackedDocBlockNum*PackedBlockSizeth个会被编码为packed blocks。
- 如果频率没有被忽略,生成PackedFreqBlock时将不考虑d-gap。
- VIntBlock存储了剩余的d-gaps(可能包括频率?)。编码为DocDelta和Freq的格式:
- 如果频率是被索引的(建立索引时记录频率信息?),DocDelta确定了文档编号和频率。具体方法见或然跟随规则。如果频率未记录,那么DocDelta就是文档编号的差值,且不需要除2。
- PackedDocBlockNum是存储了当前term的文档编号或频率的packed blocks的数目。具体有PackedDocBlockNum = floor(DocFreq/PackedBlockSize)
- TrimmedDocFreq = DocFreq % PackedBlockSize == 0 ? DocFreq – 1 : DocFreq。自从skip entry (跳跃表的入口)的定义与基本结构有了一些差别之后,我们使用这一策略(使用TrimmedDocFreq)。在MultiLevelSkipListWriter 中,跳跃表的数据被假定按照skipIntervalth, 2*skipIntervalth ……的方式存储在列表中。无论如何,在Lucene41PostingsFormat 中,跳跃表的数据按照skipInterval+1th, 2*skipInterval+1th ……的存储(在这个例子中skipInterval==PackedBlockSize)。当DocFreq 是PackedBlockSize 的倍数,相比于Lucene41SkipWriter ,MultiLevelSkipListWriter 会多要求一个跳跃表数据(包?block)。
- SkipDatum是一个跳跃表入口的元数据,对于第一个block,它是被忽略的。
- DocSkip记录了每个packed block 中最后的文档号。在硬盘上它存储为与前一个文档号的差值。
- DocFPSkip记录了在DocFile中每一个按照PackedBlockSize+1th, 2*PackedBlockSize+1th 存储的文件偏移量(?)(最终作用相当于指针?)。文件偏移量和当前term的TermFreqs的起始相关联。在硬盘上它同样储存了与前一个SkipDatum的差值。
- 由于位置和payloads也是块编码,在跳跃表(搜索过程)中应该先跳到相关的块,然后再根据块内的偏移量获取值。 分别位于.posh呃.pay的PosFPSkip和PayFPSkip记录了文件对应的块。而PosBlockOffset记录了文件在块中的偏移量(PayBlockOffset是不必要的,因为它总是等于PosBlockOffset)。同DocFPSkip,文件偏移量是相对于当前Term的TermFreqs的开始,并作为差分序列(a difference sequence以后就翻译成这个了)存储。
- PayByteUpto记录了当前payload的起始偏移量。它等于从当前块到PosBlockOffset的payload的长度的和。
.pos
- TermPositions根据term排序(term来自于term字典,是隐含的),位置值对每一个term文档对是递增的,并且根据文档号排列。
- PackedPosBlockNum是记录当前term的位置值,payload或者偏移量的packed block的数量。 具体计算方法为PackedPosBlockNum = floor(totalTermFreq/PackedBlockSize)。
- PosVIntCount是被编码为VInt的位置值的数量。具体计算方法为: PosVIntCount = totalTermFreq – PackedPosBlockNum*PackedBlockSize。
- PackedPosDeltaBlock的计算方法同PackedDocDeltaBlock(见上一节)。
- PositionDelta(在term的域中没有定义payloads时)是term出现在在文档中的两次临近的位置的差值(当term第一次出现在文档中时,值为0)。如果term的域中存在payload,那么PositionDelta/2的值是term出现在在文档中的两次临近的位置的差值。如果Payloads存在,兵器PositionDelta/2的值是奇数,PayloadLength 会被储存,用于说明当前term的位置数据中payload的长度。
举例来说,(当不存在payloads的时候),一个term出现于一片文档的第四个位置,,并且出现于下一篇文档的第5个和第九个位置,那么它的TremPositions应当为接下来的VInts串:4,5,4。 - PayloadData是当前term位置的相关元数据。如果PayloadLength是存在的,那么它将指明这个payload的长度。如果payloadLength不存在,那么这个payload的长度和前一个Position数据中的payload的长度一致。
- OffsetDelta/2是当前位置的startOffset和前一个position的startOffset的差值(如果是0则它是第一次出现于文档中)。如果OffsetDelta是奇数,那么term本身偏移量的差值(endOffset-startOffset)和前一次出现的term不同,并且长度值跟在后面。偏移量数据仅通过FieldInfo.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS写入。
.pay
- TermPayloads/TermOffsets 的顺序和TermPositions 是一样的。需要注意的是,payload和offsets的值储存在.pos中。
- 生成PackedPayLengthBlock 和PackedOffsetLengthBlock 的方法同PackedFreqBlock 是一样的。具体见上上一节。同时,PackedStartDeltaBlock 和PackedDocDeltaBlock 的生成过程也是一样的。
- 对于同一个term,PackedPayBlockNum总是等于PackedPosBlockNum。PackedPayBlockNum也是PackedOffsetBlockNum的同义词(值一样的意思)。
- SumPayLength 是写入一个block的payload的总长,应当是一个block中所有PayLengths的和。
- 在PackedPayLengthBlock 中PayLength是每个跟随当前position的payload的长度。
作者:明翼(XGogo)
-------------
公众号:TSparks
微信:shinelife
扫描关注我的微信公众号感谢
-------------