前言
有半年没写任何博客了,如果不是工作中还是碰到了非常多的挑战,我应该不会去花心思研究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把需要处理的数据分为这么几类:
- PostingList 倒排表,也就是term->[doc1, doc3, doc5]这种倒排索引数据
- BlockTree, 从term和PostingList的映射关系,这种映射一般都用FST这种数据结构来表示,这种数据结构其实是一种树形结构,类似于Tier树,所以Lucene这里就叫BlockTree, 其实我更习惯叫它TermDict。
- StoredField 存进去的原始信息;
- DocValue 键值数据,这种数据主要是用来加速对字段的排序、筛选的.
- TermVector,词向量信息,主要记一个不同term的全局出现频率等信息。
- Norms,用来存储Normalisation信息, 比如给某些field加权之类的。
- PointValue 用来加速 range Query的信息。
第二个维度是行为, 也就是定义数据的Writer, Reader, Format本质上就是用于唤起Writer和Reader的一种媒介。
基于这两个维度做排列组合,就衍生出了目录下的这些类,也是很好理解的。
document
这个包主要是对一些数据类型做一些定义, 比如Field,Docuemnt, Point, DocValue等,以及它们和Int Float String之类基本类型的排列组合。
geo
关于地理信息的一些实用类
index
这里面的类非常丰富,也是我认为Lucene非常非常核心的包,包里的文件太多了,我就在这里不一一细讲了,把类继承关系图贴出来:
可以分这几个大类来看:
- Reader相关,具体来说包括IndexReader、LeafReader CompositeReader 等等
- DocValue相关,也就是SortedDocValues, SortedNumericDocValues, NumericDocValues, BinaryDocValues,这些需要跟TermsEnum, DocValuesWriter一起看会更清晰一些。
- MergePolicy相关,也就是段合并策略,这些类定义了何时合并?怎么合并?合并的大小等等细节。
- TermsEnum 相关,其实就是定义了一个terms集合类,按照字段顺序放了terms
- IndexDeletionPolicy相关, index删除策略。
- TermsHash相关, 这个类的主要作用是承担TermVectorConsumer,FreqProxTermsWriter的基类
- DocValuesWriter, 这个应该很熟悉了,下辖SortedDocValueWriters, BinaryDocValuesWriters, SortedSetDocValuesWriters, SortedNumericDocValuesWriters, NumericDocValuesWriters
- MergeScheduler相关,定义了段合并的细节,默认采用子类ConcurrentMergeScheduler,即多线程合并类,SerialMergeScheduler基本用不上,即串行去合并分段
- DocValuesFieldUpdates,保存一个段内,所有文档的DocValue的更新信息。
- 另外还有一些必要的结构体信息,比如SegmentInfo, WriterState等状态信息类,以及Term,DocWriter, IndexWriter等核心类的定义,这些以后都会涉及到
Search
和检索相关的类都在这里
- 有一大类都是Query类的实现,Lucene面向开发者提供了各种Query,比如最简单的MatchQuery, 可以做布尔检索的BoolQuery, 支持同义词的SynoymQuery等等,这些后面会介绍
- 另外一大类是Scorer也就是算分类, Lucene返回的排序顺序是根据算出的得分取TopN来得出的,很多Scorer都有对应的Query场景, 比如PhraseScorer就是在PhraseQuery场景下使用的。
- Collector相关, 提供了多种收集最终结果的Collector实现, 最常用的是TotalHitCountCollector
- 其他就是支撑以上这些类的一些工具类,比如DocIDSet,HitQueue等。
- 有两个文件夹可以注意一下,一个是similarities,提供了一些算相似度的算法包,还有span,用于构建一些高级查询,后续再说。
Store
和最终落磁盘和读磁盘的一些类都在这里。 我认为Lucene设计的最优秀的一点就在于,它把落盘的逻辑和生成落盘数据的两个行为分开来,提供了DataInput 和DataOutput与磁盘Directory的抽象。
- DataInput, 提供的是对数据读取的抽象,定义了如何读取vint?如何读取vlong?vfloat等。
- DataOutput, 提供的是对数据写入的抽象,定义了如果写入vint?如何写入vlong?等等
- Directory, 提供以何种方式写入数据的方法,比如simple, NIOFSD, mmap等。
utils
一些编码的实现和算法的实现都在这个库里面了,具体包括:
- automation, 状态机的实现, 其实是实现了一个正则,支持正则查询必不可少的东西。
- bkd ,bkd树的实现,为了加速range query用的
- fst, fst的实现, fst其实目的就是为了提供term->id 的映射,但其拥有检索速度高、内存消耗少、支持前缀查询等优势,后面会说
- graph, 为automation 提供辅助用的。
- mutable, 不可变值的实现
- packed, 提供编码整数的若干种方法,比如可以两个字节一编、 四个字节一编等等,后续会提。
- 其他。
写完这章有点后悔,还是觉得正篇全部写完以后再回来写目录更加合适一点,anyway,先留个坑,后续慢慢填。