lucene随笔-FST(Finite State Transducer)有限状态传感器

lucene版本:6.5.1

有限状态传感器,FST(Finite State Transducer)在lucene中扮演着非常重要的一个角色,在4.0后的版本lucene大量使用了这种数据结构,主要是用于在庞大的字典中快速的定位term的位置。

那么为什么使用FST呢?
考虑到这样一个场景,在lucene中倒排索引是核心,而其带来的问题就是term的字典是非常大,如何在保证term查询效率的前提下,能够尽量减少空间的使用,这便是FST所要解决的问题。相对于hashmap和treemap来说,fst在查询效率上稍逊于两者,而空间上的使用则是远优于前两者。其主要有以下几种特性:

  • 确定:同一种状态最多只能有一个转移可以访问到。
  • 无环:不可能重复遍历同一个状态。
  • 接受机:有限状态机只能接受特定的状态转移到终止状态。

FST与TRIE的关系
TRIE可以看做是一个FST,唯一的一个不同是TRIE只共享前缀,而FSA不仅共享前缀还共享后缀。
假设我们有一个这样的Set: mon,tues,thurs。FSA是这样的:
在这里插入图片描述
相应的TRIE则是这样的,只共享了前缀。
在这里插入图片描述
那么FST在lucene中是如何实现的呢?

在lucene中,FST相关的源码保存于包org.apache.lucene.util.fs中,其中builder.java内定义了相关的类以及建立和查询的过程。FST可以简单的看作是图,其中的节点我们成为状态机的状态而边则是状态机的“转移”。我们看一个简单的例子:

 public void test () {
    try {
      String inputValues[] = {"cat", "deep", "do", "dog", "dogs"};
      long outputValues[] = {5, 7, 17, 18, 21};
      PositiveIntOutputs outputs = PositiveIntOutputs.getSingleton();
      Builder<Long> builder = new Builder<Long>(FST.INPUT_TYPE.BYTE1, outputs);
      IntsRefBuilder scratchInts = new IntsRefBuilder();
      for (int i = 0; i < inputValues.length; i++) {
        builder.add(Util.toIntsRef(new BytesRef(inputValues[i]), scratchInts), outputValues[i]);
      }
      FST<Long> fst = builder.finish();
      Long value = Util.get(fst, new BytesRef("cat"));
      System.out.println(value); // 18
    } catch (Exception e) {
      ;
    }
  }

以上的示例是建立以及查询一个fst的整个过程。首先我们明确几个问题:

  1. term在fst或者在底层索引中的保存形式?
    在lucene中,term的字符串会以BytesRef的形式进行保存,也就是16进制的形式。比如说对于字符串“cat”来说的term。在底层我们发现其实是{63 61 67}。那么为什么要这样处理呢?我的理解是可能是节约空间,我们就以cat为例子:
字符串:[Vint = 3][c][a][t]
字节流:[63][61][67]
会节省一个存储空间

其中源码的主要结构为:

  • Class ByteRef:term的类型,里面包含了String到ByteRef的转换函数等。
  • Class builder: 里面包含了建立fst的主要函数 add()。
  • Class UnCompileNode:新插入的节点。
  • Class Arc :输入的“转移”条件。

我们这里一起学习以下builder.java函数中的add()函数。

 public void add(IntsRef input, T output) throws IOException {
    //这里的 input为一组16进制的数字,通常我们可以通过new BytesRef(『String』)接口获取。
    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;
    }

    // 同LastInput相比获取最长前缀字符串。
    int pos1 = 0;
    int pos2 = input.offset;
    final int pos1Stop = Math.min(lastInput.length(), input.length);
    while(true) {
      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++;
    }
    final int prefixLenPlus1 = pos1+1;
      
    // 1.新插入的节点放到frontier数组,UnCompileNode表明是新插入的,以后还可能会变化,还未放入FST内。
    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;
    }
  // 2.从prefixLenPlus1, 进行freeze冰冻操作, 添加并构建最小FST
    freezeTail(prefixLenPlus1);

    // init tail states for current input
    // 3.将当前input剩下的部分插入,构建arc转移(前缀是共用的,不用添加新的状态)。
    for(int idx=prefixLenPlus1;idx<=input.length;idx++) {
      frontier[idx-1].addArc(input.ints[input.offset + idx - 1],
                             frontier[idx]);
      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
    // 4.如果有冲突的话,重新分配output值
    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) {
       // 使用common方法,计算output的共同前缀
        commonOutputPrefix = fst.outputs.common(output, lastOutput);
        assert validOutput(commonOutputPrefix);
         // 使用subtract方法,计算重新分配的output·
        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 = 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:
      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);
  }

freezeTail(prefixLenPlus1)函数的作用

