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

相关推荐
infiniteWei2 天前
【Lucene】搜索引擎和文档相关性评分 BM25 算法的工作原理
算法·搜索引擎·lucene
天蓝蓝235282 天前
Lucene数据写入流程
java·mybatis·lucene
shiming88793 天前
Lucene数据写入与数据刷盘机制
java·mybatis·lucene
infiniteWei3 天前
【Lucene】详解倒排表的结构,如何实现词典与文档的映射关系
搜索引擎·全文检索·lucene
infiniteWei10 天前
【Lucene】详细讲解创建索引的步骤:分词、去停用词、语言处理、倒排表构建
搜索引擎·全文检索·lucene
Elastic 中国社区官方博客10 天前
Lucene 和 Elasticsearch 中更好的二进制量化 (BBQ)
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索·lucene
infiniteWei12 天前
【Lucene】架构概览和核心组件介绍
搜索引擎·架构·全文检索·lucene
光仔December12 天前
【Elasticsearch入门到落地】1、初识Elasticsearch
大数据·elk·elasticsearch·搜索引擎·lucene
infiniteWei13 天前
【Lucene】从文本到索引:Lucene如何构建索引
搜索引擎·全文检索·lucene
infiniteWei15 天前
【Lucene】什么是全文检索?解读结构化数据与非结构化数据
django·全文检索·lucene