本文的代码以lucene-core 6.3.0为准,包含Builder的
add
函数,output
的压缩,finish
函数等整个类所有代码的解析。
0 基本信息
分享一下Builder类的源码解析。Builder类用来构建FST,以输入串(后文用term表示)acdx
,azyx
为例,得到的FST如下图:
上图可以看到FST可以共享前缀和后缀,term的一个字节单元对应FST的一个Arc。先了解一下Builder中的一些基本信息。
- 从Builder类的开头注释中看到,输入的term需要先排好序。
- 在Builder类中,用类
Arc
代表上图的边,用类UnCompiledNode
代表上图的节点。 NodeHash
就是个Node的hash表,在共享后缀Node的时候,如果两个Node的hash值一致,那就只写一次到FST。- 仔细看下Arc和UnCompiledNode这两个类,会发现都有
output
和isFinal
两个字段,Arc的output
,表示当前Arc上有输出,isFinal
表示Arc已经是终止Arc,其target是-1,不可以继续深度遍历。Node的isFinal
表示从Node出发的Arc数量是0,当Node是Final Node时,output
才有值。 - 图1中,Node的编号仅代表其写入FST的顺序, 每个Node的Arc会写入FST,Node写入FST只有编号或者Address,想了解FST的源码可以看下我的另一篇Lucene源码分析 - FST
- 对于Arc数量为0的Node,如图 1中的Node 1,不会写入FST。
- Arc的target是Final Node的是,
nextFinalOutput
等于target的output
,这样做的作用后文会提到。
1 源码分析
1.1 term 如何写入 FST
Builder的add
函数接收term并写入FST,首先判断输入的term是否为空,然后计算当前term与上个term的公共前缀长度得到prefixLenPlus1:
// 判断输入的term是否为空
if (input.length == 0) {
frontier[0].inputCount++;
frontier[0].isFinal = true;
fst.setEmptyOutput(output);
return;
}
int pos1 = 0;
int pos2 = input.offset;
final int pos1Stop = Math.min(lastInput.length(), input.length);
// 遍历当前term与上个term的公共前缀的长度
while(true) {
// 共享Node的term的数量+1
frontier[pos1].inputCount++;
if (pos1 >= pos1Stop || lastInput.intAt(pos1) != input.ints[pos2]) {
break;
}
pos1++;
pos2++;
}
final int prefixLenPlus1 = pos1+1;
// 将上个term的后缀写入FST
freezeTail(prefixLenPlus1);
这里的frontier
是个UnCompiledNode数组,主要是存公共前缀Node,Node的inputCount
是指共享这个Node的term的数量(后文用Node的共享数代替),根节点被所有的term共享。已知了公共前缀的长度,之后通过freezeTail
将上个term的后缀Node全部写入到FST中或者删除掉。
Builder设置变量minSuffixCount1
和minSuffixCount2
来控制Node的删除,Node的共享数小于minSuffixCount1
的节点删除,或者Node的父节点的共享数小于minSuffixCount2
的节点删除掉。这里删掉节点的逻辑似乎没有用到,Lucene的其他代码中,minSuffixCount1
和minSuffixCount2
都设置成了0。后文中,不再考虑删除节点的情况,也就是认为minSuffixCount1
和minSuffixCount2
都是0。
private void freezeTail(int prefixLenPlus1) throws IOException {
//这里downTo大于等于1可以保证根节点不会被写入到FST中去,根节点必须要所有节点写完之后才能写到FST
final int downTo = Math.max(1, prefixLenPlus1);
// 这里从后往前遍历
// 1. 里面有删除节点的逻辑
// 2. 所有节点写入FST之后,是深度优先遍历的序列
for(int idx=lastInput.length(); idx >= downTo; idx--) {
boolean doPrune = false;
boolean doCompile = false;
final UnCompiledNode<T> node = frontier[idx];
final UnCompiledNode<T