lucene学习-FST源码分析

FST源码分析

我们以下面方法为入口查看FST的创建过程,这里in数组为写入的数据,out数组为对应的输出,通过builder协助构造FST

@Test
public void testFST() throws IOException {
   
   
    String in[] = {
   
   "cat", "deep", "do", "dog", "dogs"};
    long out[] = {
   
   5, 7, 17, 18, 21};
    PositiveIntOutputs singleton = PositiveIntOutputs.getSingleton();
    Builder<Long> longBuilder = new Builder<Long>(FST.INPUT_TYPE.BYTE1, singleton);
    IntsRefBuilder intsRef = new IntsRefBuilder();
    for (int i = 0; i < in.length; i++) {
   
   
        String s = in[i];
        BytesRef bytesRef = new BytesRef(s);
        longBuilder.add(Util.toIntsRef(bytesRef, intsRef), out[i]);
    }
    FST<Long> fst = longBuilder.finish();
    Long values = Util.get(fst, new BytesRef("cat"));
    System.out.println(values);
}

由于FST的创建非常复杂,这里需要借助Builder来帮助创建,builder内容如下

public Builder(FST.INPUT_TYPE inputType, Outputs<T> outputs) {
   
   
    this(inputType, 0, 0, true, true, Integer.MAX_VALUE, outputs, true, 15);
  }

public Builder(FST.INPUT_TYPE inputType, int minSuffixCount1, int minSuffixCount2, boolean doShareSuffix,
                 boolean doShareNonSingletonNodes, int shareMaxTailLength, Outputs<T> outputs,
                 boolean allowArrayArcs, int bytesPageBits) {
   
   
    //穿越某个节点的边的数量的下限,如果小于这个值,则要删除这个节点
    this.minSuffixCount1 = minSuffixCount1;
    //是0,在代码中不起作用 
    this.minSuffixCount2 = minSuffixCount2;
    //当编译某一个节点的时候,如果这个节点是多个term都穿过的,是否要共享此节点,如果不共享,则直接编译入fst中,否则要放入一个去重的对象中,让其他的节点共享这个节点。 
    this.doShareNonSingletonNodes = doShareNonSingletonNodes;
    //可以共享后缀个数
    this.shareMaxTailLength = shareMaxTailLength;
    //边是否启用定长存储,当某个节点发出的边非常多的时候可以使用fixedArray的格式,使用二分搜索加快查询速度,以空间换时间 
    this.allowArrayArcs = allowArrayArcs;
    //封装的fst对象,bytesPageBits为BytesStore对象记录fst编译后的二进制内容时,使用的byte[]的大小
    fst = new FST<>(inputType, outputs, bytesPageBits);
    bytes = fst.bytes;
    assert bytes != null;
    //共享后缀的部分,用来查找共享后缀
    if (doShareSuffix) {
   
   
      dedupHash = new NodeHash<>(fst, bytes.getReverseReader(false));
    } else {
   
   
      dedupHash = null;
    }
    //arc无输出时候的默认输出
    NO_OUTPUT = outputs.getNoOutput();
	//frontier数组用于辅助构建fst,主要保存没有保存到fst的节点
    @SuppressWarnings({
   
   "rawtypes","unchecked"}) final UnCompiledNode<T>[] f =
        (UnCompiledNode<T>[]) new UnCompiledNode[10];
    frontier = f;
    for(int idx=0;idx<frontier.length;idx++) {
   
   
      frontier[idx] = new UnCompiledNode<>(this, idx);
    }
  }

我们先看下arc的属性

public static final class Arc<T> {
   
   
    public int label;
    public T output;

    /** To node (ord or address) */
    public long target;

    byte flags;
    public T nextFinalOutput;

    // address (into the byte[]), or ord/address if label == END_LABEL
    long nextArc;

    /** Where the first arc in the array starts; only valid if
     *  bytesPerArc != 0 */
    public long posArcsStart;
    
    /** Non-zero if this arc is part of an array, which means all
     *  arcs for the node are encoded with a fixed number of bytes so
     *  that we can random access by index.  We do when there are enough
     *  arcs leaving one node.  It wastes some bytes but gives faster
     *  lookups. */
    public int bytesPerArc;

    /** Where we are in the array; only valid if bytesPerArc != 0. */
    public int arcIdx;

