想优化ES检索,先了解底层Lucene,Lucene源码结构一览

Lucene源码结构剖析

前言

有半年没写任何博客了,如果不是工作中还是碰到了非常多的挑战,我应该不会去花心思研究Lucene源码,果然DDL和困难才是第一生产力,没错,我想写一篇关于存储引擎的系列博客。

先前在创业公司工作的时候,就有优化Elasticsearch的需求,当时就草草地看了ES, Lucene的底层原理(Lucene是ES最基础的检索单元,一个shard也就对应一个ES实例),但仅仅浮于表面,并没有挖它源码来看,后来进入百度后, 加入垂搜团队,希望能够对通用检索引擎能有更深的理解,正所谓知己知彼,百战不殆,如果连Lucene都不了解, 又何谈打造一个更优秀的通用搜索引擎呢。

而先前我就秉持一种观点,学习一个东西,最好的方法,就是把它讲给别人听,所以就有了写Lucene源码解析的动力。同时也因为是边学边写的,所以难免因为水平不够,出现一些错误,还希望读者指出。另外还有一个重要的动机是,现在市面上讲解Lucene源码的博客也屈指可数,相关书籍的数量也是0,为数不多的书基于的lucene版本还停留在10年前;很多人的博客也大多没有很系统地讲解Lucene,只是挑了其中一两个知识点来说,另外我看了市面上大多数的Lucene源码解析文章,都会省略很多重要的地方。而我写的这系列文章,可以毫不避讳地说是所有博客里最全面和浅显易懂的。

这系列文章主要面向的读者是有ES或者Lucene使用经验的,了解Term,Doc, Field,PostingList这些基础概念的人才适合读这些,博客里并不会告诉你怎么用Lucene搭建一个搜索引擎,而是告诉你通过Lucene源码,告诉你检索引擎背后的原理。

回过头来说,这篇文章并不打算直接讲代码细节,而是给出一个大致的框架,说说我打算怎么讲解Lucene源码,以及Lucene源码的项目结构。

目录

腹稿大概是分为这12个章节,但是应该会随着实际情况做一些扩充,有些东西放在一章里面讲的不是很透彻,比如BKD Tree这些特殊的数据结构,其实是有必要拉出来单独拎出一个章节来说的。

0. 概述

也就是把整个lucene项目代码结构过一遍,弄清楚每个大类大致负责什么工作。

1. Stored Field存储方式

所谓正排就是Stored Field, 也就是保存字段属性信息的, 这里面重点关注以下几个问题: 一、各数据类型是如何存储的? 二、最终写入索引是如何压缩的?

2. Doc Value 的存储方式

这里的Doc Value就是键值对,是为了加速筛选和排序而诞生的一种结构,主要关注: 一、DocValue 的类型有哪些?SortedNumericDocValue?SortedSet?应用场景等。 二、DocValue是如何存储的?

3. Point Values存储方式

这是Lucene6版本以后,为了加速RangeQuery提出的一种数据结构,底层用BKTree来做,所以重点关注PointValue如何存储?如何优化RangeQuery的?

4. Norm Value 存储方式

5. Term Vector 存储方式

4. 倒排索引的存储方式

前面三块主要是正排,这里开始说倒排, 检索引擎最核心的部分就是倒排索引的存储,重点关注倒排索引如何压缩?如何储存?用什么数据结构?

6. 索引构建的工作流程

这章主要是说Lucene从接收到构建请求开始,到最终执行编码、存储,经过哪些流程?重点关注flush, commit, 如何利用多线程?

7. 正排索引的检索方式

8. RangeQuery检索方式

9. 倒排索引的检索方式

10. 倒排拉链的归并方式

11. 段合并的方式

12. 其他

Lucene源码项目结构

这篇博客基于的版本是Lucene7.7.3, Lucene每个大版本的迭代更新部分还是很多的,所以在研究源码的时候一定要选择一个合适的稳定版本。扒源码的主要方式除了读源码,最重要的还是打开Debug模式跟着demo跑一遍,基本就能理解个八九不离十,众所周知,开源项目的层级结构往往很深,包装、抽象的非常到位,而且里面用到了非常多业界通用的设计模式, 非常值得学习。

这里首先梳理一下Lucene项目每个目录都包含哪些类,主要用途是什么。

.
├── LucenePackage.java
├── analysis
├── codecs
├── document
├── geo
├── index
├── package-info.java
├── search
├── store
└── util

analysis

这个包主要用于对query, document 进行解析, 将其拆解为一个个的token, 这个包并不是我们研究的重点, 一个很重要的原因是, 我们往往并不希望用Lucene默认提供的分词手段,这部分往往是离线部分做的工作,每个公司都有自己的分词方式,输入往往是已经预处理好的字段,几乎用不到Lucene提供的工具,而且这部分和其他模块的耦合程度很低,所以可以跳过不讲。

codecs

编码类的包,里面囊括了对各类数据的编码、解码的定义与实现,还包括一些类似于BKD Tree跳表之类的数据结构的实现。可以算是核心类了。

