背景
在上一篇文章中我们从源码层面详细介绍了FST构建的完整流程,本文用一个例子,按步骤借助图文描述,巩固下构建的逻辑。我们使用的例子如下:
public class FSTDemo {
public static void main(String[] args) throws IOException {
String[] inputValues = {"bat", "cat", "deep", "do", "dog", "dogs"};
long[] outputvalues = {2, 5, 15, 10, 3, 2};
PositiveIntOutputs outputs = PositiveIntOutputs.getSingleton();
FSTCompiler.Builder<Long> builder = new FSTCompiler.Builder<>(FST.INPUT_TYPE.BYTE1, outputs);
FSTCompiler<Long> build = builder.build();
IntsRefBuilder intsRefBuilder = new IntsRefBuilder();
for (int i = 0; i < inputValues.length; i ++) {
BytesRef bytesRef = new BytesRef(inputValues[i]);
build.add(Util.toIntsRef(bytesRef, intsRefBuilder), outputvalues[i]);
}
FST<Long> fst = build.compile();
BytesRef bytesRef = new BytesRef(inputValues[3]);
Long aLong = Util.get(fst, Util.toIntsRef(bytesRef, intsRefBuilder));
System.out.println(aLong);
}
}
必看说明
- 虚线表示的是还可能变化的部分,实线是已经序列化完成的部分。是否可变是对节点来说的,节点包含了arc,arc可以有输出,因此不变的判断就是节点的arc不会新增,节点所有的arc的输出不会再变化。
- 双圈表示的是终止节点。
- 在FST图中,arc的标记是label/output,node的标记是编号/finalOutput。
- 在文字描述中用arc-label和node-编号分别描述特定的边和节点。
- 构建过程存储的都是节点的arc信息。
- FSTCompiler#dedupHash用来判断一个node是否序列化过,如果序列化过可以返回node的编号。
- 以下描述,为了方便起见就不介绍相关的reverse操作,直接按最终序列化的结果顺序进行介绍。
- curIndex表示序列化缓存当前的下标,用来跟踪node编号。
- 在我们的例子中,arc5元组的每个元素都是单字节表示。
构建过程
加入{"bat": 2}
如下图所示,这是FST的第一个输入,“b”,“a”,“t”分别对应了3个arc,output在第一个arc-b上面。此时无法判断不会变化的节点,因此序列化缓存中只有一个标记结束的0,因此curIndex=0。
加入{"cat": 5}
加入“cat”的时候可以确定前一个输入“bat”中包含arc-a的节点,包含arc-t的节点以及终止节点是确定不变的,因此这三个节点可以进行序列化。序列化之后,每个node都有个编号,是node在序列化缓存中的起始位置,终止节点编号始终是-1。
序列化是从后往前进行的,先序列化终止节点,再序列化包含arc-t的节点,最后序列化包含arc-a的节点:
-
序列化终止节点
终止节点并没有执行序列化,它只是在FSTCompiler#dedupHash中存储了固定的node编号-1。后面有序列化终止节点的话,可以直接共享一个终止节点就好。
-
序列化包含arc-t的节点
arc-t序列化5元组如下:
-
target
arc-t指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-t指向的节点没有finalOutput
-
output
arc-t没有output
-
label
“t”的ASCII编码116
-
flag
- arc-t指向了可接受的节点,因此需要加BIT_FINAL_ARC标记。
- arc-t是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-t指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
- arc-t指向了终止节点,因此需要加上BIT_STOP_NODE标记。
arc-t的flag= BIT_FINAL_ARC + BIT_LAST_ARC + BIT_TARGET_NEXT + BIT_STOP_NODE = 15
因为arc-t的5元组只有label和flag,curIndex += 2,所以包含arc-t的节点的编号就是2(反着看的node在序列化缓存中的起始位置)。
-
-
序列化包含arc-a的节点
arc-a序列化5元组如下:
-
target
arc-a指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-a指向的节点没有finalOutput
-
output
arc-a没有output
-
label
“a”的ASCII编码97
-
flag
- arc-a是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-a指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
arc-a的flag = BIT_LAST_ARC + BIT_TARGET_NEXT = 6
因为arc-a的5元组只有label和flag,curIndex += 2,所以包含arc-a的节点的编号就是4(反着看的node在序列化缓存中的起始位置)。
-
不变的部分序列化之后,把新增的“cat”加入到FST中,输出5在arc-c上,最后结果如下图所示:
加入{"deep": 15}
加入“deep”的时候可以确定前一个输入“cat”中包含arc-a的节点,包含arc-t的节点以及终止节点是确定不变的,因此这三个节点可以进行序列化。序列化是从后往前进行的,先序列化终止节点,再序列化包含arc-t的节点,最后序列化包含arc-a的节点:
-
序列化终止节点
终止节点已经序列化过了,直接获取到-1的节点编号。
-
序列化包含arc-t的节点
序列化的时候FSTCompiler#dedupHash会判断“cat”中包含arc-t的节点和“bat”中包含的arc-t的节点是一样的,所以就会直接共享,不会重复序列化,从而从图中看到的就是后缀共享了。
-
序列化包含arc-a的节点
序列化的时候FSTCompiler#dedupHash会判断“cat”中包含arc-a的节点和“bat”中包含的arc-a的节点是一样的,所以就会直接共享,不会重复序列化,从而从图中看到的就是后缀共享了。
不变的部分序列化之后,把新增的“deep”加入到FST中,输出15在arc-d上,最后结果如下图所示:
加入{"do": 10}
加入“do”的时候可以确定前一个输入“deep”中包含arc-e(第二个“e”)的节点,包含arc-p的节点以及终止节点是确定不变的,因此这三个节点可以进行序列化。序列化是从后往前进行的,先序列化终止节点,再序列化包含arc-p的节点,最后序列化包含arc-e的节点:
-
序列化终止节点
终止节点已经序列化过了,直接获取到-1的节点编号。
-
序列化包含arc-p的节点
arc-a序列化5元组如下:
-
target
arc-p指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-p指向的节点没有finalOutput
-
output
arc-p没有output
-
label
“p”的ASCII编码112
-
flag
- arc-p指向了可接受的节点,因此需要加BIT_FINAL_ARC标记。
- arc-p是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-p指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
- arc-p指向了终止节点,因此需要加上BIT_STOP_NODE标记。
arc-p的flag= BIT_FINAL_ARC + BIT_LAST_ARC + BIT_TARGET_NEXT + BIT_STOP_NODE = 15
因为arc-p的5元组只有label和flag,curIndex += 2,所以包含arc-p的节点的编号就是6(反着看的node在序列化缓存中的起始位置)。
-
-
序列化包含arc-e的节点
arc-e序列化5元组如下:
-
target
arc-e指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-e指向的节点没有finalOutput
-
output
arc-e没有output
-
label
“e”的ASCII编码101
-
flag
- arc-e是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-e指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
arc-e的flag = BIT_LAST_ARC + BIT_TARGET_NEXT = 6
因为arc-e的5元组只有label和flag,curIndex += 2,所以包含arc-e的节点的编号就是8(反着看的node在序列化缓存中的起始位置)。
-
不变的部分序列化之后,把新增的“do”加入到FST中,这时需要调整部分arc的输出:
设置arc的输出,“deep”和“do”共享d这个前缀,两者的公共输出是min(10, 15)= 10,因此arc-d的输出调整为10,“deep”剩下的15 -10 = 5就下推到arc-e上,最后结果如下图所示:
加入{"dog": 3}
加入“dog”没有不变的节点需要序列化。这里有个情况,要用到节点的finalOutput。
“do”和“dog”的公共输出是min(3,10)=3,所以arc-d的输出改为3,对“deep”来说,arc-d原来是10,现在是3,剩下的7需要下推到arc-e,因此arc-e的输出是12。
“do”剩下的输出7也不能放在arc-o,因为放在arc-o的话“dog”的输出就不对了。因此,把剩下的输出7存储在节点中,后面序列化的时候以finalOutput的形式存储在arc-o中。这个finalOutput的输出只对“do”有用。
最后结果如下图所示:
加入{"dogs": 2}
加入“dogs”没有新的序列化节点。
“dog”和“dogs”的公共输出是min(3,2)= 2,所以arc-d的输出改为2,对“deep”来说,arc-d原来是3,现在是2,剩下的1需要下推到arc-e,因此arc-e的输出是13。
“do”剩下的输出1不能放在arc-o,因为放在arc-o的话dog的输出就不对了,把剩下的1下推到arc-o的target中,因此arc-o的target的finalOutput是8。
“dog”剩下的输出1不能放在arc-o和arc-g上,把剩下的1下推到arc-g的target中,因此arc-g的target的finalOutput是1。
最后结果如下:
结束构建
结束构建相当于所有的节点都是确定不变的,都需要序列化。
-
序列化终止节点
终止节点已经序列化过了,直接获取到-1的节点编号。
-
序列化包含arc-s的节点
arc-s序列化5元组如下:
-
target
arc-s指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-s指向的节点没有finalOutput
-
output
arc-s没有output
-
label
“s”的ASCII编码115
-
flag
- arc-s指向了可接受的节点,因此需要加BIT_FINAL_ARC标记。
- arc-s是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-s指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
- arc-s指向了终止节点,因此需要加上BIT_STOP_NODE标记。
arc-s的flag= BIT_FINAL_ARC + BIT_LAST_ARC + BIT_TARGET_NEXT + BIT_STOP_NODE = 15
因为arc-s的5元组只有label和flag,curIndex += 2,所以包含arc-s的节点的编号就是10(反着看的node在序列化缓存中的起始位置)。
-
-
序列化包含arc-g的节点
arc-s序列化5元组如下:
-
target
arc-g指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-g指向的节点finalOutput是1
-
output
arc-g没有output
-
label
“g”的ASCII编码103
-
flag
- arc-g指向了可接受的节点,因此需要加BIT_FINAL_ARC标记。
- arc-g是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-g指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
- arc-g指向的节点存在finalOutput,因此需要加BIT_ARC_HAS_FINAL_OUTPUT标记。
arc-g的flag = BIT_FINAL_ARC + BIT_LAST_ARC + BIT_TARGET_NEXT + BIT_ARC_HAS_FINAL_OUTPUT = 39
因为arc-g的5元组只有finalOutput,label和flag,curIndex += 3,所以包含arc-g的节点的编号就是13(反着看的node在序列化缓存中的起始位置)。
-
-
序列化包含arc-o和arc-e的节点
arc-o的序列化5元组如下:
-
target
arc-o指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-o指向的节点finalOutput是1
-
output
arc-o没有output
-
label
“o”的ASCII编码111
-
flag
- arc-o指向了可接受的节点,因此需要加BIT_FINAL_ARC标记。
- arc-o是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-o指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
- arc-o指向的节点存在finalOutput,因此需要加BIT_ARC_HAS_FINAL_OUTPUT标记。
arc-o的flag = BIT_FINAL_ARC + BIT_LAST_ARC + BIT_TARGET_NEXT + BIT_ARC_HAS_FINAL_OUTPUT = 39
arc-e的序列化5元组如下:
-
target
arc-e指向的节点的编号是8
-
finalOutput
arc-e指向的节点不存在finalOutput
-
output
arc-e的output是13
-
label
“e”的ASCII编码101
-
flag
- arc-e存在output,因此需要加BIT_ARC_HAS_OUTPUT标记。
arc-e的flag = BIT_ARC_HAS_OUTPUT = 16
因为arc-o的5元组只有finalOutput,label和flag,arc-e的5元组只有target,output,label和flag,curIndex += 7,所以包含arc-o和arc-e的节点的编号就是20(反着看的node在序列化缓存中的起始位置)。
-
-
序列化root节点
arc-d的序列化5元组如下:
-
target
arc-d指向了上一个序列化的节点,因此不需要存储target。
-
finalOutput
arc-d指向的节点不存在finalOutput
-
output
arc-d的output是2
-
label
“d”的ASCII编码100
-
flag
- arc-d存在output,因此需要加BIT_ARC_HAS_OUTPUT标记。
- arc-d是节点的最后一个arc,因此需要加BIT_LAST_ARC标记。
- arc-d指向的target是上一个序列化的节点,因此需要加BIT_TARGET_NEXT标记。
arc-d的flag = BIT_ARC_HAS_OUTPUT + BIT_LAST_ARC + BIT_TARGET_NEXT = 22
arc-c的序列化5元组如下:
-
target
arc-c指向的节点的编号是4
-
finalOutput
arc-c指向的节点不存在finalOutput
-
output
arc-c的output是5
-
label
“c”的ASCII编码99
-
flag
- arc-c存在output,因此需要加BIT_ARC_HAS_OUTPUT标记。
arc-c的flag = BIT_ARC_HAS_OUTPUT = 16
arc-b的序列化5元组如下:
-
target
arc-b指向的节点的编号是4
-
finalOutput
arc-b指向的节点不存在finalOutput
-
output
arc-b的output是2
-
label
“b”的ASCII编码98
-
flag
- arc-b存在output,因此需要加BIT_ARC_HAS_OUTPUT标记。
arc-b的flag = BIT_ARC_HAS_OUTPUT = 16
因为arc-d的5元组只有output,label和flag,arc-c的5元组只有target,output,label和flag,arc-d的5元组只有target,output,label和flag,curIndex += 11,所以root的节点的编号就是31(反着看的node在序列化缓存中的起始位置)。
-
最终构建的结果如下:
至于构建好的FST怎么使用,我们后面介绍。