Lucene解析 - 基本概念

前言

Apache Lucene是一个开源的高性能、可扩展的信息检索引擎,提供了强大的数据检索能力。Lucene已经发展了不少年,其功能愈来愈强大,架构也愈来愈精细。它目前不只仅能支持全文索引,也可以提供多种其余类型的索引方式,来知足不一样类型的查询需求。数据库

基于Lucene的开源项目有不少,最知名的要属Elasticsearch和Solr,若是说Elasticsearch和Solr是一辆设计精美、性能卓越的跑车,那Lucene就是为其提供强大动力的引擎。为了驾驭这辆跑车让它跑的更快更稳定,咱们须要对它的引擎研究透彻。数据结构

在此以前咱们在专栏已经发表了多篇文章来剖析Elasticsearch的数据模型、读写路径、分布式架构以及Data/Meta一致性等问题,这篇文章以后咱们会陆续发表一系列的关于Lucene的原理和源码解读,来全面解析Lucene的数据模型和数据读写路径。架构

Lucene官方对本身的优点总结为几点:
Scalable, High-Performance Indexing
Powerful, Accurate and Efficient Search Algorithms
但愿经过咱们的系列文章,可以让读者理解Lucene是如何达到这些目标的。elasticsearch

整个分析会基于Lucene 7.2.1版本,在读这篇文章以前,须要有必定的知识基础,例如了解基本的搜索和索引原理,知道什么是倒排、分词、相关性等基本概念,了解Lucene的基本使用,例如Directory、IndexWriter、IndexSearcher等。分布式

基本概念

在深刻解读Lucene以前,先了解下Lucene的几个基本概念,以及这几个概念背后隐藏的一些东西。性能

clipboard.png

如图是一个Index内的基本组成,Segment内数据只是一个抽象表示,不表明其内部真实数据结构。优化

Index(索引)
相似数据库的表的概念,可是与传统表的概念会有很大的不一样。传统关系型数据库或者NoSQL数据库的表,在建立时至少要定义表的Scheme,定义表的主键或列等,会有一些明肯定义的约束。而Lucene的Index,则彻底没有约束。Lucene的Index能够理解为一个文档收纳箱,你能够往内部塞入新的文档,或者从里面拿出文档,但若是你要修改里面的某个文档,则必须先拿出来修改后再塞回去。这个收纳箱能够塞入各类类型的文档,文档里的内容能够任意定义,Lucene都能对其进行索引。ui

Document(文档)
相似数据库内的行或者文档数据库内的文档的概念,一个Index内会包含多个Document。写入Index的Document会被分配一个惟一的ID,即Sequence Number(更多被叫作DocId),关于Sequence Number后面会再细说。this

Field(字段)
一个Document会由一个或多个Field组成,Field是Lucene中数据索引的最小定义单位。Lucene提供多种不一样类型的Field,例如StringField、TextField、LongFiled或NumericDocValuesField等,Lucene根据Field的类型(FieldType)来判断该数据要采用哪一种类型的索引方式(Invert Index、Store Field、DocValues或N-dimensional等),关于Field和FieldType后面会再细说。编码

Term和Term Dictionary
Lucene中索引和搜索的最小单位,一个Field会由一个或多个Term组成,Term是由Field通过Analyzer(分词)产生。Term Dictionary即Term词典,是根据条件查找Term的基本索引。

Segment
一个Index会由一个或多个sub-index构成,sub-index被称为Segment。Lucene的Segment设计思想,与LSM相似但又有些不一样,继承了LSM中数据写入的优势,可是在查询上只能提供近实时而非实时查询。

