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] = new UnCompiledNode<>(this, idx);
      }
      frontier = next;
    }

    // minimize/compile states from previous input's
    // orphan'd suffix
    //三、将prefixLenPlus1下标的这个节点包括后续的节点都要冷冻,也就是进入FST 
    freezeTail(prefixLenPlus1);

    // init tail states for current input
    //将input数据中非共享的部分存入frontier数组
    //idx<=input.length:多添加一个,最后一个为stop节点(结束节点)
    for(int idx=prefixLenPlus1;idx<=input.length;idx++) {
      //添加一个边指向下一个节点
      frontier[idx-1].addArc(input.ints[input.offset + idx - 1],
                             frontier[idx]);
      //指向该节点的边个数+1
      frontier[idx].inputCount++;
    }
	//最后一个节点,及结束节点
    final UnCompiledNode<T> lastNode = frontier[input.length];
    
	//本次添加的字符串长度和上次添加的长度相同,或者是完全相同则不需要设置结束节点,因为之前这条路径已经设置过了
    if (lastInput.length() != input.length || prefixLenPlus1 != input.length + 1) {
      //结束标志
      lastNode.isFinal = true;
      //默认没有输出
      lastNode.output = NO_OUTPUT;
    }

    // push conflicting outputs forward, only as far as
    // needed
    //idx=0为root节点跳过
    for(int idx=1;idx<prefixLenPlus1;idx++) {
      //获取节点
      final UnCompiledNode<T> node = frontier[idx];
      //父节点
      final UnCompiledNode<T> parentNode = frontier[idx-1];
	  //
      final T lastOutput = parentNode.getLastOutput(input.ints[input.offset + idx - 1]);
      assert validOutput(lastOutput);
	  //公共输出
      final T commonOutputPrefix;
      //现在的输出-公共输出,转移到下一个输出
      final T wordSuffix;

      if (lastOutput != NO_OUTPUT) {
        //公共输出
        commonOutputPrefix = fst.outputs.common(output, lastOutput);
        assert validOutput(commonOutputPrefix);
        //转移到下一个输出
        wordSuffix = fst.outputs.subtract(lastOutput, commonOutputPrefix);
        assert validOutput(wordSuffix);
        //修改父节点的输出为公共输出
        parentNode.setLastOutput(input.ints[input.offset + idx - 1], commonOutputPrefix);
        //将剩余输入设置到当前节点
        node.prependOutput(wordSuffix);
      } else {
        commonOutputPrefix = wordSuffix = NO_OUTPUT;
      }
	  //更新output,然后继续看下一个节点  
      output = fst.outputs.subtract(output, commonOutputPrefix);
      assert validOutput(output);
    }
	//两次输入的内容相同,则需要将输出合并
    if (lastInput.length() == input.length && prefixLenPlus1 == 1+input.length) {
      // same input more than 1 time in a row, mapping to
      // multiple outputs
      lastNode.output = fst.outputs.merge(lastNode.output, output);
    } else {
      // this new arc is private to this new input; set its
      // arc output to the leftover output:
      //将剩余的输出写到arc上
      frontier[prefixLenPlus1-1].setLastOutput(input.ints[input.offset + prefixLenPlus1-1], output);
    }

    // save last input
    //保存上一个输入
    lastInput.copyInts(input);

    //System.out.println("  count[0]=" + frontier[0].inputCount);
  }

这段代码主要做了以下几件事情:

  • 获取前一个写入数据和后一个写入的公共前缀,及prefixLenPlus1就表示公共前缀的长度
  • 检查frontier的容量是否需要扩容
  • 调用freezeTail方法将已经确定下来不再改变的后缀冻结,就是写入fst
  • 构建output输出值

freezeTail方法就是将冻结的尾部写入fst,这个是构造fst的核心方法