    /** How many arcs in the array; only valid if bytesPerArc != 0. */
    public int numArcs;
  • label:存放输入值的字符作为key,但是为ASCII的二进制形式
  • output:存放对应的输出值value
  • target:节点地址
  • flags:标记位,每一位都记录了arc的属性信息,通过位运算获取信息

flags记录了arc的属性,主要有几下几种:

static final int BIT_FINAL_ARC = 1 << 0;
static final int BIT_LAST_ARC = 1 << 1;
static final int BIT_TARGET_NEXT = 1 << 2;
static final int BIT_STOP_NODE = 1 << 3;
public static final int BIT_ARC_HAS_OUTPUT = 1 << 4;
static final int BIT_ARC_HAS_FINAL_OUTPUT = 1 << 5;
  • BIT_FINAL_ARC:arc对应字符是不是term最后一个字符
  • BIT_LAST_ARC:arc是不是当前节点最后一条边
  • BIT_TARGET_NEXT:arc连接的两个节点是临近节点,及需不需要记录跳转信息
  • BIT_STOP_NODE:arc的目标节点是一个终止节点
  • BIT_ARC_HAS_OUTPUT:arc是否有output输出值
  • BIT_ARC_HAS_FINAL_OUTPUT:arc是否有final output输出值,当output不能满足输出时需要通过final output协助完成

这里我们以cat/5、deep/7、do/17、dog/18、dogs/21为例看下FST是如何生成的

第一步先写入cat/5,节点全部为UnCompiledNode保存在frontier数组中,并且每个节点都有一条边,通过arc保存了label值和边的属性
在这里插入图片描述
第二步插入deep/7,FST写入的数据必须是已经排序好的,上面的cat以c开头,当写入deep时候,说明以c开头的数据已经写入完毕,可以将cat字符串中的‘t’和’a’,进行冻结存入FST。FST的写入为倒叙,首先会写入flags,这里flags为15(BIT_FINAL_ARC+BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_STOP_NODE)表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点。然后写入字符‘t’对应的二进制,最后对写入部分进行翻转。然后将字符‘a’写入FST,其flags为6(BIT_LAST_ARC+BIT_TARGET_NEXT)表明arc是当前节点最后一条边而且arc连接的两个节点是临近节点不需要记录target
在这里插入图片描述
第三步插入do/17,do和deep有公共字符d可以将字符p和字符e写入FST,这里字符p的flags和上面字符t的flags相同为15,表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点,字符e的flags和上面的字符a的flags相同,表明arc是当前节点最后一条边而且arc连接的两个节点是临近节点
在这里插入图片描述
第四步插入dog/18和dogs/21,和上面的do公共前缀为do,这里无法知道以do开头的字符是否写入结束此时无数据写入FST
在这里插入图片描述
第五步所有数据写入完毕,调用finish方法将剩余数据写入FST,首先写入字符s,字符s的flags为31(BIT_FINAL_ARC+BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_STOP_NODE+BIT_ARC_HAS_OUTPUT)表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点,并且arc记录了output并且值为3
在这里插入图片描述
将字符g写入FST,flags为23(BIT_FINAL_ARC+BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_ARC_HAS_OUTPUT)表明arc对应字符是term最后一个字符,arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,arc的目标节点是一个终止节点,并且arc记录了output并且值为1
在这里插入图片描述
然后将字符o和字符e写入FST,字符o的flags为23和上面一样,而字符e的flags为0说明上面所有属性都是没有的,字符e和前一个节点也不是临近节点所以这里记录了target值为8,8就是current数组的索引值,这里就是指向字符e的flags
在这里插入图片描述
最近将从root发出的所有边及字符d和c写入FST,这里d的flags为22(BIT_LAST_ARC+BIT_TARGET_NEXT+BIT_ARC_HAS_OUTPUT)表明arc是当前节点最后一条边,arc连接的两个节点是临近节点不需要记录target,并且arc记录了output并且值为7。字符c的flags为16(BIT_ARC_HAS_OUTPUT)表明arc记录了output并且值为5,记录了target值为4,指向了这里字符a的flags。下图current数组记录了FST的最终结果。
在这里插入图片描述
当我们需要读取FST时,比如读取cat时会先找到字符c对应的flags,该值为16说明arc记录了output值,不是指向最终节点,同时需要通过target找到下一个字符,这里target为4需要重置读取索引到4,读取字符a的flags为6,不是指向最终节点并且前一个节点为临近节点,则读取前一个字符t的flags值为15,说明字符t指向了最终节点结束,且字符和cat完全匹配,将查找过程经过的output值累加这里就一个output值及输出5。上面的图画的不够标准,这里主要为了方便理解

下面分析一下FST实现的源码,调用builder的add方法添加数据

public void add(IntsRef input, T output) throws IOException {
   
   
    // De-dup NO_OUTPUT since it must be a singleton:
    //无输出时候的默认值
    if (output.equals(NO_OUTPUT)) {
   
   
      output = NO_OUTPUT;
    }

    assert lastInput.length() == 0 || input.compareTo(lastInput.get()) >= 0: "inputs are added out of order lastInput=" + lastInput.get() + " vs input=" + input;
    assert validOutput(output);

    //System.out.println("\nadd: " + input);
    //输入内容为空时候的处理,只允许作为第一个输入
    if (input.length == 0) {
   
   
      // empty input: only allowed as first input.  we have
      // to special case this because the packed FST
      // format cannot represent the empty input since
      // 'finalness' is stored on the incoming arc, not on
      // the node
      frontier[0].inputCount++;
      frontier[0].isFinal = true;
      fst.setEmptyOutput(output);
      return;
    }

    // compare shared prefix length
    //一、查询上次数据和本次数据共享前缀长度
    int pos1 = 0;
    int pos2 = input.offset;
    //有个字符串结束就结束了
    final int pos1Stop = Math.min(lastInput.length(), input.length);
    while(true) {
   
   
      //穿过节点的数量+1
      frontier[pos1].inputCount++;
      //System.out.println("  incr " + pos1 + " ct=" + frontier[pos1].inputCount + " n=" + frontier[pos1]);
      //找到不相同的字符或已经到达其中一个字符串的尾部,也就是字符串遍历完了,就退出
      if (pos1 >= pos1Stop || lastInput.intAt(pos1) != input.ints[pos2]) {
   
   
        break;
      }
      pos1++;
      pos2++;
    }
    //公共前缀的数量,由于第一个是root所以要加一
    final int prefixLenPlus1 = pos1+1;
    //二、frontier初始化时候的数组长度默认为10,如果字符串长度超过了,则需要对frontier进行扩容
    if (frontier.length < input.length+1) {
   
   
      final UnCompiledNode<T>[] next = ArrayUtil.grow(frontier, input.length+1);
      for(int idx=frontier.length;idx<next.length;idx++) {
   
   
        next[idx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值