Lucene中的数据写入会先写内存的一个Buffer(相似LSM的MemTable,可是不可读),当Buffer内数据到必定量后会被flush成一个Segment,每一个Segment有本身独立的索引,可独立被查询,但数据永远不能被更改。这种模式避免了随机写,数据写入都是Batch和Append,能达到很高的吞吐量。Segment中写入的文档不可被修改,但可被删除,删除的方式也不是在文件内部原地更改,而是会由另一个文件保存须要被删除的文档的DocID,保证数据文件不可被修改。Index的查询须要对多个Segment进行查询并对结果进行合并,还须要处理被删除的文档,为了对查询进行优化,Lucene会有策略对多个Segment进行合并,这点与LSM对SSTable的Merge相似。

Segment在被flush或commit以前,数据保存在内存中,是不可被搜索的,这也就是为何Lucene被称为提供近实时而非实时查询的缘由。读了它的代码后,发现它并非不能实现数据写入便可查,只是实现起来比较复杂。缘由是Lucene中数据搜索依赖构建的索引(例如倒排依赖Term Dictionary),Lucene中对数据索引的构建会在Segment flush时,而非实时构建,目的是为了构建最高效索引。固然它可引入另一套索引机制,在数据实时写入时即构建,但这套索引实现会与当前Segment内索引不一样,须要引入额外的写入时索引以及另一套查询机制,有必定复杂度。

Sequence Number
Sequence Number(后面统一叫DocId)是Lucene中一个很重要的概念,数据库内经过主键来惟一标识一行,而Lucene的Index经过DocId来惟一标识一个Doc。不过有几点要特别注意:
DocId实际上并不在Index内惟一,而是Segment内惟一,Lucene这么作主要是为了作写入和压缩优化。那既然在Segment内才惟一,又是怎么作到在Index级别来惟一标识一个Doc呢?方案很简单,Segment之间是有顺序的,举个简单的例子,一个Index内有两个Segment,每一个Segment内分别有100个Doc,在Segment内DocId都是0-100,转换到Index级的DocId,须要将第二个Segment的DocId范围转换为100-200。
DocId在Segment内惟一,取值从0开始递增。但不表明DocId取值必定是连续的,若是有Doc被删除,那可能会存在空洞。
一个文档对应的DocId可能会发生变化,主要是发生在Segment合并时。

Lucene内最核心的倒排索引,本质上就是Term到全部包含该Term的文档的DocId列表的映射。因此Lucene内部在搜索的时候会是一个两阶段的查询,第一阶段是经过给定的Term的条件找到全部Doc的DocId列表,第二阶段是根据DocId查找Doc。Lucene提供基于Term的搜索功能,也提供基于DocId的查询功能。

DocId采用一个从0开始底层的Int32值,是一个比较大的优化,同时体如今数据压缩和查询效率上。例如数据压缩上的Delta策略、ZigZag编码,以及倒排列表上采用的SkipList等,这些优化后续会详述。

索引类型

Lucene中支持丰富的字段类型,每种字段类型肯定了支持的数据类型以及索引方式,目前支持的字段类型包括LongPoint、TextField、StringField、NumericDocValuesField等。

clipboard.png