//prefixLenPlus1及其之后的节点需要冻结,及写入fst
private void freezeTail(int prefixLenPlus1) throws IOException {
    //System.out.println("  compileTail " + prefixLenPlus1);
    //第一个节点是root节点,此处至少从第二个节点开始冻结,只有在最后的时候才会为0,冻结所有节点
    final int downTo = Math.max(1, prefixLenPlus1);
    for(int idx=lastInput.length(); idx >= downTo; idx--) {
	  //doPrune都是false
      boolean doPrune = false;
      //doCompile都是true
      boolean doCompile = false;
	  //需要冻结的节点
      final UnCompiledNode<T> node = frontier[idx];
      //需要冻结的节点的父节点
      final UnCompiledNode<T> parent = frontier[idx-1];
      //minSuffixCount1为0
      if (node.inputCount < minSuffixCount1) {
        doPrune = true;
        doCompile = true;
      } else if (idx > prefixLenPlus1) {
        // prune if parent's inputCount is less than suffixMinCount2
        //minSuffixCount2也为0
        if (parent.inputCount < minSuffixCount2 || (minSuffixCount2 == 1 && parent.inputCount == 1 && idx > 1)) {
          // my parent, about to be compiled, doesn't make the cut, so
          // I'm definitely pruned 

          // if minSuffixCount2 is 1, we keep only up
          // until the 'distinguished edge', ie we keep only the
          // 'divergent' part of the FST. if my parent, about to be
          // compiled, has inputCount 1 then we are already past the
          // distinguished edge.  NOTE: this only works if
          // the FST outputs are not "compressible" (simple
          // ords ARE compressible).
          doPrune = true;
        } else {
          // my parent, about to be compiled, does make the cut, so
          // I'm definitely not pruned 
          doPrune = false;
        }
        doCompile = true;
      } else {
        // if pruning is disabled (count is 0) we can always
        // compile current node
        doCompile = minSuffixCount2 == 0;
      }

      //System.out.println("    label=" + ((char) lastInput.ints[lastInput.offset+idx-1]) + " idx=" + idx + " inputCount=" + frontier[idx].inputCount + " doCompile=" + doCompile + " doPrune=" + doPrune);
	  //minSuffixCount2为0
      if (node.inputCount < minSuffixCount2 || (minSuffixCount2 == 1 && node.inputCount == 1 && idx > 1)) {
        // drop all arcs
        for(int arcIdx=0;arcIdx<node.numArcs;arcIdx++) {
          @SuppressWarnings({"rawtypes","unchecked"}) final UnCompiledNode<T> target =
          (UnCompiledNode<T>) node.arcs[arcIdx].target;
          target.clear();
        }
        node.numArcs = 0;
      }

      if (doPrune) {
        // this node doesn't make it -- deref it
        node.clear();
        parent.deleteLast(lastInput.intAt(idx-1), node);
      } else {

        if (minSuffixCount2 != 0) {
          compileAllTargets(node, lastInput.length()-idx);
        }
        //要冷冻的节点的输出,也就是在他位置结束的term的finalOutput,这个必须单独拿出来,因为在已经编译到fst的节点中是没有nextFinalOutput的,所以要将这个nextFinalOutput转移到前面的arc中去  
        final T nextFinalOutput = node.output;

        // We "fake" the node as being final if it has no
        // outgoing arcs; in theory we could leave it
        // as non-final (the FST can represent this), but
        // FSTEnum, Util, etc., have trouble w/ non-final
        // dead-end states:
        final boolean isFinal = node.isFinal || node.numArcs == 0;

        if (doCompile) {
          // this node makes it and we now compile it.  first,
          // compile any targets that were previously
          // undecided:
          // 先对node进行编译,进入fst,返回一个fst中的节点(编译过的节点),然后再用父节点的arc指向这个新的编译过的节点(使用的父节点的arc一定是最后一个,如果不是最后一个早就进入fst了)  
          parent.replaceLast(lastInput.intAt(idx-1),
                             compileNode(node, 1+lastInput.length()-idx),
                             nextFinalOutput,
                             isFinal);
        } else {
          // replaceLast just to install
          // nextFinalOutput/isFinal onto the arc
          parent.replaceLast(lastInput.intAt(idx-1),
                             node,
                             nextFinalOutput,
                             isFinal);
          // this node will stay in play for now, since we are
          // undecided on whether to prune it.  later, it
          // will be either compiled or pruned, so we must
          // allocate a new node:
          frontier[idx] = new UnCompiledNode<>(this, idx);
        }
      }
    }
  }

将父节点指向冻结后的节点,冻结后的节点就是compileNode,该方法就是将节点转变为冻结节点

