热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

lucene查询原理

lucene查询原理1.lucene数据模型2.lucene查询过程3.SkipList哨兵数组skipDocdocDeltaBufferLucene中使用读取跳表SkipList

lucene查询原理

    • 1. lucene 数据模型
    • 2. lucene 查询过程
    • 3. SkipList
      • 哨兵数组 skipDoc
      • docDeltaBuffer
      • Lucene中使用读取跳表SkipList的过程
      • 跳表查询演示
        • 文档号:23
        • 文档号:700
    • 4. 倒排合并

前言:
  lucene 是一个基于java实现的全文信息检索工具包,目前主流的搜索系统 Elasticsearch 和 solr 都是基于 lucene 检索功能实现的。想要理解搜索系统的实现原理,就需要深入 lucene 这一层,看看 lucene 是如何存储需要检索的数据,以及如何完成高效的数据检索。

1. lucene 数据模型

  lucene 中包含四种基本的数据类型:

  • index:索引,由很多的 Document 组成。相当于 mysql 的数据库
  • Document:由很多的 Field 组成。相当于 mysql 的一行数据
  • Field:由 Field Name 和 Field Value 组成。相当于 mysql 的字段和字段值
  • Term:由很多的字节组成。一般将Text类型的 Field Value 分词之后的每个最小单元叫做 Term。

2. lucene 查询过程

  在 lucene 中查询是基于 segment。每个 segment 可以看做是一个独立的 subindex,在建立索引的过程中,lucene会不断的 flush 内存中的数据持久化形成新的 segment。多个 segment也会不断的被 merge 成一个大的 segment,在老的 segment 还有查询在读取的时候,不会被删除,没有被读取且被 merge 的 segement 会被删除。这个过程类似于 LSM 数据库的 merge 过程。下面我们主要看在一个 segment 内部如何实现高效的查询。
  为了方便大家理解,我们以人名字,年龄,学号为例,如何实现查某个名字(有重名)的列表。

docididnameage
1101Alice18
2102Alice20
3103Alice21
4104Alan21
5105Alan18

  每个 document 的 name value 经过分词形成多个 term,多个 term 组成 term dictionary,如下显示基于 name 创建倒排链如下:

在这里插入图片描述

  在这里,Alice、Alan 都是 term。所以倒排本质上就是基于 term 的反向列表,方便通过属性反向查找 doc_id。
  到这里我们有个很自然的问题,如何快速拿到这个倒排表呢?在 lucene 里面就引入了 term dictonary 的概念,也就是 term 的字典。term 字典里我们可以按照 term 进行排序,那么用一个二分查找就可以找到这个 term 所对应的倒排表信息,这样的复杂度是 logN。
  但在 term 很多,内存放不下的时候,效率还是需要进一步提升。 为解决这个问题,lucene 引用 term dict index 来存放 term 的共享前缀,后缀则放在 term dictonary 中。

  • term dict index:是采用 FST 数据结构,存放在内存中。term dict index 查到对应的 term dict 的 block 位置之后,再去磁盘上找 term,大大减少了磁盘的 random access 次数
  • Term dict:在磁盘上是以分 block 的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去
  • SkipList: 是以 docId 建立的数据结构

  如果所有的 term 都是英文字符的话,可能这个 term index 就真的是 26 个英文字符表构成的了。但是实际的情况是,term 未必都是英文字符,term 可以是任意的 byte 数组。

  图示如下:。

在这里插入图片描述

  在 term dict index 中需要保存的是 Term 的前面部分字段,以及与 Term Dictionary 之间的映射关系,这使得存储的信息量减少。再结合 FST(Finite State Transducer)压缩技术,term dict index 可以被压缩到足够小,以至于可以被缓存进服务器内存中,而 Term 后缀则存放在 trem dict。
  整个检索过程如下:

  • 内存加载 term dict index(tip文件),通过 FST 匹配前缀找到后缀词块位置。
  • 根据词块位置,读取磁盘中 Term dict(tim 文件),匹配后缀,读取倒排表信息。
  • 根据倒排表位置去 doc文件 中加载文档数据。

  找到了倒排表,怎么通过 docId 找到我们需要的文档呢?