简单讲讲每个类都用来干嘛的, 这里面出现了大量的抽象类,也就是只有声明,没有实现,这是一种非常优秀、可拓展的设计,开发者可以自行根据需求,基于这些抽象类来实现一些满足自己需求的类。

├── BlockTermState.java    # 记录Term在一个Block中的状态
├── Codec.java  # 这是一个抽象类,定义索引的压缩方式//todo
├── CodecUtil.java  # 用来读取version header的类
├── CompoundFormat.java  # 抽象类,定义压缩格式
├── DocValuesConsumer.java # 抽象类,声明DocValues创建、merge接口
├── DocValuesFormat.java # 抽象类, 定义Docvalue格式//todo
├── DocValuesProducer.java # 抽象类,声明DocValue读取接口
├── FieldInfosFormat.java # 抽象类,声明FieldInfo读写接口
├── FieldsConsumer.java # 抽象类,声明写入所有Fields的写接口和merge接口
├── FieldsProducer.java # 抽象类,//todo 
├── FilterCodec.java # 抽象类, 和Codec构成一种委托者模式, 这是委托者,Codec是受托者
├── LegacyDocValuesIterables.java # 废弃
├── LiveDocsFormat.java # 抽象类,这是对于live/deleted documents 读写操作的声明, todo
├── MultiLevelSkipListReader.java 跳表读取类
├── MultiLevelSkipListWriter.java 跳表写入类
├── MutablePointValues.java # 抽象类, 定义不可变的PointValues类型
├── NormsConsumer.java # 抽象类,声明写Norms信息的方法
├── NormsFormat.java # 抽象类, 定义Norm格式
├── NormsProducer.java # 抽象类,声明NormValue读取接口
├── PointsFormat.java # 抽象类, 声明Points格式定义
├── PointsReader.java  # 抽象类, 声明PointsValue读取方法
├── PointsWriter.java  #抽象类, 声明PointsValue写入方法
├── PostingsFormat.java # 抽象类,声明倒排格式
├── PostingsReaderBase.java # 抽象类, 声明Posting倒排表读取方法
├── PostingsWriterBase.java # 抽象类, 声明Posting倒排表写入方法
├── PushPostingsWriterBase.java  # 相比上面那种多了PushAPI, 是一种SAX API, 上面是DOM API
├── SegmentInfoFormat.java  # 抽象类,声明segmentInfo格式
├── StoredFieldsFormat.java # 抽象类, 声明StoredField格式
├── StoredFieldsReader.java # 抽象类,声明读取StoredField相关方法
├── StoredFieldsWriter.java # 抽象类,声明写入StoredField相关方法
├── TermStats.java  # 数据类,用于记录docFreq和termFreq
├── TermVectorsFormat.java # 抽象类, 声明TermVector相关方法
├── TermVectorsReader.java # 抽象类, 声明读取TermVector相关方法
├── TermVectorsWriter.java #抽象类, 声明写入TermVector相关方法
├── blocktree # TermDict相关编码都在这个目录下
├── compressing # StoredField和TermVector相关的抽象类的最终实现都在这里,一些压缩算法也在这里
├── lucene50 # PostingReader, PostingWriter, SkipReader, SkipWriter最终实现在这里, 
├── lucene60  # PointsReader, PointsWriter,PointsFormat最终实现在这里
├── lucene62 # SegmentInfoFormat最终实现在这里
├── lucene70 # DocValueWriter, DocValueReader, NormsConsumer, NormsProducer最终实现在这里
├── package-info.java
└── perfield # 支持单个Field格式的实现

继承图如下:

别被上面这么多类唬住了,但实际上可以大致可以分为两个维度来看:

第一个维度是数据, 其实整个Lucene把需要处理的数据分为这么几类: 1. PostingList 倒排表,也就是term->[doc1, doc3, doc5]这种倒排索引数据 2. BlockTree, 从term和PostingList的映射关系,这种映射一般都用FST这种数据结构来表示,这种数据结构其实是一种树形结构,类似于Tier树,所以Lucene这里就叫BlockTree, 其实我更习惯叫它TermDict。 3. StoredField 存进去的原始信息; 4. DocValue 键值数据,这种数据主要是用来加速对字段的排序、筛选的. 5. TermVector,词向量信息,主要记一个不同term的全局出现频率等信息。 6. Norms,用来存储Normalisation信息, 比如给某些field加权之类的。 7. PointValue 用来加速 range Query的信息。

第二个维度是行为, 也就是定义数据的Writer, Reader, Format本质上就是用于唤起Writer和Reader的一种媒介。

基于这两个维度做排列组合,就衍生出了目录下的这些类,也是很好理解的。

document

这个包主要是对一些数据类型做一些定义, 比如Field,Docuemnt, Point, DocValue等,以及它们和Int Float String之类基本类型的排列组合。

geo

关于地理信息的一些实用类

index

这里面的类非常丰富,也是我认为Lucene非常非常核心的包,包里的文件太多了,我就在这里不一一细讲了,把类继承关系图贴出来:

