在前几篇索引文件的读取的系列文章中,我们介绍索引文件tim&&tip的读取时机点时说到,在生成StandardDirectoryReader对象期间,会生成SegmentReader对象,该对象中的StoredFieldsReader信息描述了索引文件fdx&&fdt&&fdm中所有域的索引信息,故我们从本篇文章开始介绍索引文件fdx&&fdt&&fdm的读取。
StoredFieldsReader
在生成StandardDirectoryReader阶段,就已经开始读取索引文件fdx&&fdt&&fdm实现一些初始化,为随后的搜索阶段做准备,读取后的信息用StoredFieldsReader来描述。
.fdm
图1:
.fdm文件中存储了元数据,即描述存储域数据的信息,它包含的所有数据将被完整的读入到内存中。
.fdx
图2:
通过索引文件.fdm中的StartPointsIndex与NumDocsIndex的差值来确定在索引文件.fdx中的一段数据区间,该区间中存储了chunk中的文档数量,同理SPEndPointer与StartPointsIndex的差值确定在索引文件.fdx中的一段数据区间,该区间中存储了每个chunk在索引文件.fdt中的起始读取位置。
注意的是,在生成StandardDirectoryReader阶段,图2中NumDoc跟StartPoints的字段的值并没有读取到内存中,只是将这两块数据的起始读取位置读取到了内存,相比较在Lucene 8.5.0之前的版本,这种读取方式即所谓的"off-heap"。
off-heap
在Lucene 8.5.0之前,描述存储域的索引文件为.fdx、.fdt,它们的数据结构完整介绍可以见文章索引文件之fdx&&fdt,本文中中我们暂时只给出索引文件.fdx来介绍"off-heap":
图3:
在Lucene 8.5.0之前,图3中所有的Block会在生成StandardDirectoryReader阶段就全部读取到内存中,注意的是图3中的DocBases对应图2中NumDocs,命名不同而已,描述的内容是一致。
此次优化的详细内容可以见这个issue的介绍:https://issues.apache.org/jira/browse/LUCENE-9147 。在文章索引文件的读取(七)之tim&&tip中我们提到,索引文件.tip的读取也是用了off-heap,并且在Lucene 8.0.0就早早实现了,为什么在Lucene 8.5.0之后才将存储域的索引文件的读取使用off-heap呢?原因有两点,直接贴出issue原文:
图4:
图4的大意就是,在terms index(索引文件.tip)使用了off-heap之后,存储域(stored fields)的索引文件变成了占用内存的大头,但是它没有terms index那样对性能有很大的影响(见文章索引文件的读取(七)之tim&&tip)。
图5:
图5中的更有意思,Erick Erickson说当在技术层面(technical aspects)无法创新时,那么从内存方面去考虑优化了。
.fdt
图6:
对于索引文件.fdt,在此阶段通过索引文件.fdm的maxPointer,读取出ChunkCount、DIrtyChunkCount以及ChunkSize、PackedIntsVersion字段而已,也就是说Chunk字段,占用内存最大的数据块,没有被读取到内存中,在搜索阶段才根据条件读取,下文中中会展开介绍。
读取索引文件fdx&&fdt&&fdm的流程图
图7:
准备数据
图8:
准备数据是全局的文档号。该文档号就是满足搜索条件的文档对应的文档号,例如下图中,ScoreDoc[ ]对象中存放的就是满足查询条件的全局的文档号。
图9:
计算出所属段并转化为段内文档号
图10:
在生成StandardDirectoryReader对象期间,通过获取每个段中的文档数量,会初始化一个int类型的starts[ ]数组,随后根据这个数据就可以计算出某个全局文档号属于哪一个段。通过读取索引文件.si中的segSize字段来获取每个段中的文档数量,如下所示:
图11:
我们直接以一个例子来介绍starts[ ]数组:
图12:
图12的例子,每当count达到1000、3000、20000、100000、结束时就生成一个段,即索引目录中存在5个段。那么对应的starts[]数组如下所示:
图13:
那么根据starts[ ]数组,通过二分法就能计算出某个全局文档号所属的段了。
另外starts[ ]数组中的元素还代表了某个段的段内的第一篇文档号,那么将全局文档号与之做减法就获得了段内文档号。注意的是,为了便于介绍,下文中出现的文档号都是段内文档号。
文档号是否在BlockState中?
图14:
BlockState中存储的是当前正在读取的Chunk的一些元数据,至少包含以下的内容:
- docBase
- chunkDocs
- sliced
- offsets[ ]数组
- numStoredFields[ ]数组
这些元数据对应在索引文件中内容如下所示:
图15:
通过docBase跟chunkDocs就可以判断当前chunk中是否包含某个文档号。
如果当前chunk中包含某个文档号,那么直接读取该Chunk即可,否则需要重新从.fdt中找到所属chunk(查找过程将在下一篇文中展开),同时更新BlockState,继而获得文档中的存储域信息。从这里我们可以看出,在实际使用过程中,获取存储域的信息时,最好按照全局的文档号的大小依次获取,随机的文档号会导致频繁的更新blockState,也就是需要从索引文件中不断的读取Chunk,即增加了I/O开销。
结语
基于篇幅,剩余的内容将在下一篇文章中展开。
点击下载附件