private CompiledNode compileNode(UnCompiledNode<T> nodeIn, int tailLength) throws IOException {
    final long node;
    //bytes编译之后存储的字节数组
    long bytesPosStart = bytes.getPosition();
     //共享后缀,多个路线穿过的节点要共享
     if (dedupHash != null && (doShareNonSingletonNodes || 
         //单个的当然更要共享
         nodeIn.numArcs <= 1) && 
         //尾部共享长度不能超过阈值,默认是Integer.MAX_VALUE
         tailLength <= shareMaxTailLength ) {
        //最后的节点无arc,直接写入fst,因为判断重复是根据arc判断的
        if (nodeIn.numArcs == 0) {  
            //写入fst
            node = fst.addNode(this, nodeIn); 
            //如果两个节点紧挨着,则就不需要target记录跳转
            lastFrozenNode = node;} else {
            //不是最后一个节点,对节点进行去重,共享后缀的实现
            node = dedupHash.add(this, nodeIn); 
        }  
    } else {
        //不去重的逻辑,此时直接进入fst
        node = fst.addNode(this, nodeIn);  
    }  
    assert node != -2;
    //重新获取编译后的字节数组结尾位置
    long bytesPosEnd = bytes.getPosition();
    //不相等说明写入了节点
    if (bytesPosEnd != bytesPosStart) {
      // The FST added a new node:
      assert bytesPosEnd > bytesPosStart;
      //节点没有被去重,最后冻结的节点更新为本节点
      lastFrozenNode = node;
    }
    //添加进fst后可以清除arc了
    nodeIn.clear();
    //构造已经写入fst的节点然后返回
    final CompiledNode fn = new CompiledNode();
    fn.node = node;
    return fn;
  }

上面代码中有一个dedupHash.add(this, nodeIn)方法,改方法主要是为了实现共享后缀,dedupHash其实可以认为是一个hashmap用来去重

public long add(Builder<T> builder, Builder.UnCompiledNode<T> nodeIn) throws IOException {
    //System.out.println("hash: add count=" + count + " vs " + table.size() + " mask=" + mask);
    //获取hash值
    final long h = hash(nodeIn);
    //坐标
    long pos = h & mask;
    int c = 0;
    while(true) {
      //v=0表示该坐标无数据,也就是没有重复
      final long v = table.get(pos);
      if (v == 0) {
        // freeze & add
        //将节点添加到fst中
        final long node = fst.addNode(builder, nodeIn);
        //System.out.println("  now freeze node=" + node);
        assert hash(node) == h : "frozenHash=" + hash(node) + " vs h=" + h;
        //记录写入fst节点数
        count++;
        //将数据设置到该坐标
        table.set(pos, node);
        // Rehash at 2/3 occupancy:
        //数量达到table长度的2/3,需要进行扩容
        if (count > 2*table.size()/3) {
          rehash();
        }
        return node;
      } else if (nodesEqual(nodeIn, v)) {
        // same node is already here
        //节点重复,返回之前的节点
        return v;
      }

      // quadratic probe
      pos = (pos + (++c)) & mask;
    }
  }

我们知道hashmap其中最主要的方法就是hash值的计算,下面看下这里是如何计算hash值的,也就是在什么情况下发生hash冲突

private long hash(Builder.UnCompiledNode<T> node) {
    //31是一个质数减少碰撞的概率提高散列(hashing)算法的性能和效率而且31可以方便地通过位运算来进行乘法
    final int PRIME = 31;
    //System.out.println("hash unfrozen");
    long h = 0;
    // TODO: maybe if number of arcs is high we can safely subsample?
    //所有边都要参与计算
    for(int arcIdx=0;arcIdx<node.numArcs;arcIdx++) {
      final Builder.Arc<T> arc = node.arcs[arcIdx];
      //System.out.println("  label=" + arc.label + " target=" + ((Builder.CompiledNode) arc.target).node + " h=" + h + " output=" + fst.outputs.outputToString(arc.output) + " isFinal?=" + arc.isFinal);
      //label参与计算
      h = PRIME * h + arc.label;
      //目标节点
      long n = ((Builder.CompiledNode) arc.target).node;
      h = PRIME * h + (int) (n^(n>>32));
      //输出值
      h = PRIME * h + arc.output.hashCode();
      h = PRIME * h + arc.nextFinalOutput.hashCode();
      if (arc.isFinal) {
        h += 17;
      }
    }
    //System.out.println("  ret " + (h&Integer.MAX_VALUE));
    return h & Long.MAX_VALUE;
  }