如图是Lucene中对于不一样类型Field定义的一个基本关系,全部字段类都会继承自Field这个类,Field包含3个重要属性:name(String)、fieldsData(BytesRef)和type(FieldType)。name即字段的名称,fieldsData即字段值,全部类型的字段的值最终都会转换为二进制字节流来表示。type是字段类型,肯定了该字段被索引的方式。
FieldType是一个很重要的类,包含多个重要属性,这些属性的值决定了该字段被索引的方式。
Lucene提供的多种不一样类型的Field,本质区别就两个:一是不一样类型值到fieldData定义了不一样的转换方式;二是定义了FieldType内不一样属性不一样取值的组合。这种模式下,你也可以经过自定义数据以及组合FieldType内索引参数来达到定制类型的目的。
要理解Lucene可以提供哪些索引方式,只须要理解FieldType内每一个属性的具体含义,咱们来一个一个看:
stored: 表明是否须要保存该字段,若是为false,则lucene不会保存这个字段的值,而搜索结果中返回的文档只会包含保存了的字段。
tokenized: 表明是否作分词,在lucene中只有TextField这一个字段须要作分词。
termVector: 这篇文章很好的解释了term vector的概念,简单来讲,term vector保存了一个文档内全部的term的相关信息,包括Term值、出现次数(frequencies)以及位置(positions)等,是一个per-document inverted index,提供了根据docid来查找该文档内全部term信息的能力。对于长度较小的字段不建议开启term verctor,由于只须要从新作一遍分词便可拿到term信息,而针对长度较长或者分词代价较大的字段,则建议开启term vector。Term vector的用途主要有两个,一是关键词高亮,二是作文档间的类似度匹配(more-like-this)。
omitNorms: Norms是normalization的缩写,lucene容许每一个文档的每一个字段都存储一个normalization factor,是和搜索时的相关性计算有关的一个系数。Norms的存储只占一个字节,可是每一个文档的每一个字段都会独立存储一份,且Norms数据会所有加载到内存。因此若开启了Norms,会消耗额外的存储空间和内存。但若关闭了Norms,则没法作index-time boosting(elasticsearch官方建议使用query-time boosting来替代)以及length normalization。
indexOptions: Lucene提供倒排索引的5种可选参数(NONE、DOCS、DOCS_AND_FREQS、DOCS_AND_FREQS_AND_POSITIONS、DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS),用于选择该字段是否须要被索引,以及索引哪些内容。
docValuesType: DocValue是Lucene 4.0引入的一个正向索引(docid到field的一个列存),大大优化了sorting、faceting或aggregation的效率。DocValues是一个强schema的存储结构,开启DocValues的字段必须拥有严格一致的类型,目前Lucene只提供NUMERIC、BINARY、SORTED、SORTED_NUMERIC和SORTED_SET五种类型。
dimension:Lucene支持多维数据的索引,采起特殊的索引来优化对多维数据的查询,这类数据最典型的应用场景是地理位置索引,通常经纬度数据会采起这个索引方式。

来看下Lucene中对StringField的一个定义:

clipboard.png

StringFiled有两种类型索引定义,TYPE_NOT_STORED和TYPE_STORED,惟一的区别是这个Field是否须要Store。从其余的几个属性也能够解读出,StringFiled选择omitNorms,须要进行倒排索引而且不须要被分词。

Elasticsearch数据类型

Elasticsearch内对用户输入文档内Field的索引,也是按照Lucene能提供的几种模式来提供。除了用户能自定义的Field,Elasticsearch还有本身预留的系统字段,用做一些特殊的目的。这些字段映射到Lucene本质上也是一个Field,与用户自定义的Field无任何区别,只不过Elasticsearch根据这些系统字段不一样的使用目的,定制有不一样的索引方式。

clipboard.png

举个例子,上图​是Elasticsearch内两个系统字段_version和_uid的FieldType定义,咱们来解读下它们的索引方式。Elasticsearch经过_uid字段惟一标识一个文档,经过_version字段来记录该文档当前的版本。从这两个字段的FieldType定义上能够看到,_uid字段会作倒排索引,不须要分词,须要被Store。而_version字段则不须要被倒排索引,也不须要被Store,可是须要被正排索引。很好理解,由于_uid须要被搜索,而_version不须要。但_version须要经过docId来查询,并且Elasticsearch内versionMap内须要经过docId作大量查询且只须要查询出_version字段,因此_version最合适的是被正排索引。

关于Elasticsearch内系统字段全面的解析,能够看下这篇文章。

总结

这篇文章主要介绍了Lucene的一些基本概念以及提供的索引类型。后续咱们会有一系列文章来解析Lucene提供的IndexWriter的写入流程,其In-Memory Buffer的结构以及持久化后的索引文件结构,来了解Lucene为什么能达到如此高效的数据索引性能。也会去解析IndexSearcher的查询流程,以及一些特殊的查询优化的数据结构,来了解为什么Lucene能提供如此高效的搜索和查询。

相关文章
相关标签/搜索