lucene学习-FST源码分析

Lucene源码分析 - FST-Builder 分享一下Builder类的源码解析 阅读详情

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结构,目的就是为了加快查询索引字典速度。

lucene5.1 fst源码分析(BooleanQuery分析) 1 测试代码 1-1 写入代码 1-2 读取代码 2 源码分析 2-1createweight 2-1-1 TermQuery构造weight 2-1-2 MultiTermQueryConstantScoreWrapper构造weight 2-2 构造scorer 2-3 求交集并集差集.1 测试代码.1-1 写入代码public static void writeIndex() throws I 阅读详情

相关推荐

Lucene源码分析 - BlockTreeTermsWriter 存储词典(Term Dictionary)与索引(Term Index)

本文的代码以lucene-core 6.3.0为准,包含BlockTreeTermsWriter的bufferSkip函数,writeSkip函数等整个类所有代码的解析。转载请注明出处。 0 基本信息    BlockTreeTermsWriter类将倒排索引存到磁盘,先了解下这个类的基本信息: 1 源码分析    write函数将segment中的每个非IndexOptions....

zoov的专栏 2643

Lucene FST

Lucene FST 1. FST简介 FST, 全称Finite State Transducer, 中文翻译: 有限状态转换器或有限状态传感器。 FST最重要的功能是可以实现Key到Value的映射,相当于HashMap&amp;amp;lt;Key,Value&amp;amp;gt;。FST的内存消耗要比HashMap少很多, 但FST的查询速度比HashMap要慢。FST在Lucene中被大量使用,例如:倒排索引的存储,...

zx的博客 8273

lucene4.5源码分析系列:lucene概述

lucene索引格式:http://www.cnblogs.com/forfuture1978/archive/2009/12/14/1623597.html

笨狐狸 1万+

关于Lucene的词典FST深入剖析

搜索引擎为什么能查询速度那么快? 核心是在于如何快速的依据查询词快速的查找到所有的相关文档,这也是倒排索引(Inverted Index)的核心思想。那么如何设计一个快速的(常量,或者1)定位词典的数据结构就显得尤其重要。简单来说,我们可以采用HashMap, TRIE, Binary Search Tree, Tenary Search Tree等各种数据结构来实现。 那么开源的搜索引擎包Lucene是怎么来设计的呢?Lucene采用了一种称为FST(Finite State Transducer)的结构

Koikoi12的博客 1771

Lucene中的FST算法:高效构建termindex

Lucene中的FST算法:高效构建termindex 作者:禅与计算机程序设计艺术 1. 背景介绍 1.1 搜索引擎中的索引构建 在搜索引擎中,索引扮演着至关重要的角色。高效的索引构建和查询是搜索引

AI天才研究院 872

【Lucene】Lucene 10.0 相对于 9.x 版本有哪些重大的架构或性能改进?(例如硬件效率提升)

摘要: Apache Lucene 10.0针对现代硬件特性进行了深度优化,显著提升搜索性能。主要改进包括: SIMD向量化加速:通过AVX2/AVX-512指令集优化BKD Tree范围查询(如商品价格区间搜索),性能提升2-3倍; FST压缩优化:重构词典存储结构,加载速度提升15%-20%,内存占用降低5%,尤其利好高基数字段; 并行搜索增强:改进多核CPU利用率,解决高并发查询时的吞吐量瓶颈。电商场景测试显示,数值查询P99延迟显著下降,适用于亿级商品库的实时检索需求。这些改进使Lucene 10.

九师兄 143

JAVA面试小结三十八:lucene字典实现原理——FST

利用该结构可以很方便的进行查询,如给定一个term “dog”,我们可以通过上述结构很方便的查询存不存在,甚至我们在构建过程中可以将单词与某一数字、单词进行关联,从而实现key-value的映射。我们对“cat”、 “deep”、 “do”、 “dog” 、“dogs”这5个单词进行插入构建FST(注:必须已排序)。与前一个单词“deep”进行最大前缀匹配,发现是d,则在d边后增加新边o,o边指向终点。与前一个单词“do”进行最大前缀匹配,发现是do,则在o边后增加新边g,g边指向终点。