nodesEqual判断两个节点是否相同,如果相同说明是重复而非hash冲突

private boolean nodesEqual(Builder.UnCompiledNode<T> node, long address) throws IOException {
    fst.readFirstRealTargetArc(address, scratchArc, in);
    //边数量不同
    if (scratchArc.bytesPerArc != 0 && node.numArcs != scratchArc.numArcs) {
      return false;
    }
    //所有边上的属性都相同,才说明相同
    for(int arcUpto=0;arcUpto<node.numArcs;arcUpto++) {
      final Builder.Arc<T> arc = node.arcs[arcUpto];
      if (arc.label != scratchArc.label ||
          !arc.output.equals(scratchArc.output) ||
          ((Builder.CompiledNode) arc.target).node != scratchArc.target ||
          !arc.nextFinalOutput.equals(scratchArc.nextFinalOutput) ||
          arc.isFinal != scratchArc.isFinal()) {
        return false;
      }

      if (scratchArc.isLast()) {
        //数量一致
        if (arcUpto == node.numArcs-1) {
          return true;
        } else {
          return false;
        }
      }
      fst.readNextRealArc(scratchArc, in);
    }

    return false;
  }

下面看看addNode方法如何将node添加到fst中

long addNode(Builder<T> builder, Builder.UnCompiledNode<T> nodeIn) throws IOException {
    T NO_OUTPUT = outputs.getNoOutput();

    //System.out.println("FST.addNode pos=" + bytes.getPosition() + " numArcs=" + nodeIn.numArcs);
    //没有边的节点且是结束节点则统一返回-1
    if (nodeIn.numArcs == 0) {
      if (nodeIn.isFinal) {
        return FINAL_END_NODE;
      } else {
        return NON_FINAL_END_NODE;
      }
    }
	//节点写入的起始位置
    final long startAddress = builder.bytes.getPosition();
    //System.out.println("  startAddr=" + startAddress);
	//如果节点太多则可以使用固定长度保存arc,及长度相同,因为arc上的label是已经排好序的则可以使用二分查找,以空间换时间加速查询速度
    final boolean doFixedArray = shouldExpand(builder, nodeIn);
    if (doFixedArray) {
      //System.out.println("  fixedArray");
      //记录arc占用内存的数据扩容
      if (builder.reusedBytesPerArc.length < nodeIn.numArcs) {
        builder.reusedBytesPerArc = new int[ArrayUtil.oversize(nodeIn.numArcs, 1)];
      }
    }
    //arc的数量
    builder.arcCount += nodeIn.numArcs;
    //最后一条边
    final int lastArc = nodeIn.numArcs-1;
    //记录一个arc写入的开始
    long lastArcStart = builder.bytes.getPosition();
    //做大的arc占用的大小
    int maxBytesPerArc = 0;
    //遍历节点所有的arc
    for(int arcIdx=0;arcIdx<nodeIn.numArcs;arcIdx++) {
      //边
      final Builder.Arc<T> arc = nodeIn.arcs[arcIdx];
      //边指向的目的节点
      final Builder.CompiledNode target = (Builder.CompiledNode) arc.target;
      //开始计算flags
      int flags = 0;
      //System.out.println("  arc " + arcIdx + " label=" + arc.label + " -> target=" + target.node);
      //如果是该节点最后一条边,则需要记录到flags中
      if (arcIdx == lastArc) {
        flags += BIT_LAST_ARC;
      }
      //上一次冻结的节点就是target节点,及两个节点是紧挨着的节点,此时不用存储target
      if (builder.lastFrozenNode == target.node && !doFixedArray) {
        // TODO: for better perf (but more RAM used) we
        // could avoid this except when arc is "near" the
        // last arc:
        flags += BIT_TARGET_NEXT;
      }
      //指向结束节点
      if (arc.isFinal) {
        flags += BIT_FINAL_ARC;
        //finalOutput存在
        if (arc.nextFinalOutput != NO_OUTPUT) {
          flags += BIT_ARC_HAS_FINAL_OUTPUT;
        }
      } else {
        assert arc.nextFinalOutput == NO_OUTPUT;
      }
      //大于0说明不是最后一个节点
      boolean targetHasArcs = target.node > 0;
      //是最后一个节点则记录到flags中
      if (!targetHasArcs) {
        flags += BIT_STOP_NODE;
      }
	  //flags记录存在output值
      if (arc.output != NO_OUTPUT) {
        flags += BIT_ARC_HAS_OUTPUT;
      }
      //保存flags
      builder.bytes.writeByte((byte) flags);
      //保存label
      writeLabel(builder.bytes, arc.label);

      // System.out.println("  write arc: label=" + (char) arc.label + " flags=" + flags + " target=" + target.node + " pos=" + bytes.getPosition() + " output=" + outputs.outputToString(arc.output));
      //将输出保存
      if (arc.output != NO_OUTPUT) {
        outputs.write(arc.output, builder.bytes);
        //System.out.println("    write output");
      }
      //将final输出保存
      if (arc.nextFinalOutput != NO_OUTPUT) {
        //System.out.println("    write final output");
        outputs.writeFinalOutput(arc.nextFinalOutput, builder.bytes);
      }
      //targetHasArcs==ture:表示不是结束节点
      //(flags & BIT_TARGET_NEXT) == 0:表示目标节点和本节点不是紧挨着
      if (targetHasArcs && (flags & BIT_TARGET_NEXT) == 0) {
        assert target.node > 0;
        //System.out.println("    write target");
        //需要记录目标节点的编号位置
        builder.bytes.writeVLong(target.node);
      }

      // just write the arcs "like normal" on first pass,
      // but record how many bytes each one took, and max
      // byte size:
      //使用固定长度保存arc
      if (doFixedArray) {
        //记录每个arc使用的内存
        builder.reusedBytesPerArc[arcIdx] = (int) (builder.bytes.getPosition() - lastArcStart);
        lastArcStart = builder.bytes.getPosition();
        //arc占用的最大内存
        maxBytesPerArc = Math.max(maxBytesPerArc, builder.reusedBytesPerArc[arcIdx]);
        //System.out.println("    bytes=" + builder.reusedBytesPerArc[arcIdx]);
      }
    }
    
    // TODO: try to avoid wasteful cases: disable doFixedArray in that case
    /* 
     * 
     * LUCENE-4682: what is a fair heuristic here?
     * It could involve some of these:
     * 1. how "busy" the node is: nodeIn.inputCount relative to frontier[0].inputCount?
     * 2. how much binSearch saves over scan: nodeIn.numArcs
     * 3. waste: numBytes vs numBytesExpanded
     * 
     * the one below just looks at #3
    if (doFixedArray) {
      // rough heuristic: make this 1.25 "waste factor" a parameter to the phd ctor????
      int numBytes = lastArcStart - startAddress;
      int numBytesExpanded = maxBytesPerArc * nodeIn.numArcs;
      if (numBytesExpanded > numBytes*1.25) {
        doFixedArray = false;
      }
    }
    */
    //按照固定长度重写一遍
    if (doFixedArray) {
      final int MAX_HEADER_SIZE = 11; // header(byte) + numArcs(vint) + numBytes(vint)
      assert maxBytesPerArc > 0;
      // 2nd pass just "expands" all arcs to take up a fixed
      // byte size

      //System.out.println("write int @pos=" + (fixedArrayStart-4) + " numArcs=" + nodeIn.numArcs);
      // create the header
      // TODO: clean this up: or just rewind+reuse and deal with it
      byte header[] = new byte[MAX_HEADER_SIZE]; 
      ByteArrayDataOutput bad = new ByteArrayDataOutput(header);
      // write a "false" first arc:
      //写入header标记
      bad.writeByte(ARCS_AS_FIXED_ARRAY);
      bad.writeVInt(nodeIn.numArcs);
      bad.writeVInt(maxBytesPerArc);
      int headerLen = bad.getPosition();
      //写完header之后的位置
      final long fixedArrayStart = startAddress + headerLen;

      // expand the arcs in place, backwards
      //写入之前的起始位置
      long srcPos = builder.bytes.getPosition();
      // 写完所有的arc之后的position
      long destPos = fixedArrayStart + nodeIn.numArcs*maxBytesPerArc;
      assert destPos >= srcPos;
      if (destPos > srcPos) {
        //空出位置
        builder.bytes.skipBytes((int) (destPos - srcPos));
        //从后往前写入,将之前写入的arc移动到新位置,这里没有重新创建一个byte进行copy,而是在原来的byte进行移动,srcPos代表原来arc的起始位置,destpos是新的需要写入数据的起始位置
        for(int arcIdx=nodeIn.numArcs-1;arcIdx>=0;arcIdx--) {
          //arc将要写入的开始位置
          destPos -= maxBytesPerArc;
          //之前写的这个arc的开始位置,也就是将从srcPos位置开始的数据copy到这里以destPos起始的位置
          srcPos -= builder.reusedBytesPerArc[arcIdx];
          //System.out.println("  repack arcIdx=" + arcIdx + " srcPos=" + srcPos + " destPos=" + destPos);
          //相等说明左右两边移动到的位置刚好相等,则不需要将数据copy
          if (srcPos != destPos) {
            //System.out.println("  copy len=" + builder.reusedBytesPerArc[arcIdx]);
            assert destPos > srcPos: "destPos=" + destPos + " srcPos=" + srcPos + " arcIdx=" + arcIdx + " maxBytesPerArc=" + maxBytesPerArc + " reusedBytesPerArc[arcIdx]=" + builder.reusedBytesPerArc[arcIdx] + " nodeIn.numArcs=" + nodeIn.numArcs;
            //拷贝数据到新位置
            builder.bytes.copyBytes(srcPos, destPos, builder.reusedBytesPerArc[arcIdx]);
          }
        }
      }
      
      // now write the header
      //将header写入
      builder.bytes.writeBytes(startAddress, header, 0, headerLen);
    }
    //节点写入结束的位置
    final long thisNodeAddress = builder.bytes.getPosition()-1;
    //fst存储节点是倒叙存储,也就是最前写入的数据保存到最后,在读取的时候方便读取,刚刚添加的这个node的所有的arc都是正序的,所以把这个node的所有的arc都倒叙。
    builder.bytes.reverse(startAddress, thisNodeAddress);
    //计数+1
    builder.nodeCount++;
    //返回结束位置,及倒叙读取的第一个arc
    return thisNodeAddress;
  }

