从零开始实现 Go 搜索引擎(二)-FST 构造算法
缘由
最近 N+1 了,年底也没打算怎么找工作(现在爽是真的爽,估计明年就要自闭了🐶)。
空闲之余捣鼓下开源项目,在紧锣密鼓实现 Golang 版本的 FST,欢迎大佬观摩(还没开发完,暂不支持 PR😂),https://github.com/geange/lucene-go。
什么是 FST
FST 是 Trie 树的一种。下图可见,FST 是一种可以共享前缀/后缀的 Trie 树。
FST 的优点在于可以使用更紧凑的空间存储大量的数据。
思考
由于 lucene 中数据结构复杂,希望读者能专注于 FST 的构建算法。
结构
构建 FST 之前,需要先了解构建 FST 的时候有哪些关键的数据结构。
Label:输入值中的一个字符,对于
MOP
这个单词,存在 3 个字符,即 3 个 LabelArc:连接节点(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 的使用的实际场景。
算法
我们使用一个例子来讲解 FST 的构建。
场景
上面我们讲过 FST 实际上可以理解为一个 map。我们向这个 KV 写入以下数据:
需要注意点是,输入的 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 的同学。在写本文的,大佬们的文章对我帮助很大。
版权声明: 本文为 InfoQ 作者【geange】的原创文章。
原文链接:【http://xie.infoq.cn/article/5bc801cf4ef8863edc9b40ebe】。文章转载请联系作者。
评论