1. 图解FST构造算法

缘由

年底离职了,最近也没打算认真找工作(现在爽是真的爽,估计明年就要自闭了🐶)。空闲之余捣鼓下开源项目,在紧锣密鼓实现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

由于MOPMOTH存在前缀MO,且路径对应的Output是Arc的Output之和,因此N1->N2的Output=91,而N3->P的Output=9,保证了MOP=100MOTH=91

写入POP=72前,节点冻结

处理END节点

终止节点返回值为固定值-1,并更新lastFrozenNode为 -1。

处理N4节点/H

在写入POP=72前,需要将部分节点持久化到内存中。由于POPMOTH没有相同的前缀,因此,需要将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:HMOTH的最后一个字符
  • 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):PMOP的最后一个字符
  • 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前,节点冻结

因为STARPOP没有相同前缀,因此需要处理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):PPOP的最后一个字符
  • 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前,节点冻结

由于STOPSTAR存在共同前缀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前,节点冻结

由于TOPSTOP没有公共前缀,需要冻结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节点/复用后缀

TOPPOP存在相同后缀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的同学。在写本文的,阅读了许多大佬的文章。

相关推荐
cyh男3 天前
lucene中AutomatonQuery类的作用
lucene
cyh男3 天前
lucene中的PointRangeQuery和PointInSetQuery有什么区别
lucene
cyh男3 天前
为什么ES中不推荐使用wildcard查询
elasticsearch·lucene
渣渣盟5 天前
中文分词技术全解析
搜索引擎·全文检索·lucene
cyh男17 天前
lucene 8.7.0 版本中的倒排索引、数字、DocValues三种类型的查询性能对比
lucene
cyh男19 天前
Lucene 8.7.0 版本中dvd、dvm文件详解
lucene
是犹橐籥19 天前
头歌Educoder答案 Lucene - 全文检索入门
搜索引擎·全文检索·lucene
cyh男20 天前
Lucene 8.7.0 版本中docFreq、totalTermFreq、getDocCount等方法的含义
lucene
cyh男21 天前
Lucene 8.7.0 版本中doc、tim、tip、tmd文件详解
lucene
极限实验室1 个月前
搜索百科(1):Lucene —— 打开现代搜索世界的第一扇门
搜索引擎·lucene