后面构造OUT输出值,根据需要调整node的输出值。上面调用结束后还有部分数据没有写入FST,最后需要调用finish方法将所有内容写入。

public FST<T> finish() throws IOException {
	//根节点
    final UnCompiledNode<T> root = frontier[0];

    // minimize nodes in the last word's suffix
    //冻结所有,及将所有数据写入FST
    freezeTail(0);
    if (root.inputCount < minSuffixCount1 || root.inputCount < minSuffixCount2 || root.numArcs == 0) {
      if (fst.emptyOutput == null) {
        return null;
      } else if (minSuffixCount1 > 0 || minSuffixCount2 > 0) {
        // empty string got pruned
        return null;
      }
    } else {
      if (minSuffixCount2 != 0) {
        compileAllTargets(root, lastInput.length());
      }
    }
    //if (DEBUG) System.out.println("  builder.finish root.isFinal=" + root.isFinal + " root.output=" + root.output);
    //将root写入FST
    fst.finish(compileNode(root, lastInput.length()).node);

    return fst;
  }

freezeTail(0)方法传入的是0,但是内部会转变为1,final int downTo = Math.max(1, prefixLenPlus1),目的就是将frontier数组中的未编译的数据写入FST中。该方法结束后则所有term都写带了FST中,然后调用FST的finish方法对root节点进行编译,因为这里root发出的arc还没有编译

