Lucene源码系列(十一):从一个例子来说说Lucene FST的构建

666 阅读12分钟

背景

上一篇文章中我们从源码层面详细介绍了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-labelnode-编号分别描述特定的边和节点。
  • 构建过程存储的都是节点的arc信息。
  • FSTCompiler#dedupHash用来判断一个node是否序列化过,如果序列化过可以返回node的编号。
  • 以下描述,为了方便起见就不介绍相关的reverse操作,直接按最终序列化的结果顺序进行介绍。
  • curIndex表示序列化缓存当前的下标,用来跟踪node编号。
  • 在我们的例子中,arc5元组的每个元素都是单字节表示。

构建过程

加入{"bat": 2}

如下图所示,这是FST的第一个输入,“b”,“a”,“t”分别对应了3个arc,output在第一个arc-b上面。此时无法判断不会变化的节点,因此序列化缓存中只有一个标记结束的0,因此curIndex=0。

FST_1.png

fst_store_1.png

加入{"cat": 5}

加入“cat”的时候可以确定前一个输入“bat”中包含arc-a的节点,包含arc-t的节点以及终止节点是确定不变的,因此这三个节点可以进行序列化。序列化之后,每个node都有个编号,是node在序列化缓存中的起始位置,终止节点编号始终是-1。

序列化是从后往前进行的,先序列化终止节点,再序列化包含arc-t的节点,最后序列化包含arc-a的节点:

  1. 序列化终止节点

    终止节点并没有执行序列化,它只是在FSTCompiler#dedupHash中存储了固定的node编号-1。后面有序列化终止节点的话,可以直接共享一个终止节点就好。

  2. 序列化包含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在序列化缓存中的起始位置)。

  3. 序列化包含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上,最后结果如下图所示:

FST_2.png

fst_store_3.png

加入{"deep": 15}

加入“deep”的时候可以确定前一个输入“cat”中包含arc-a的节点,包含arc-t的节点以及终止节点是确定不变的,因此这三个节点可以进行序列化。序列化是从后往前进行的,先序列化终止节点,再序列化包含arc-t的节点,最后序列化包含arc-a的节点:

  1. 序列化终止节点

    终止节点已经序列化过了,直接获取到-1的节点编号。

  2. 序列化包含arc-t的节点

    序列化的时候FSTCompiler#dedupHash会判断“cat”中包含arc-t的节点和“bat”中包含的arc-t的节点是一样的,所以就会直接共享,不会重复序列化,从而从图中看到的就是后缀共享了。

  3. 序列化包含arc-a的节点

    序列化的时候FSTCompiler#dedupHash会判断“cat”中包含arc-a的节点和“bat”中包含的arc-a的节点是一样的,所以就会直接共享,不会重复序列化,从而从图中看到的就是后缀共享了。

不变的部分序列化之后,把新增的“deep”加入到FST中,输出15在arc-d上,最后结果如下图所示:

FST_3.png

fst_store_3.png

加入{"do": 10}

加入“do”的时候可以确定前一个输入“deep”中包含arc-e(第二个“e”)的节点,包含arc-p的节点以及终止节点是确定不变的,因此这三个节点可以进行序列化。序列化是从后往前进行的,先序列化终止节点,再序列化包含arc-p的节点,最后序列化包含arc-e的节点:

  1. 序列化终止节点

    终止节点已经序列化过了,直接获取到-1的节点编号。

  2. 序列化包含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在序列化缓存中的起始位置)。

  3. 序列化包含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上,最后结果如下图所示:

FST_4.png

fst_store_6.png

加入{"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”有用。

最后结果如下图所示:

FST_5.png

fst_store_6.png

加入{"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。

最后结果如下:

FST_6.png

fst_store_6.png

结束构建

结束构建相当于所有的节点都是确定不变的,都需要序列化。

  1. 序列化终止节点

    终止节点已经序列化过了,直接获取到-1的节点编号。

  2. 序列化包含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在序列化缓存中的起始位置)。

  3. 序列化包含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在序列化缓存中的起始位置)。

  4. 序列化包含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在序列化缓存中的起始位置)。

  5. 序列化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_7.png

fst_store_7.png

至于构建好的FST怎么使用,我们后面介绍。