Point/Range 类型写入详解
(本文示例都是简单的一维IntPoint)
1. 写入主要流程
说明: 对Point来说,在DefaultIndexingChain
的processField
方法中,只走到了indexPoint
方法, 其中主要步骤有:
- 根据field name构造
PerField
,PerField
保存了每一个filed基本上所有的元数据, PerField数组组织形式如下:
主要算法: 根据fieldName hash取模得到数组下标index, 然后在对应的PerFiled中查找对应的PerFiled(如果index所对应PerField值不为null), 这个实现很类似于java的Map中根据key查找value过程。
关于为什么不直接使用Java 的Map, 作者在Java doc说根据测试表明采用数组 + 链表然后手动扩容方式相比于Map来说有3%的性能优势
PerField对应的数据结构:
PerField为
DefaultIndexingChain
的内部类, 其中有关数据数据结构如图:
其中需要注的的有:
- fieldInfo //field元数据
- PointValuesWriter //写PointValue类
像InvertState, TermHashPerFiled、docValuesWriter等对于Point类型来说,这个不需要关心。
根据PerField信息,构造维度信息;接着构造
PointValuesWriter
对象, 这个对象负责写Point数据,并将这个对象赋值给PerField的pointValuesWriter成员变量将文档的id与point值存入
PointValuesWriter
, 其中Point值会以ByteRef形式存储,这实际上是byte[]的一个封装。
Doc id会存入docID的int数组,下标为该point的序号,即第几个point值
Point会存入bytes的ByteBlockPool
, 这个对象是一个可变的byte[][], 每个point 会转化成byte[]存入bytes, 因为point是固定字节,因此不需要设置offset与length。
具体代码如下:
public void addPackedValue(int docID, BytesRef value) {
if (value == null) {
throw new IllegalArgumentException("field=" + fieldInfo.name + ": point value must not be null");
}
if (value.length != packedBytesLength) {
throw new IllegalArgumentException("field=" + fieldInfo.name + ": this field's value has length=" + value.length + " but should be " + (fieldInfo.getPointDataDimensionCount() * fieldInfo.getPointNumBytes()));
}
if (docIDs.length == numPoints) {
docIDs = ArrayUtil.grow(docIDs, numPoints+1);
iwBytesUsed.addAndGet((docIDs.length - numPoints) * Integer.BYTES);
}
bytes.append(value);
docIDs[numPoints] = docID;
if (docID != lastDocID) {
numDocs++;
lastDocID = docID;
}
numPoints++;
}
现在将Point值存储内存的过程就结束了,是不是看起来很简单呢? 确实, 将数据保存在内存中的逻辑相对简单,复杂的过程在最后flush
中程中
Flush过程
flush
操作一般是由于用户显示调用commit()方法, 其它可能的情况包括内存缓存的数据过多,内存不足、合并段文件等,下面以用户显示调用IndexWriter.commit()
来说明Point类型说明对应的过程。
flush
操作总入口在DocumentsWriterPerThread
的flush
方法中, 主要方法为
//...
sortMap = consumer.flush(flushState);
// ...
- 从
DefaultIndexingChain
的flush
函数进去后,对于point类型来说,主要会调用-
writeNorm()
//没有显示设置的话,这个不起作用, 主要作用是写入Norm
值,关于Lucene中Norm
作用请google -
writeDocValues()
//对于point类型来说这个不起作用, 主要记录像String, Filed docId到value的正向索引、NumericType与binary 对应的值。 -
writepoint()
这个函数会将所有的point filed数据写入文件, 包括数据与索引 - 写入String, text field类型的数据、索引
- 构造segment文件中关于所有field信息的元数据
-
现在重点关注第3步过程
- 后面调用链路如下:
DefaultIndexingChain.writePoints()
-->PointValuesWriter.flush()
-->Lucene60PointsWriter.writeField()
-->BKDWriter.writeField()
BKDWriter.writeField()
会调用BKDWriter.add()
与BKDWriter.finish()
方法。 代码链路比较长,直接贴最后的文件格式,后面会对文件格式具体内容进行说明
笔者测试代码如下:
for (int i = 0; i < 10240; i++) {
Document d = new Document();
IntPoint p = new IntPoint("age", i);
d.add(p);
writer.addDocument(d);
}
writer.commit();
最终的存储形式如下图:
Point索引元数据文件组织形式
dii
文件保存的是block数据在dim
文件的偏移量,查询的时候首先加载dii
文件,根据dii
文件同时加载dim
文件的packedIndex
区,拿到BKD的索引数据以构造BKD查询树。
查询的时候访问BKD
(Block K Dimension)查询树,确定Block下标,比如查询1025这个值,可以很明确的知道在以1024为起始值的block中,通这该block的下标在dii文件中获取block在dim文件的地址,最后采用二分的方式找到对应的point
以上三张图大致展示Point field的存储结构及细节,关于Point field需要特别掌握BKD算法思想与实现
这是实现Point/Range类型数据存储与索引的核心。
关于具体代码流程与细节地方读者如果有兴趣,有可以自行参考上图去阅读。