3. SkipList

  为了能够快速查找 docid 对应的 document,lucene 采用了SkipList 这一数据结构。SkipList 有以下几个特征:

  • skipInterval:该值描述了在 level=0 层,每处理 N 篇 document 文档,就生成一个 skipDatum,该值默认为 128。
  • skipMultiplier:该值描述了所有层,每处理 N 个skipDatum,就在上一层生成一个新的 skipDatum,该值默认为 8
  • numberOfSkipLevels:skipList中的层数

  直接给出一个生成后的跳表:

在这里插入图片描述

  文档号0~3455的3456篇文档,我们令 skipInterval 为128,即每处理128篇文档生成一个 skipDatum 和对应的 PackedBlock;令 skipMultiplier 为3(源码中默认值为8),即每生成3个 skipDatum 就在上一层生成一个新的 skipDatum 索引,它是 3个 skipDatum 中的最后一个。对于 level > 0 的 skipDatum 都有一个索引值 SkipChildLevelPointer 字段来指向下一层对应的 skipDatum ,例如 level=1 的第一个 skipDatum(383),SkipChildLevelPointer 值就是 127、255、383。

哨兵数组 skipDoc

  哨兵数组skipDoc的定义如下所示:

int[] skipDoc;

  该数组用来描述每一层中正在处理的 skipDatum ,skipDatum 对应的 PackedBlock 中的最后一篇文档的文档号作为哨兵值添加到哨兵数组中,在初始化阶段,skipDoc 数组中的数组元素如下所示:

int[] skipDoc = {127, 383, 1151, 3455}

  初始化阶段将每一层的第一个 skipDatum 对应的 PackedBlock 中的最后一篇文档的文档号作为哨兵值。

docDeltaBuffer

  docDeltaBuffer 是一个int类型数组,上文我知道一个 skipDatum 可以映射128篇文档,而 docDeltaBuffer 记录了一个 skipDatum 中所有的文档号。

Lucene中使用读取跳表SkipList的过程

  读取过程分为下面三步:

  • 步骤一:获得需要更新哨兵值的层数N
    • 从 skipDoc 数组的第一个哨兵值开始,依次与待处理的文档号比较,找出所有比待处理的文档号小的层
  • 步骤二:从 N 层开始依次更新每一层在 skipDoc 数组中的哨兵值
    • 如果待处理的文档号大于当前层的哨兵值,那么令当前层的下一个 skipDatum 对应的 PackedBlock 中的最后一篇文档的文档号作为新的哨兵值,直到待处理的文档号小于当前层的哨兵值
    • 在处理 level=0 时,更新后的 skipDatum 对应的 PackedBlock 中的文档集合更新到 docDeltaBuffer 中
  • 步骤三:遍历 docDeltaBuffer 数组
    • 取出 PackedBlock 中的所有文档号到 docDeltaBuffer 数组中,依次与待处理的文档号作比较,判断 SkipList 中是否存在该文档号

跳表查询演示

  我们依次处理下面的文档号,判断是否在跳表 SKipList 中来了解读取过程:

文档号:{23, 700}

文档号:23

  更新前的skipDoc数组如下所示:

int[] skipDoc = {127, 383, 1151, 3455}

  当前 skipDoc 数组对应的 SkipList 如下所示:

在这里插入图片描述

  由于文档号 23 小于 skipDoc 数组中的所有哨兵值,故不需要更新 skipDoc 数组中的哨兵值,那么直接遍历 docDeltaBuffer,判断文档号23 是否在该数组中即可。

文档号:700

  更新前的skipDoc数组如下所示:

int[] skipDoc = {127, 383, 1151, 3455}

  文档号700小于1151、3455,执行了步骤一之后,判断出需要更新哨兵值的层数N为2(两层),即只要从level=1开始更新level=1以及level=0对应的skipDoc数组中的哨兵值。

  对于 level=1 层,在执行了步骤二后,更新后的skipDoc数组如下所示:

int[] skipDoc = {127, 767, 1151, 3455}

在这里插入图片描述

  随后更新 level=0 层,在执行了步骤二后,更新后的 skipDoc 数组如下所示:

int[] skipDoc = {767, 767, 1151, 3455}

在这里插入图片描述

  这里要注意的是,level=0 层的 skipDatum 更新过程如下所示:

