lucene的高效数据查询

时间:2022-07-22
本文章向大家介绍lucene的高效数据查询,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

lucene是一个常用的并发处理时的全局搜索引擎,它和目前搜索引擎采取的处理大量查询数据的思路都是,事先把跟关键词相匹配数据存储起来,查找的时候直接把存储好的数据进行返回。

lucene也对内部的数据结构和算法进行优化,著名的有内嵌FST数据结构,在索引生成方面的应用。LZ4的实时压缩算法。

lucene对基本数据结构压缩优化

  1. 普通的 Int 和 Long 存储一个整数,必须用 32 位和 64 位,哪怕该整数的值为 1 。这样 就带来了存储空间的浪费。

首先lucene在进行存储时,文档id,词频等一定是非负整数。

Vint: 由不超过 4 个 Byte 组成,Byte 的最高位表示是否需要再读取一个 Byte,剩下的 7 位存 储数值。这样当数值范围在[0,127]之间用一个 Byte 就够了,数值范围在[128, 16383]之间用 两个 Byte 就够了,有效节约了存储空间。

VLong: 由不超过 8 个 Byte 组成,存储方式与 VInt 相同。

String: 存储一个 VInt 表示字符串的长度,然后用 UTF8 编码存储字符串。

  1. Zigzag 编码主要在于对负数的压缩,比如-1(1111 1111 1111 1111 1111 1111 1111 1111), 经过转码后,变成了 1(0000 0000 0000 0000 0000 0000 0000 0001),节约了很多符号位。
  2. Lucene 中用到的一项技术就是位压缩(bit-packing).这意味着整型数组的类型从固定大小 (8,16,32,64 位)4 种类型,扩展到了[1-64]位共 64 种类型。这样的话,在lucene中的整型数组实际上变为了变长。
FST数据结构
FST本质上是一种有限状态自动机。FST在 Lucene 中的应用多以 FST<Key,Value>的形式出现,其功能与 Map 类似,支持用 Key 来查询 Value;同时 FST 也支持用 Value 来查找最优 Key,这是 Map 做不到的。

其复杂度为O(len(key)),空间利用率高,普通有状态机,如 Trie 树就已经能够实现 O(len(Key))的时间复杂度,然而空间利用率却 低得可怜。

FST 正 是一个最小的、有向的、无环的最小自动机。

但是FST方法有一个局限条件:为了保证最小自动机,给定的 List 必须是有序的。

假设有{w1,w2....,wn} n 个有序的字符串集。 a、先构造一个除 w1 外,最小的 FST。(此时 FST 中有 w1 一个字符串) b、构造一个除 w2 外,最小的 FST。(此时 FST 中有 w1,w2 两个字符串) c、构造一个除 w3 外,最小的 FST。(此时 FST 中有 w1,w2,w3 三个字符串)其实就是一种迭代的思想,通过对字符串后缀和前缀的重复利用以实现状态的压缩, 这也是为什么需要一个排序的 List。

FST实现类map查询

首先我们来看看常用的map字典效率

数据结构

优缺点

排序列表Array/List

使用二分法查找,不平衡

HashMap/TreeMap

性能高,内存消耗大,几乎是原始数据的三倍

Skip List

跳跃表,可快速查找词语,相对于TreeMap等结构,特别适合高并发场景

Trie树

适合英文词典,如果系统中存在大量字符串基本没有公共前缀,则相应的trie树将非常消耗内存

 public static void main(String[] args) throws IOException {
        String inputValues[] = {"cat","deep","do","dog","dogs"};
        Long outputValues[] = {5L,7L,17L,18L,21L};

        //lucene中的int输出
        PositiveIntOutputs outputs = PositiveIntOutputs.getSingleton();
        //构建FST,并配置输入类型,按byte优化数据存储大小,以及数据的输出类型
        Builder<Long> builder = new Builder<Long>(FST.INPUT_TYPE.BYTE1,outputs);
        IntsRefBuilder scratchInts = new IntsRefBuilder();

        for (int i = 0; i < inputValues.length; i++) {
            BytesRef scratchBytes =  new BytesRef(inputValues[i].getBytes());
            scratchInts.copyUTF8Bytes(scratchBytes);
            //build添加的key只能为int
            builder.add(scratchInts.toIntsRef(),outputValues[i]);
        }

        FST<Long> fst = builder.finish();
        Long value = Util.get(fst, new BytesRef("dog"));
        System.out.println(value); // 18
    }

FST压缩率一般在3倍~20倍之间,相对于TreeMap/HashMap的膨胀3倍,内存节省就有9倍到60倍!

我们看到input是经过排序的,也就是ordered。否则生成的就不是最小的FST。另外如果NO_OUTPUT就退化为FSA了,不用执行第4步重新分配output了。

其中freezeTail 方法就是将不再变化的部分进行冰冻,又叫compile,把UnCompileNode,给构建进FST里。进入到FST是先进行compileNode, 然后addNode进去的。

总结以下,加入节点过程:

1)新插入input放入frontier,这里还没有加入FST 2)依据当前input, 对上次插入数据进行freezeTail操作, 放入FST内 3)构建input的转移(Arc)关系 4)解决Output冲突,重新分配output,保证路径统一(NO_OUTPUT,不执行) 最后在finish方法里,执行freezeTail(0), 把所有的input构建进FST内。

FST,不但能共享前缀还能共享后缀。不但能判断查找的key是否存在,还能给出响应的输入output。 它在时间复杂度和空间复杂度上都做了最大程度的优化,使得Lucene能够将Term Dictionary完全加载到内存,快速的定位Term找到响应的output(posting倒排列表)。