之乎者也·的博客 369

Lucene 源码解析

FileReaderAll函数用来从文件中读取字符串,默认编码为“GBK”。在创建完最重要的IndexWriter之后,就开始遍历需要索引的文件,构造对应的Document和Filed类,最终通过IndexWriter的addDocument函数开始索引。 Document的构造函数为空,StringField、TextField和Field的构造函数也很简单。下面重点分析IndexWriter的addDocument函数,代码如下

Lucene源码分析 - FST

lucene的FST的二进制存储和查询方式都是由FST这个类来实现。如果要读这些代码,首先需要了解这个类里面的一些基本的东西。

zoov的专栏 4896

【scp】Windows 下scp报错 bad permissions

Windows 下使用 SSH 连接报 Bad owner or permissions on C:\\Users\\Administrator/.ssh/config 错误问题解决 C:\Users\Administrator>scp -P 66666 D:\1111.pdf zzd@58.213.45.xx:/home/zzd/ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING:...

走在搬砖的路上! 4154

JSP期末考试复习习题及答案

以下习题是对于平时课堂小测的整理,如有错误敬请指正! 欢迎一起交流~ 预祝各位小伙伴们考试顺利! Test1 一. 单选题 – 1.(单选题)当用户请求jsp页面时,JSP引擎就会执行该页面的字节码文件响应客户的请求,执行字节码文件的结果是() A. 发送一个JSP源文件到客户端 B. 发送一个Java文件到客户端 C. 发送一个HTML页面到客户端 D. 什么都不做 – 2.(单选题)下列动态网页和静态网页的根本区别描述错误的是( D) A. 静态网页服务器端返回的HTML文件是事先存储好的 B. 动.

技术小白 2万+

lucene5.1 fst源码分析(fst接口方法写入和读取测试)