在这里插入图片描述

  在更新的过程中,跳过了两个 skipDatum ,其原因是在图3中,当更新完 level=1 的 skipDatum 之后,该 skipDatum 通过它包含的SkipChildLevelPointer字段,重新设置在 level=0 层的哨兵值为 511,随后在处理 level=0 时,继续往下遍历 skipDatum 找到 767 > 700,并更新 skipDoc ,然后通过 skipDatum(767 )对应的 PackedBlock 遍历看里面是否有 doc_id = 700,有的话返回读取 doc_id = 700 的文档数据。

为啥使用跳表而不是Hash?

  通过区间来查找数据这个操作,跳表天然有序的数据结构效率比 hash 高的多

4. 倒排合并

  到这里还有另一个问题,那就是当我们查询中出现了 name=‘Alan’ and name=‘Alice’ limit 0,20 该如何处理?

  它会拆分为 term = ‘Alan’ 和 term = 'Alice’的查询。分别得到每个term的倒排链(docId列表),再对多个倒排表做交集。那么倒排表怎么做交集呢?

水平分桶,多线程并行

有序集合1{1,3,5,7,8,9, 10,30,50,70,80,90}和有序集合2{2,3,4,5,6,7, 20,30,40,50,60,70},先进行分桶拆分【桶1的范围为[1, 9]、桶2的范围为[10, 100]、桶3的范围为[101, max_int]】,于是拆分成集合1【a={1,3,5,7,8,9}、b={10,30,50,70,80,90}、c={}】和集合2【d={2,3,4,5,6,7}、e={20,30,40,50,60,70}、e={}】,利用多线程对桶1(a和d)、桶2(b和e)、桶3(c和e)分别求交集,最后求并集。


bitmap,大大提高运算并行度,时间复杂度O(n)

假设set1{1,3,5,7,8,9}和set2{2,3,4,5,6,7}的所有元素都在桶值[1, 16]的范围之内,可以用16个bit来描述这两个集合,原集合中的元素x,在这个16bitmap中的第x个bit为1,此时两个bitmap求交集,只需要将两个bitmap进行“与”操作,结果集bitmap的3,5,7位是1,表明原集合的交集为{3,5,7}

水平分桶(每个桶内的数据一定处于一个范围之内),使用bitmap来表示集合,能极大提高求交集的效率,但时间复杂度仍旧是O(n),但bitmap需要大量连续空间,占用内存较大。


推荐阅读
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • OpenMap教程4 – 图层概述
    本文介绍了OpenMap教程4中关于地图图层的内容,包括将ShapeLayer添加到MapBean中的方法,OpenMap支持的图层类型以及使用BufferedLayer创建图像的MapBean。此外,还介绍了Layer背景标志的作用和OMGraphicHandlerLayer的基础层类。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 2018深入java目标计划及学习内容
    本文介绍了作者在2018年的深入java目标计划,包括学习计划和工作中要用到的内容。作者计划学习的内容包括kafka、zookeeper、hbase、hdoop、spark、elasticsearch、solr、spring cloud、mysql、mybatis等。其中,作者对jvm的学习有一定了解,并计划通读《jvm》一书。此外,作者还提到了《HotSpot实战》和《高性能MySQL》等书籍。 ... [详细]
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 微信官方授权及获取OpenId的方法,服务器通过SpringBoot实现
    主要步骤:前端获取到code(wx.login),传入服务器服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId ... [详细]
  • 本文总结和分析了JDK核心源码(2)中lang包下的基础知识,包括常用的对象类型包和异常类型包。在对象类型包中,介绍了Object类、String类、StringBuilder类、StringBuffer类和基本元素的包装类。在异常类型包中,介绍了Throwable类、Error类型和Exception类型。这些基础知识对于理解和使用JDK核心源码具有重要意义。 ... [详细]
  • 使用freemaker生成Java代码的步骤及示例代码
    本文介绍了使用freemaker这个jar包生成Java代码的步骤,通过提前编辑好的模板,可以避免写重复代码。首先需要在springboot的pom.xml文件中加入freemaker的依赖包。然后编写模板,定义要生成的Java类的属性和方法。最后编写生成代码的类,通过加载模板文件和数据模型,生成Java代码文件。本文提供了示例代码,并展示了文件目录结构。 ... [详细]
author-avatar
idc01
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有