void finish(long newStartNode) throws IOException {
    assert newStartNode <= bytes.getPosition();
    if (startNode != -1) {
      throw new IllegalStateException("already finished");
    }
    if (newStartNode == FINAL_END_NODE && emptyOutput != null) {
      newStartNode = 0;
    }
    //节点在bytes中的开始位置
    startNode = newStartNode;
    //将current数据copy进blocks中
    bytes.finish();
    //缓存从root发出的边
    cacheRootArcs();
  }

首先设置节点位置,然后判断current数组不为空则将数据copy到blocks中

//copy数据,存入blocks中
public void finish() {
    if (current != null) {
      byte[] lastBuffer = new byte[nextWrite];
      System.arraycopy(current, 0, lastBuffer, 0, nextWrite);
      blocks.set(blocks.size()-1, lastBuffer);
      current = null;
    }
  }

然后这里为了提高效率和后续使用将缓存从root节点发出的所有边,因为在查询的时候从root发出的边每次都会被查找。

缓存root发出的边

private void cacheRootArcs() throws IOException {
    // We should only be called once per FST:
    assert cachedArcsBytesUsed == 0;
    //虚拟创建一个指向root的边
    final Arc<T> arc = new Arc<>();
    //填充上面虚拟指向root的边的数据
    getFirstArc(arc);
    //这个边有目标节点
    if (targetHasArcs(arc)) {
      //用来读取bytes
      final BytesReader in = getBytesReader();
      //用来存储查询到的边,大小为128
      Arc<T>[] arcs = (Arc<T>[]) new Arc[0x80];
      //读取真正第一个边
      readFirstRealTargetArc(arc.target, arc, in);
      //缓存个数
      int count = 0;
      while(true) {
        assert arc.label != END_LABEL;
        //label小于128才会进行缓存
        if (arc.label < arcs.length) {
          arcs[arc.label] = new Arc<T>().copyFrom(arc);
        } else {
          break;
        }
        //节点最后一个边
        if (arc.isLast()) {
          break;
        }
        readNextRealArc(arc, in);
        count++;
      }
      //缓存占用内存大小
      int cacheRAM = (int) ramBytesUsed(arcs);

      // Don't cache if there are only a few arcs or if the cache would use > 20% RAM of the FST itself:
      //当边的数量最少为5一条和缓存占用内存小于20%才会进行缓存
      if (count >= FIXED_ARRAY_NUM_ARCS_SHALLOW && cacheRAM < ramBytesUsed()/5) {
        cachedRootArcs = arcs;
        cachedArcsBytesUsed = cacheRAM;
      }
    }
  }

