seq搜索引擎优化至少包括那几步?(全面解析Lucene的数据模型和数据读写路径(一)_)
优采云 发布时间: 2022-02-05 03:09seq搜索引擎优化至少包括那几步?(全面解析Lucene的数据模型和数据读写路径(一)_)
前言
Apache Lucene 是一个开源的高性能、可扩展的信息检索引擎,提供强大的数据检索能力。Lucene发展了很多年,功能越来越强大,架构也越来越精致。目前不仅支持全文索引,还提供了多种其他类型的索引方式来满足不同类型的查询需求。
有很多基于 Lucene 的开源项目。最著名的是 Elasticsearch 和 Solr。如果说 Elasticsearch 和 Solr 是一款设计精美、性能卓越的跑车,那么 Lucene 就是提供强大动力的引擎。为了驾驶这辆跑车,让它跑得更快更稳定,我们需要深入研究它的引擎。
在此之前,我们已经在专栏中发表了多篇文章的文章,分析了Elasticsearch的数据模型、读写路径、分布式架构、Data/Meta一致性等。在此文章之后,我们将继续发布Lucene的一系列原理和源码解读,全面分析Lucene的数据模型和数据读写路径。
Lucene 官方将其优势总结如下:
可扩展的高性能索引
强大、准确、高效的搜索算法
我们希望通过我们的系列 文章,读者可以了解 Lucene 是如何实现这些目标的。
整个分析将基于Lucene7.2.1版本,在阅读本文章之前,需要有一定的知识基础,比如了解基本的搜索和索引原理,了解什么是倒置,分词、相关等基本概念,并了解Lucene的基本使用,如Directory、IndexWriter、IndexSearcher等。
基本概念
在深入解读 Lucene 之前,我们先了解一下 Lucene 的一些基本概念,以及这些概念背后隐藏的一些东西。
该图显示了索引的基本组成。Segment中的数据只是一个抽象的表示,并不代表它实际的内部数据结构。
指数
表的概念类似于数据库,但与传统表的概念有很大不同。对于传统关系型数据库或NoSQL数据库中的表,至少在创建时必须定义表的schema、表的主键或列等,并且会有一些定义好的约束。而且Lucene的Index完全没有限制。Lucene的Index可以理解为一个文档存储盒,可以往里面插入新文档,也可以从中取出文档,但是如果要修改里面的文档,必须先取出修改后再放背部。这个存储盒可以塞进各种类型的文档,文档中的内容可以任意定义,Lucene可以对其进行索引。
文档
类似于数据库中的行或文档数据库中的文档的概念,索引将收录多个文档。写入Index的Document会被分配一个唯一的ID,即Sequence Number(更多称为DocId),后面会详细讨论。
场地
一个 Document 将由一个或多个 Fields 组成,Field 是 Lucene 中数据索引的最小定义单元。Lucene提供了很多不同类型的Fields,如StringField、TextField、LongFiled或NumericDocValuesField等。Lucene决定了哪种类型的索引方法(Invert Index、Store Field、DocValues或N维等),关于Field和FieldType稍后将详细讨论。
术语和术语字典
Lucene中索引和搜索的最小单位,一个Field会由一个或多个Term组成,Term是Field通过Analyzer(分词)生成的。Term Dictionary是一个Term字典,是根据条件查找Term的基本索引。
分割
索引将由一个或多个子索引组成,这些子索引称为段。Lucene的Segment设计思想,与LSM类似但又有些不同,继承了LSM中数据写入的优点,但只能提供近实时查询,不能提供实时查询。
在 Lucene 中写入数据会首先在内存中写入一个 Buffer(类似于 LSM 的 MemTable,但不可读)。当 Buffer 中的数据达到一定数量时,会被刷新到一个 Segment 中。每个Segment都有自己独立的索引,可以独立查询,但数据永远不能改变。这种模式避免了随机写入,数据写入都是batch和append,可以实现高吞吐。段中写入的文档不能修改,但可以删除。删除的方法不是在文件中更改,而是将要删除的文档的DocID保存在另一个文件中,以保证数据文件不能被修改。索引查询需要查询多个segment并合并结果,还需要处理已删除的文档。为了优化查询,
在刷新或提交段之前,数据存储在内存中并且无法搜索,这就是为什么说 Lucene 提供接近实时而不是实时查询的原因。看了它的代码,发现写数据查查也不是不行,只是实现起来比较复杂。原因是 Lucene 中的数据搜索依赖于构建的索引(例如,反转依赖于 Term Dictionary)。Lucene 在 Segment flush 期间构建数据索引,而不是实时构建,以便构建最有效的索引。当然也可以引入另外一套索引机制,在数据实时写入的时候构建,但是这个索引的实现会和段中当前的索引不同。它需要引入一个额外的写时索引和另一套查询机制,这很复杂。花费。
序列号
Sequence Number(以下统称DocId)是Lucene中一个非常重要的概念。一行由数据库中的主键唯一标识,Lucene的Index通过DocId唯一标识一个Doc。但是,有几点需要特别注意:
DocId 实际上在 Index 中并不是唯一的,但在 Segment 中是唯一的。Lucene 这样做主要是为了写和压缩优化。那么既然它在一个 Segment 中是唯一的,那么如何才能在 Index 级别唯一标识一个 Doc?解决方案非常简单。分段是有序的。举个简单的例子,一个Index中有两个Segment,每个Segment有100个Docs。Segment 中的 DocId 都是 0-100,并转换为 Index 级别。DocId,需要将第二段的DocId范围转换为100-200。
DocId在一个segment内是唯一的,值从0开始递增。但是,并不代表DocId的值一定是连续的。如果一个 Doc 被删除,可能会有漏洞。
文档对应的 DocId 可能会发生变化,主要是在合并片段时。
Lucene 中的核心倒排索引本质上是 Term 到收录该 Term 的所有文档的 DocId 列表的映射。因此,Lucene 的内部搜索将是一个两阶段的查询。第一个阶段是通过给定的Term条件找到所有Docs的DocId列表,第二个阶段是根据DocId找到Docs。Lucene 提供了基于 Term 的搜索功能和基于 DocId 的查询功能。
DocId使用从0开始的底层Int32值,是一个比较大的优化,也体现在数据压缩和查询效率上。比如数据压缩中的Delta策略、ZigZag编码、倒排列表中使用的SkipList等,这些优化后面会详细介绍。
索引类型
Lucene 支持丰富的字段类型。每个字段类型确定支持的数据类型和索引方法。目前支持的字段类型包括 LongPoint、TextField、StringField、NumericDocValuesField 等。
该图显示了Lucene中为不同类型的Field定义的基本关系。所有字段类都继承自 Field 类。Field 收录三个重要的属性: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的概念。简而言之,词向量保存了一个文档中所有词条相关的信息,包括词条值、频率(frequencies)和位置(positions)等,是一种逐文档倒排索引,提供了在文档中查找所有词条信息的能力。基于docid的文档。不建议对长度较小的字段启用term verctor,因为可以通过重做分词获得term信息。对于长度较长或分词成本较高的字段,建议启用词向量。词向量主要有两个用途,一是关键词高亮,二是做文档间的相似度匹配(more-like-this)。
omitNorms:Norms 是 normalization 的缩写。Lucene 允许每个文档的每个字段存储一个归一化因子,这是一个与搜索时相关性计算相关的系数。Norms 的存储只占用一个字节,但是每个文档的每个字段都会独立存储,Norms 数据会完全加载到内存中。所以如果Norms开启的话,会消耗额外的存储空间和内存。但是,如果关闭 Norms,则无法进行索引时间提升(elasticsearch 官方建议使用查询时间提升)和长度规范化。
indexOptions:Lucene为倒排索引提供了5个可选参数(NONE、DOCS、DOCS_AND_FREQS、DOCS_AND_FREQS_AND_POSITIONS、DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS),用于选择字段是否需要索引,以及索引什么内容。
docValuesType:DocValue是Lucene4.0引入的前向索引(docid to field的一列),大大优化了排序、分面或聚合的效率。DocValues 是一个强大的模式存储结构。启用 DocValues 的字段必须具有严格一致的类型。目前,Lucene 只提供了五种类型:NUMERIC、BINARY、SORTED、SORTED_NUMERIC 和 SORTED_SET。
维度:Lucene支持多维数据的索引,采用特殊索引优化多维数据的查询。这类数据最典型的应用场景是地理位置索引,一般用于经纬度数据。
我们看一下Lucene中对StringField的定义:
StringFiled有两种类型的索引定义,TYPE_NOT_STORED和TYPE_STORED,唯一的区别就是这个Field是否需要Store。也可以从 StringFiled 选择 omitNorms 的其他几个属性来解释,它需要倒排索引,不需要分词。
Elasticsearch 数据类型
Elasticsearch中用户输入文档中Field的索引也是根据Lucene可以提供的几种模式来提供的。除了用户自定义的字段,Elasticsearch 也有自己的预留系统字段用于一些特殊用途。这些字段到 Lucene 的映射本质上是一个 Field,和用户自定义的 Field 没有区别,但是 Elasticsearch 根据这些系统字段的不同用途定制了不同的索引方式。
比如上图是Elasticsearch中两个系统字段_version和_uid的FieldType定义,我们来解读一下它们的索引方法。Elasticsearch 通过 _uid 字段唯一标识一个文档,并通过 _version 字段记录文档的当前版本。从这两个字段的FieldType定义可以看出,_uid字段会被反向索引,不需要分词,需要存储。_version字段不需要向后索引,也不需要被Store索引,但需要向前索引。很容易理解,因为需要搜索_uid,而_version不需要。但是 _version 需要通过 docId 查询,而 Elasticsearch 中的 versionMap 需要通过 docId 做很多查询,
Elasticsearch中系统字段的综合分析,可以阅读这篇文章。
总结
这篇文章主要介绍了Lucene的一些基本概念和提供的索引类型。以后我们会有一系列文章来分析Lucene提供的IndexWriter的写入过程,其In-Memory Buffer的结构以及持久化后的索引文件的结构,了解Lucene为什么能实现如此高效的数据索引性能。还会分析IndexSearcher的查询过程,以及一些特殊的查询优化数据结构,来了解Lucene为什么能提供如此高效的搜索和查询。