缘由
年底离职了,最近也没打算认真找工作(现在爽是真的爽,估计明年就要自闭了🐶)。空闲之余捣鼓下开源项目,在紧锣密鼓实现Golang版本的FST,欢迎大佬观摩(还没开发完,暂不支持PR😂),github.com/geange/luce...。
什么是FST
FST是Trie树的一种。下图可见,FST是一种可以共享前缀/后缀的Trie树。
FST的优点在于可以使用更紧凑的空间存储大量的数据。
思考
由于lucene中数据结构复杂,希望读者能专注于FST的构建算法。
结构
构建FST之前,需要先了解构建FST的时候有哪些关键的数据结构。
- Label :输入值中的一个字符,对于
MOP
这个单词,存在3个字符,即3个Label - Arc :连接节点(Node),存储Label和Output数据
- Output :你可以将FST理解为一个KV存储结构。Output就是这个KV存储的值(Value)。在FST中,一条路径下对于一个Key值,而这条路径上所有Arc上存储的Output之和才是这个Key对应的Value。
- Node:节点,它的作用是用于容纳Arc,你认为它就是一个容器即可。
下面2条路径可以看做 key="MOP",value=100。
在FST中,一条路径下对于一个Key值,而这条路径上所有Arc上存储的Output之和才是这个Key对应的Value。
Flag标记
这里的标记有一个概念即可,后续的流程中会穿插介绍Flag的使用的实际场景。
Flag | Value | Description |
---|---|---|
BIT_FINAL_ARC | 1 | arc对应的label是某个term的最后一个字符 |
BIT_LAST_ARC | 2 | arc是Node节点中的最后一个Arc |
BIT_TARGET_NEXT | 4 | 当前的arc中的label不是输入值的最后一个字符 |
BIT_STOP_NODE | 8 | arc的target是一个终止节点 |
BIT_ARC_HAS_OUTPUT | 16 | arc有output值(output不为0) |
算法
我们使用一个例子来讲解FST的构建。
场景
上面我们讲过FST实际上可以理解为一个map。我们向这个KV写入以下数据:
Key | Value/Output |
---|---|
MOP | 100 |
MOTH | 91 |
POP | 72 |
STAR | 83 |
STOP | 54 |
TOP | 55 |
需要注意点是,输入的Key都是已经排序的
重现
写入MOP=100
方格块对应的是字节数组,FST使用字节数组存储数据(即冻结)。构建完成后数组不再变动。
写入MOTH=91
由于MOP
和MOTH
存在前缀MO
,且路径对应的Output是Arc的Output之和,因此N1->N2的Output=91,而N3->P的Output=9,保证了MOP=100
和MOTH=91
。
写入POP=72前,节点冻结
处理END节点
终止节点返回值为固定值-1,并更新lastFrozenNode为 -1。
处理N4节点/H
在写入POP=72
前,需要将部分节点持久化到内存中。由于POP
和MOTH
没有相同的前缀,因此,需要将N2到N4的节点进行冻结(存储到字节数组中)。处理的顺序从后往前,所以先处理N4。处理完N4后,lastFronzenNode=N4。
index=2存储的是flag,当前H的flag满足以下几个条件:
15 = BIT_LAST_ARC(2) + BIT_TARGET_NEXT(4) + BIT_FINAL_ARC(1) + BIT_STOP_NODE(8)
- BIT_LAST_ARC:它是N4中的最后一个arc
- BIT_TARGET_NEXT:arc的目标节点为
END
,因此认为跟lastFrozenNode一致都为-1(lastFrozenNode默认初始值为-1) - BIT_FINAL_ARC:
H
为MOTH
的最后一个字符 - BIT_STOP_NODE:arc的目标节点是一个终止节点(BIT_STOP_NODE)
处理N3节点
因为N3有2个Arc,需要从下往上处理。处理完N3节点后,lastFronzenNode=N3
处理ARC=T
一个节点如果存在多个Arc,就从下往上处理。这里处理Arc=T的。
T
对应的arc,满足以下几个flag:
6 = BIT_LAST_ARC(2) + BIT_TARGET_NEXT(4)
- BIT_LAST_ARC(2):它是N3中的最后一个arc
- BIT_TARGET_NEXT(4):arc的target节点的值为N4,而最新的lastFronzenNode的值是N4对应生成的,故满足BIT_TARGET_NEXT
处理ARC=P
因为Arc=P存在Output=9,因此index=5的值为9。
P
对应的arc,满足以下几个flag,因此index=7的值为25:
25 = BIT_FINAL_ARC(1) + BIT_STOP_NODE(8) + BIT_ARC_HAS_OUTPUT(16)
- BIT_FINAL_ARC(1):
P
是MOP
的最后一个字符 - BIT_STOP_NODE(8):arc的目标节点是一个终止节点(nil)
- BIT_ARC_HAS_OUTPUT(16):arc有output值
处理N2节点/O
O
对应的arc满足以下几个flag:
6 = BIT_LAST_ARC(2) + BIT_TARGET_NEXT(4)
BIT_LAST_ARC(2):arc是Node2节点中的最后一个arc
BIT_TARGET_NEXT(4):arc的目标节点为N3,而最新的lastFronzenNode的值是N3对应生成的,故满足
写入POP=72
上一步处理完N2节点后,获取字节数组的当前游标的位置为9,因此Arc=M/91的目标位置为9。
写入STAR=83前,节点冻结
因为STAR
和POP
没有相同前缀,因此需要处理N2、N3的Arc(节点冻结)。
处理END节点
终止节点返回值为固定值-1,并更新lastFrozenNode为 -1。
处理N3节点/P
处理END
节点,导致lastFrozenNode=-1。
P
对应的arc,满足以下几个flag:
15 = BIT_LAST_ARC(1) + BIT_FINAL_ARC(2) + BIT_TARGET_NEXT(4) + BIT_STOP_NODE(8)
- BIT_FINAL_ARC(1):
P
是POP
的最后一个字符 - BIT_LAST_ARC(2):arc是N3节点中的最后一个arc
- BIT_TARGET_NEXT(4):arc的目标节点为终止节点,上一个lastFrozenNode的值为终止节点对应的值,故相同
- BIT_STOP_NODE(8):arc的目标节点是一个终止节点
处理N2节点/O
O
对应的arc,满足以下几个flag:
6 = BIT_LAST_ARC(2) + BIT_TARGET_NEXT(4)
- BIT_LAST_ARC(2):arc是N2节点中的最后一个arc
- BIT_TARGET_NEXT(4):arc的目标节点为终止节点,上一个lastFrozenNode为N3
写入STAR=83
上一步处理完N2节点后,获取字节数组的当前游标的位置为13,因此Arc=P/72的目标位置为13。
写入STOP前,节点冻结
由于STOP
和STAR
存在共同前缀ST
。因此需要冻结N4节点。
处理END节点
终止节点返回值为固定值-1,并更新lastFrozenNode为 -1。
处理N4节点/R
R
对应的arc,满足以下几个flag:
- BIT_FINAL_ARC(1):arc对应的
STAR
最后一个字符 - BIT_LAST_ARC(2):arc是N4节点中的最后一个Arc
- BIT_TARGET_NEXT(4):当前的arc的目标节点为-1
- BIT_STOP_NODE(8):arc的目标节点是一个终止节点
写入STOP=54
写入TOP前,节点冻结
由于TOP
和STOP
没有公共前缀,需要冻结N2后的节点。
处理END节点
终止节点返回值为固定值-1,并更新lastFrozenNode为 -1。
处理N4节点/复用后缀
之前写入的POP
的最后的P
的跟当前N4的Arc=P相同,因此复用已有的后缀。
处理N3节点
处理Arc=O
由于Arc=P是复用POP的Arc,因此不满足BIT_TARGET_NEXT
O
对应的arc,满足以下几个flag:
- BIT_LAST_ARC(2):arc是N3节点中的最后一个Arc
处理Arc=A/29
A
对应的arc,满足以下几个flag:
- BIT_ARC_HAS_OUTPUT(16):arc有output值
处理N2节点
T
对应的arc,满足以下几个flag:
- BIT_LAST_ARC(2):arc是N2节点中的最后一个Arc
- BIT_TARGET_NEXT(4):arc的目标节点为N3,满足
写入TOP=55
冻结N2/3节点
处理N2、N3节点/复用后缀
TOP
和POP
存在相同后缀OP
。因此可以复用后缀。(字节数组不变动)
冻结N1节点
处理ARC=T/55
-
由于Arc=T/55的目标节点位置为13,因此
bs[25] = 13
-
output=55,因此
bs[26] = 55
T
对应的arc,满足以下几个flag:
18 = BIT_LAST_ARC(2) + BIT_ARC_HAS_OUTPUT(16)
- BIT_LAST_ARC(2):arc是N1节点中的最后一个Arc
- BIT_ARC_HAS_OUTPUT(16):arc有output值
处理ARC=S/54
-
由于Arc=S/54的目标节点位置为24,因此
bs[29] = 24
-
output=54,因此
bs[30] = 54
S
对应的arc,满足以下几个flag:
- BIT_ARC_HAS_OUTPUT(16):arc有output值
处理ARC=P/72
-
由于Arc=P/72的目标节点位置为13,因此
bs[33] = 13
-
output=72,因此
bs[34] = 72
P
对应的arc,满足以下几个flag:
- BIT_ARC_HAS_OUTPUT(16):arc有output值
处理ARC=M/91
-
由于Arc=M/91的目标节点位置为9,因此
bs[37] = 9
-
output=91,因此
bs[38] = 91
M
对应的arc,满足以下几个flag:
- BIT_ARC_HAS_OUTPUT(16):arc有output值
结语
本文简单介绍了FST的构建过程,希望有助于希望了解FST的同学。在写本文的,阅读了许多大佬的文章。