这里会先创建一个虚拟的arc边,然后填充数据,判断这个虚拟的从root发出的边是不是有目标节点,如果没有则什么也不做,如果有目标节点则获取真实的从root发出的边并判断label值是否小于128,只有小于128才会进行缓存,最后再判断从root发出的边的个数是否超过阈值如果小于5则不进行缓存,边的数量太少没有缓存的价值,同时需要判断缓存需要使用的内存占用不能超过内存使用的20%,否则也不会进行缓存

获取一个真实从root发出的边

public Arc<T> readFirstRealTargetArc(long node, Arc<T> arc, final BytesReader in) throws IOException {
    //节点位置
    final long address = node;
    //定位到需要读取的节点位置
    in.setPosition(address);
    //System.out.println("  readFirstRealTargtArc address="
    //+ address);
    //System.out.println("   flags=" + arc.flags);
    //如果节点是固定长度存储则先读取header信息,然后在读取边
    if (in.readByte() == ARCS_AS_FIXED_ARRAY) {
      //System.out.println("  fixedArray");
      // this is first arc in a fixed-array
      // 这个节点发出的arc的数量 
      arc.numArcs = in.readVInt();
      //占用字节数
      arc.bytesPerArc = in.readVInt();
      arc.arcIdx = -1;
      arc.nextArc = arc.posArcsStart = in.getPosition();
      //System.out.println("  bytesPer=" + arc.bytesPerArc + " numArcs=" + arc.numArcs + " arcsStart=" + pos);
    } else {
      //不是固定长度存储则直接获取arc的开始位置
      //arc.flags = b;
      arc.nextArc = address;
      arc.bytesPerArc = 0;
    }
    //定位到第一个arc的位置,这里开始读取
    return readNextRealArc(arc, in);
  }