1 fst基本概念 1-1 节点node和弧arc 2 lucene中的fst测试代码 3 源码分析 3-1 util的get方法分析 3-2 FST的findTargetArc方法 3-2-1 类成员变量介绍 3-2-2方法原型 3-2-3源码分析 3-3 readFirstRealTargetArc方法 3-3 readNextRealArc方法.1 fst基本概念有限状态机.1-1 节点(no

sinat_27545249的博客 2600

(2)NPW(监控片)【中】---业务场景精讲

自己在日常工作当中,由于非产线人员出生,对NPW的知识一直一知半解,业务场景认识的不够透彻,做系统的时候很难理解甲方的NPW业务应用,特收集了不少关于NPW的一些业务场景,下面都是一些摘抄,大家关注我标红标粗的部分,

阿拉伯梳子的专栏 1586

转一篇不错的介绍Lucene4 FST的文章

http://download.youkuaiyun.com/download/guanxinquan/7380591 http://blog.sina.com.cn/s/blog_616e189f0101fxxq.html

jollyjumper的专栏 3278

【数据集4】GSWP3数据集详解

简介GSWP3数据集

WW、forever的博客 1366

Figma做原型设计一篇教会你,全栈软件开发必备技能,前后端岗位锐减找工作的底牌

本文介绍了使用Figma进行前端应用原型设计的方法与优势。Figma作为主流设计工具,能够满足大多数应用系统的原型设计需求,强调系统设计应保持简洁合理。文章详细说明了Figma的基本操作流程,包括创建Frame页面、组件使用、交互设置等核心功能,并指出命名规范的重要性。同时对比了Axure、墨刀等其他设计工具的特点,建议根据具体需求选择合适的平台。最后强调原型设计主要关注交互功能展示,业务逻辑应通过需求文档表述。整体而言,Figma凭借其简洁高效的特点,已成为原型设计的优选工具。

有问题或资源连接有缺失的可以私信公众号新潮看世界获取 458

lucene5.1 fst源码分析(嵌入到lucene中的写入过程)

1 说明 2 写入数据 2-1 调用栈 2-2 源码分析 2-2-1 write方法 2-2-2 BlockTreeTermWriter的write方法 2-2-3 BlockTreeTermWriter的pushterm方法 2-2-3 BlockTreeTermWriter的writerblocks方法还没添加注释 2-2-5 BlockTreeTermWriter的writeblock方法.1

sinat_27545249的博客 1648

Mac 安装neo4j(解压版)最新版本教程

本文介绍了在Mac M系列操作系统上安装Neo4j图数据库的具体步骤。内容包括:从官网下载解压版Neo4j社区版压缩包;通过终端命令将其解压到/opt目录并重命名;配置环境变量以便全局访问;以及常用命令如启动/停止服务、重置密码等。安装完成后,用户可通过浏览器访问http://localhost:7474,使用默认账号neo4j/neo4j登录并根据提示重置密码。该教程简洁明了地指导用户完成Neo4j在Mac上的安装配置过程。

猿小白的博客 906

Lucene 7.5.0 FST算法

FST(Finite State Transducer)算法的概念在这篇博客中并不涉及,网上有太多的资料啦,写的都非常的不错。这里推荐这位网友的介绍:https://www.shenyanchao.cn/blog/2018/12/04/lucene-fst/ 。如果链接失效了,可以看附件中的副本。本文中,我们基于一个例子来介绍在Lucene中如何实现FST算法及应用,感谢网友 关新全 的分享,基于...

q364367207的专栏 764

【数据集】基于ubESTARFM法的100m 地温LST数据集(澳大利亚)

【数据集】基于ubESTARFM法的100m 地温LST数据集(澳大利亚)

WW、forever的博客 264

FreeRTOS全攻略:从入门到精通

本文聚焦 FreeRTOS,从基础概念出发,阐述其开源、轻量级等优势。着重介绍 FreeRTOS 入门知识,涵盖内存管理、任务操作、同步互斥机制等核心内容。其中,空闲任务与钩子函数保障系统基础运行与功能扩展;队列、信号量、互斥锁和事件组等机制,分别从数据传递、资源访问控制、优先级问题解决及多任务同步等方面,为开发者提供高效的任务间协作与资源管理方案,助力开发者掌握 FreeRTOS 核心技术,为嵌入式项目开发筑牢基础。

邓邓子的博客 6354

Lucene源码分析 - 倒排索引之索引表

概要 前文已经介绍过什么是倒排索引了,本文将对倒排索引的结构深入进行理解,见下图 整个倒排索引其实分为两部分,左边是Term Dictionary,右边是Postings List,例如对以下内容建立倒排索引,结构较为贴近HashMap>,可类比设想Map的key为Term,value为Postings ...

RovisuKi的博客 1411

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

Lucene 源码分析

Lucene源码项目结构 Lucene项目每个目录包含的类以及用途: 文章目录Lucene源码项目结构Analysiscodecs二、使用步骤1.引入库2.读入数据总结 Analysis 主要用于对query,document的解析,拆解为token codecs 示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。 二、使用步骤 1.引入库 代码如下(示例): import numpy as np import pandas as pd import matpl

qq_47537678的博客 1514

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

Lucene源码分析 - Lucene50PostingsFormat 倒排索引简介

本文的代码以lucene-core 6.3.0为准,倒排索引的基本信息和Lucene50PostingsFormat 等整个类所有代码的解析。转载请注明出处。    在lucene中,有相当多的时候需要存正整数的数组,当这个数组比较大的时候,就把它分一个个的block,每个block包含固定数量的整数,然后把每个block用PackedInts来压缩,为了达到压缩的效果,每个元素都存它本身...

zoov的专栏 903

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

lucene源码分析---9

lucene源码分析---倒排索引的写过程

conansonic的博客 6242

Lucene查找过程源码分析

Lucene查找过程源码分析

花开的博客 729
上一篇: elasticsearch源码分析-08Serch查询流程
下一篇: 记一次knife4j文档请求异常 SyntaxError: Unexpected token ‘<‘, ... is not valid JSON
胖柯G
博客等级 码龄10年 453粉丝 57原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值