首先来说,对于新插入的UnCompileNode首先会保存到fronter数组,然后在将“静态”也就是不可改变的部分以CompileNode形式插入到fst中。
那么为什么会有部分是不变的呢?
因为输入是预先排序好的,比如插入完所有以字母“a”开始的term后,该部分就变成了静态的,因为以“b”为开始的term不会对上面的产生改变。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: lucene-core-4.9.0.jar是Lucene搜索引擎的核心库文件,它是一个用Java语言编写的开源软件,主要用于实现全文搜索和索引功能。 Lucene是一个高性能的全文搜索引擎库,它提供了一系列用于创建、更新和查询索引的API。对于需要在大量文本数据中进行快速和准确搜索的应用程序来说,Lucene是一个非常好的选择。 核心库文件lucene-core-4.9.0.jar包含了Lucene搜索引擎的所有核心功能,包括索引的创建和管理、搜索查询的执行、搜索结果的排序和过滤等。通过引入该库文件,开发人员可以在自己的应用程序中使用Lucene提供的功能来实现文本数据的全文索引和搜索。 在使用Lucene时,开发人员可以根据自己的需要,使用lucene-core-4.9.0.jar的API来创建索引,将文档进行分词、过滤和标准化处理,然后将处理后的文档添加到索引中。当需要进行搜索时,可以使用Lucene提供的查询语法和查询API,对索引中的文档进行高效的全文搜索。 除了lucene-core-4.9.0.jar之外,Lucene还提供了其他一些相关的jar文件,例如lucene-analyzers-common.jar和lucene-queryparser.jar等,它们可以用于分析和处理文本数据,以及解析查询语法。通过组合使用这些库文件,开发人员可以更灵活地构建自己的全文搜索应用程序。 总之,lucene-core-4.9.0.jar是Lucene搜索引擎的核心库文件,它提供了一系列用于创建、更新和查询文本数据索引的API,为开发人员提供了实现全文搜索功能的便利。 ### 回答2: lucene-core-4.9.0.jar是Lucene项目的核心库,它是用Java编写的全文搜索引擎库。Lucene是一个开源项目,旨在提供一个强大且高效的搜索引擎,用于构建各种应用程序,如网站搜索,文本分析和信息检索等。 lucene-core-4.9.0.jar提供了许多关键的功能和类,包括索引创建和管理,查询解析和执行,文本分析和词法处理等。通过使用这个库,开发人员可以轻松地构建自己的搜索应用程序,并实现快速和准确的搜索功能。 在Lucene的使用过程中,开发人员可以通过创建索引来处理需要搜索的文档集合。索引是一个用于快速查找和检索文档的数据结构,它包含了文档中的关键词、位置和其他重要信息。lucene-core-4.9.0.jar提供了一些类来帮助创建和维护索引,在搜索时可以快速定位相关文档。 另外,lucene-core-4.9.0.jar还提供了丰富的查询语法和API,以便开发人员可以根据特定的需求构建和执行复杂的查询。这些查询可以包括布尔逻辑、过滤器、范围查询和模糊匹配等多种形式。通过使用Lucene的查询功能,用户可以快速找到符合其需求的相关文档。 总之,lucene-core-4.9.0.jar是Lucene项目的核心库,提供了全文搜索引擎的关键功能和类。它可以帮助开发人员构建强大的搜索应用程序,实现快速和准确的搜索功能。 ### 回答3: lucene-core-4.9.0.jar 是一个用于构建搜索引擎的Java库。Lucene是一个开源的全文搜索引擎工具包,它提供了用于索引和搜索文本的功能。 lucene-core-4.9.0.jar 是Lucene库的核心组件,用于实现基本的搜索和索引功能。它包含了索引文档和搜索相关内容所需的所有必要类和方法。这个.jar文件是由Lucene 4.9.0版本的源代码编译而成的。 使用 lucene-core-4.9.0.jar,我们可以很方便地在自己的应用程序中实现搜索功能。首先,我们需要构建一个索引,将要搜索的文本内容进行索引化。然后,我们可以使用 Lucene 提供的 API 进行搜索操作,通过关键字或特定的查询语句来搜索索引中的内容。Lucene 提供了丰富的查询语法和搜索算法,可以根据需求进行高级的搜索操作,如模糊搜索、排序、过滤等。 通过使用 lucene-core-4.9.0.jar,我们可以为我们的应用程序添加强大的搜索能力,无论是搜索文件、数据库、网站内容或是其他结构化的数据。Lucene 库已经在众多应用中得到了广泛的应用,包括网站搜索引擎、文档管理系统、电子邮件客户端等。 总之,lucene-core-4.9.0.jar 是一个功能强大且灵活的Java库,可以帮助开发人员快速构建搜索引擎和实现全文搜索功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值