可以分这几个大类来看:

  1. Reader相关,具体来说包括IndexReader、LeafReader CompositeReader 等等
  2. DocValue相关,也就是SortedDocValues, SortedNumericDocValues, NumericDocValues, BinaryDocValues,这些需要跟TermsEnum, DocValuesWriter一起看会更清晰一些。
  3. MergePolicy相关,也就是段合并策略,这些类定义了何时合并?怎么合并?合并的大小等等细节。
  4. TermsEnum 相关,其实就是定义了一个terms集合类,按照字段顺序放了terms
  5. IndexDeletionPolicy相关, index删除策略。
  6. TermsHash相关, 这个类的主要作用是承担TermVectorConsumer,FreqProxTermsWriter的基类
  7. DocValuesWriter, 这个应该很熟悉了,下辖SortedDocValueWriters, BinaryDocValuesWriters, SortedSetDocValuesWriters, SortedNumericDocValuesWriters, NumericDocValuesWriters
  8. MergeScheduler相关,定义了段合并的细节,默认采用子类ConcurrentMergeScheduler,即多线程合并类,SerialMergeScheduler基本用不上,即串行去合并分段
  9. DocValuesFieldUpdates,保存一个段内,所有文档的DocValue的更新信息。
  10. 另外还有一些必要的结构体信息,比如SegmentInfo, WriterState等状态信息类,以及Term,DocWriter, IndexWriter等核心类的定义,这些以后都会涉及到

Search

和检索相关的类都在这里

1. 有一大类都是Query类的实现,Lucene面向开发者提供了各种Query,比如最简单的MatchQuery, 可以做布尔检索的BoolQuery, 支持同义词的SynoymQuery等等,这些后面会介绍 2. 另外一大类是Scorer也就是算分类, Lucene返回的排序顺序是根据算出的得分取TopN来得出的,很多Scorer都有对应的Query场景, 比如PhraseScorer就是在PhraseQuery场景下使用的。 3. Collector相关, 提供了多种收集最终结果的Collector实现, 最常用的是TotalHitCountCollector 4. 其他就是支撑以上这些类的一些工具类,比如DocIDSet,HitQueue等。 5. 有两个文件夹可以注意一下,一个是similarities,提供了一些算相似度的算法包,还有span,用于构建一些高级查询,后续再说。

Store

和最终落磁盘和读磁盘的一些类都在这里。

我认为Lucene设计的最优秀的一点就在于,它把落盘的逻辑和生成落盘数据的两个行为分开来,提供了DataInput 和DataOutput与磁盘Directory的抽象。 1. DataInput, 提供的是对数据读取的抽象,定义了如何读取vint?如何读取vlong?vfloat等。 2. DataOutput, 提供的是对数据写入的抽象,定义了如果写入vint?如何写入vlong?等等 3. Directory, 提供以何种方式写入数据的方法,比如simple, NIOFSD, mmap等。

utils

一些编码的实现和算法的实现都在这个库里面了,具体包括: 1. automation, 状态机的实现, 其实是实现了一个正则,支持正则查询必不可少的东西。 2. bkd ,bkd树的实现,为了加速range query用的 3. fst, fst的实现, fst其实目的就是为了提供term->id 的映射,但其拥有检索速度高、内存消耗少、支持前缀查询等优势,后面会说 4. graph, 为automation 提供辅助用的。 5. mutable, 不可变值的实现 6. packed, 提供编码整数的若干种方法,比如可以两个字节一编、 四个字节一编等等,后续会提。 7. 其他。

写完这章有点后悔,还是觉得正篇全部写完以后再回来写目录更加合适一点,anyway,先留个坑,后续慢慢填。

编辑于 2021-06-27 19:28

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Lucene是一个开源的全文搜索引擎工具包,它提供了丰富的API和工具,可以用于创建和管理全文索引。Lucene源码是以Java编写的,其主要目的是为了提供高效的文本搜索和索引功能。 在Lucene源码中,最核心的组件是索引和搜索。索引是指将文本数据分解为若干个文档(Document),然后对每个文档建立一种倒排索引结构。所谓倒排索引,是指通过某个关键词直接找到包含该关键词的文档,而不是通过文档去找关键词。这种倒排索引结构能够提供快速的搜索和检索功能。 Lucene源码还包含一些高级的搜索功能,例如搜索结果的排序和评分。排序是指根据某种规则,将搜索结果按相关性或其他因素进行排序,以便更好地展示给用户。评分是指根据某种算法,为搜索结果打分,以衡量其与查询的相关性。这些高级搜索功能可以根据用户的需求进行定制和扩展。 此外,Lucene源码还包含了一些辅助功能,例如分词器(Tokenizer)和过滤器(Filter)。分词器用于将输入的文本数据拆分为词组,并去除无关的符号和停用词。过滤器则用于对已经拆分的词组进行处理,例如大小写转换、同义词替换等。 总体来说,Lucene源码是非常庞大和复杂的,其中包含了大量的算法和数据结构。研究和理解Lucene源码需要对Java编程和搜索引擎原理有一定的了解。通过对源码的阅读和分析,可以更好地理解Lucene的工作机制,并能够根据自己的需求进行二次开发和定制。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值