本文的代码以lucene-core 6.3.0为准,包含构建空FST的
addNode
函数,pack
压缩函数等整个类所有代码的解析。转载请注明出处。
0 基本信息
lucene的FST的二进制存储和查询方式都是由FST这个类来实现。如果要读这些代码,首先需要了解这个类里面的一些基本的东西。
- 整个FST存到了
BytesStore
类的bytes
对象里,bytes
相当于一个大的字节数组,提供正向遍历和反向遍历数组的方式,bytes
中存的是Node的Arc
的内容,包括label,output,nextFinalOutput,target等。 - 在
Arc
中,target是指Arc指向的Node,这个值可以是Node的ID,也就是ord
,或者Node的Address。 nodeAddress
存的是Node ID->Address的映射,inCounts
存的是Node ID -> Node的入度(指向该Node的Arc数量),这两个类都是GrowableWriter
,包了一下PackedInts
,关于PackedInts
可以参考下作者@iteye_14612的PackedInts
源码分析。cachedRootArcs
缓存一部分从根节点出发的Arc,INPUT_TYPE
是Arc的label所占用的字节数。- 文中多次提到Node的Arc会按照label的字典序存,这与Builder构建FST有关,可以看下我的另一篇Lucene源码分析 - FST-Builder。
1 源码分析
先看下构建空的FST。addNode
函数把Builder中的UnCompiledNode写入FST的bytes
对象,写入的这个Node的ID就是nodeCount
,这里把Node的Arc顺序写到字节数组中,由于FST的Builder是按照label的字典序调用构建的FST,所以这里的Arc也是按照label排序的,代码如下:
// 处理没有Arc的空节点
if (nodeIn.numArcs == 0) {
if (nodeIn.isFinal) {
return FINAL_END_NODE;
} else {
return NON_FINAL_END_NODE;
}
}
// 记录当前Node在Address
final long startAddress = builder.bytes.getPosition();
// 判断是否用FIXED_ARRAY方式存Arc的信息
final boolean doFixedArray = shouldExpand(builder, nodeIn);
if (doFixedArray) {
if (builder.reusedBytesPerArc.length < nodeIn.numArcs) {
//reusedBytesPerArc 用来存下每个Arc的字节长度
builder.reusedBytesPerArc = new int[ArrayUtil.oversize(nodeIn.numArcs, 1)];
}
}
1.1 什么是 FIXED ARRAY
如果这里的doFixedArray
是true
,那么每个Arc都用一样的字节长度(后文都用长度代替),由于有的Arc可能有output
或nextFinalOutput
,而有的却没有,所以每个Arc的长度可能是不一样。FIXED_ARRAY
方式下,每个Arc的存储长度都用最长的那个Arc的长度。
很明显,这种FIXED_ARRAY方式会导致Arc与Arc之间存在一些内存碎片,好处就是能快速定位Node下的第N个Arc的位置,所以在查询Node下是否有Arc的label匹配一个确定的值的时候,可以用二分查找,这样就是空间换了时间。
当然用这种模式也是有条件的,shouldExpand
函数中给出了条件,比如Node下Arc数量要超过10,Arc数量太少的话,二分的效果比起遍历没有优势,而且还浪费了一些内存,反而亏了。
1.2 存储Node的Arc
接下来是遍历这个Node的所有的Arc,并且把每个Arc的信息等写入bytes
,前后两个Arc紧挨着存的,没有内存空隙:
for(int arcIdx=0;arcIdx<nodeIn.numArcs;arcIdx++) {
... //省略flags处理逻辑
// 存标志位信息
builder.bytes.writeByte((byte) flags);
writeLabel(builder.bytes, arc.label);
// 这里可以看到,每个Arc的长度不一样,主要是下面这三个write的内容不一样
if (arc.output != NO_OUTPUT) {
outputs.write(arc.output, builder.bytes);
}
if (arc.nextFinalOutput != NO_OUTPUT) {
outputs.writeFinalOutput(arc.nextFinalOutput, builder.bytes);
}
if (targetHasArcs && (flags & BIT_TARGET_NEXT) == 0) {
builder.bytes.writeVLong(target.node);
}
if (doFixedArray) {
// 记录下每个Arc的长度
builder.reusedBytesPerArc[arcIdx] = (int) (builder.bytes.getPosition() - lastArcStart);
lastArcStart = builder.bytes.getPosition();
// 记录最长的Arc的长度
maxBytesPerArc = Math.max(maxBytesPerArc, builder.reusedBytesPerArc[arcIdx]);
}
}
1.3 将Arc紧邻而存转变成 FIXED ARRAY 存储
所有Arc存下之后,如果doFixedArray
为true
,由于前面Arc内容写入bytes
的时候是紧挨着写入的,所以还需要调整一下,每个Arc的长度要调整为最长的Arc的长度,所以从最后一个Arc开始,基本上每个Arc都需要向后移动到正确的位置上。代码如下:
if (doFixedArray) {
final int MAX_HEADER_SIZE = 11; // header(byte) + numArcs(vint) + numBytes(vint)
byte header[] = new byte[MAX_HEADER_SIZE];
ByteArrayDataOutput bad = new ByteArrayDataOutput(header);
bad.writeByte(ARCS_AS_FIXED_ARRAY); // 写入标志位,代表ARC是FIXED_ARRAY
bad.writeVInt(nodeIn.numArcs); // 写入Arc的数量
bad.writeVInt(maxBytesPerArc); // 写入最大的边的长度
int headerLen = bad.getPosition();
final long fixedArrayStart = startAddress + headerLen;
//记录最后一个Arc的Address,注意这里是反向读取
long srcPos = builder.bytes.getPosition();
//记录FIXED_ARRAY方式下,最后一个Arc调整之后的Address
long destPos = fixedArrayStart + nodeIn.numArcs*maxBytesPerArc;
if (destPos > srcPos) {
// bytes 移动到调整的位置
builder.bytes.skipBytes((int) (destPos - srcPos));
for(int arcIdx=nodeIn.numArcs-1;arcIdx>=0;arcIdx--) {
// 从最后一个Arc开始调整
// 计算应该调整后的位置
destPos -= maxBytesPerArc;
srcPos -= builder.reusedBytesPerArc[arcIdx];
if (srcPos != destPos) {
// 如果Arc调整后的位置与当前所处位置相比发生了变化,那么就移动Arc到调整之后的位置
builder.bytes.copyBytes(srcPos, destPos, builder.reusedBytesPerArc