public Arc<T> readNextRealArc(Arc<T> arc, final BytesReader in) throws IOException {

    // TODO: can't assert this because we call from readFirstArc
    // assert !flag(arc.flags, BIT_LAST_ARC);

    // this is a continuing arc in a fixed array
    //固定长度形式
    if (arc.bytesPerArc != 0) {
      // arcs are at fixed entries
      arc.arcIdx++;
      assert arc.arcIdx < arc.numArcs;
      //定位读取位置
      in.setPosition(arc.posArcsStart);
      //回退指定的长度然后开始读取
      in.skipBytes(arc.arcIdx*arc.bytesPerArc);
    } else {
      //非定长存储,直接设置读取位置
      // arcs are packed
      in.setPosition(arc.nextArc);
    }
    //先读取flags和label
    arc.flags = in.readByte();
    arc.label = readLabel(in);
    //根据flag判断是否有OUT输出
    if (arc.flag(BIT_ARC_HAS_OUTPUT)) {
      arc.output = outputs.read(in);
    } else {
      arc.output = outputs.getNoOutput();
    }
    //根据flag判断是否有FINAL_OUT输出
    if (arc.flag(BIT_ARC_HAS_FINAL_OUTPUT)) {
      arc.nextFinalOutput = outputs.readFinalOutput(in);
    } else {
      arc.nextFinalOutput = outputs.getNoOutput();
    }
    //指向结束节点,说明路线是一个完整路线,不需要读取下个节点了
    if (arc.flag(BIT_STOP_NODE)) {
      if (arc.flag(BIT_FINAL_ARC)) {
        arc.target = FINAL_END_NODE;
      } else {
        arc.target = NON_FINAL_END_NODE;
      }
      arc.nextArc = in.getPosition();
      //前一个节点和后一个节点是临近节点,不需要跳转
    } else if (arc.flag(BIT_TARGET_NEXT)) {
      //获取下一条边的读取位置
      arc.nextArc = in.getPosition();
      // TODO: would be nice to make this lazy -- maybe
      // caller doesn't need the target and is scanning arcs...
      //最后一条边
      if (!arc.flag(BIT_LAST_ARC)) {
        if (arc.bytesPerArc == 0) {
          // must scan
          //跳转下一个节点
          seekToNextNode(in);
        } else {
          //固定长度
          in.setPosition(arc.posArcsStart);
          in.skipBytes(arc.bytesPerArc * arc.numArcs);
        }
      }
      arc.target = in.getPosition();
    } else {
      //设置目的连接节点
      arc.target = readUnpackedNodeTarget(in);
      //下一条边
      arc.nextArc = in.getPosition();
    }
    return arc;
  }

这样就完成了边的缓存。

经过这些步骤就完成了所有的term的添加形成了一个FST,数据最终存储在BytesStore中,不过FST在存储的时候是倒叙的,就是先冻结的节点和边会存储到前面,而最后由root发出的边才会进行存储,如果需要读取FST数据,则必须要从后向前读取

最后我们看一下BytesStore的主要属性和方法

class BytesStore extends DataOutput implements Accountable {
  //内存使用
  private static final long BASE_RAM_BYTES_USED =
        RamUsageEstimator.shallowSizeOfInstance(BytesStore.class)
      + RamUsageEstimator.shallowSizeOfInstance(ArrayList.class);
  //数据会先写入current数组然后达到blockSize后写入blocks
  private final List<byte[]> blocks = new ArrayList<>();
  
  private final int blockSize;
  //默认为15
  private final int blockBits;
  private final int blockMask;

  private byte[] current;
  private int nextWrite;

  public BytesStore(int blockBits) {
    this.blockBits = blockBits;
    blockSize = 1 << blockBits;
    blockMask = blockSize-1;
    nextWrite = blockSize;
  }
  //将索引位置数据覆盖为目标值
  public void writeByte(int dest, byte b) {}
  //追加写入
  public void writeByte(byte b) {}
  //将数组数据从offet开始len长的数据写入current数组
  public void writeBytes(byte[] b, int offset, int len) {}
  //覆盖写入一个int
  public void writeInt(long pos, int value) {}
  //将指定区间内容的数据翻转
  public void reverse(long srcPos, long destPos) {}
  //挪动指针,跳过len长度,创造空间
  public void skipBytes(int len) {}
  //返回指针位置
  public long getPosition() {}
  //截断数据
  public void truncate(long newLen) {}
  //
  public void finish() {}
  //获取一个从前往后读取器
  public FST.BytesReader getForwardReader() {}
  //创建一个从后往前读取器
  FST.BytesReader getReverseReader(boolean allowSingle) {}
  //内存占用大小
  public long ramBytesUsed() {}
  }

在这里插入图片描述

总结一下:到这里我们分析完了FST的创建过程,本篇文章没有介绍FST的概念,只是阅读一下lucene的FST实现,FST可以实现key-value的映射关系,对内存使用非常友好,查询性能对应HashMap来说稍有下降,但同等数量的key-value数据来说HashMap使用内存比FST要大很多,当然如果内存对于用户来说是足够的,使用HashMap肯定比FST的查询速度更快,也是一个优化方向。lucene在构造term索引时就用到了FST,目的就是在有限的内存情况下缓存更多的term索引,term索引并不是全局存储在一个FST中,而是针对不同的段下不同的字段下构建多个层级的FST结构,目的就是为了加快查询索引字典速度。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值