Lucene 查询原理
|
前言 Lucene 是一个基于 Java 的全文信息检索工具包,目前主流的搜索系统 Elasticsearch 和 solr 都是基于 lucene 的索引和搜索能力进行。想要理解搜索系统的实现原理,就需要深入 lucene 这一层,看看 lucene 是如何存储需要检索的数据,以及如何完成高效的数据检索。 在数据库中因为有索引的存在,也可以支持很多高效的查询操作。不过对比 lucene,数据库的查询能力还是会弱很多,本文就将探索下 lucene 支持哪些查询,并会重点选取几类查询分析 lucene 内部是如何实现的。为了方便大家理解,我们会先简单介绍下 lucene 里面的一些基本概念,然后展开 lucene 中的几种数据存储结构,理解了他们的存储原理后就可以方便知道如何基于这些存储结构来实现高效的搜索。本文重点关注是 lucene 如何做到传统数据库较难做到的查询,对于分词,打分等功能不会展开介绍。 本文具体会分以下几部分:
Lucene 数据模型Lucene 中包含了四种基本数据类型,分别是:
在 lucene 中,读写路径是分离的。写入的时候创建一个 IndexWriter,而读的时候会创建一个 IndexSearcher,
从这个示例中可以看出,lucene 的读写有各自的操作类。本文重点关注读逻辑,在使用 IndexSearcher 类的时候,需要一个 DirectoryReader 和 QueryParser,其中 DirectoryReader 需要对应写入时候的 Directory 实现。QueryParser 主要用来解析你的查询语句,例如你想查 “A and B",lucene 内部会有机制解析出是 term A 和 term B 的交集查询。在具体执行 Search 的时候指定一个最大返回的文档数目,因为可能会有过多命中,我们可以限制单词返回的最大文档数,以及做分页返回。 下面会详细介绍一个索引查询会经过几步,每一步 lucene 分别做了哪些优化实现。 Lucene 查询过程在 lucene 中查询是基于 segment。每个 segment 可以看做是一个独立的 subindex,在建立索引的过程中,lucene 会不断的 flush 内存中的数据持久化形成新的 segment。多个 segment 也会不断的被 merge 成一个大的 segment,在老的 segment 还有查询在读取的时候,不会被删除,没有被读取且被 merge 的 segement 会被删除。这个过程类似于 LSM 数据库的 merge 过程。下面我们主要看在一个 segment 内部如何实现高效的查询。为了方便大家理解,我们以人名字,年龄,学号为例,如何实现查某个名字(有重名)的列表。
在 lucene 中为了查询 name=XXX 的这样一个条件,会建立基于 name 的倒排链。以上面的数据为例,倒排链如下:
Alice | [1,2,3]
18 | [1,5] 在这里,Alice,Alan,18,这些都是 term。所以倒排本质上就是基于 term 的反向列表,方便进行属性查找。到这里我们有个很自然的问题,如果 term 非常多,如何快速拿到这个倒排链呢?在 lucene 里面就引入了 term dictonary 的概念,也就是 term 的字典。term 字典里我们可以按照 term 进行排序,那么用一个二分查找就可以定为这个 term 所在的地址。这样的复杂度是 logN,在 term 很多,内存放不下的时候,效率还是需要进一步提升。可以用一个 hashmap,当有一个 term 进入,hash 继续查找倒排链。这里 hashmap 的方式可以看做是 term dictionary 的一个 index。 从 lucene4 开始,为了方便实现 rangequery 或者前缀,后缀等复杂的查询语句,lucene 使用 FST 数据结构来存储 term 字典,下面就详细介绍下 FST 的存储结构。
FST
这样你就得到了一个有向无环图,有这样一个数据结构,就可以很快查找某个人名是否存在。FST 在单 term 查询上可能相比 hashmap 并没有明显优势,甚至会慢一些。但是在范围,前缀搜索以及压缩率上都有明显的优势。 在通过 FST 定位到倒排链后,有一件事情需要做,就是倒排链的合并。因为查询条件可能不止一个,例如上面我们想找 name=“alan” and age="18" 的列表。lucene 是如何实现倒排链的合并呢。这里就需要看一下倒排链存储的数据结构
SkipList
有了这个 SkipList 以后比如我们要查找 docid=12,原来可能需要一个个扫原始链表,1,2,3,5,7,8,10,12。有了 SkipList 以后先访问第一层看到是然后大于 12,进入第 0 层走到 3,8,发现 15 大于 12,然后进入原链表的 8 继续向下经过 10 和 12。
有了这张图,我们可以理解为什么基于 lucene 可以快速进行倒排链的查找和 docid 查找,下面就来看一下有了这些后如何进行倒排链合并返回最后的结果。
倒排合并
在 lucene 中会采用下列顺序进行合并:
因为 currentDocId ==1,继续
整个合并步骤我可以发现,如果某个链很短,会大幅减少比对次数,并且由于 SkipList 结构的存在,在某个倒排中定位某个 docid 的速度会比较快不需要一个个遍历。可以很快的返回最终的结果。从倒排的定位,查询,合并整个流程组成了 lucene 的查询过程,和传统数据库的索引相比,lucene 合并过程中的优化减少了读取数据的 IO,倒排合并的灵活性也解决了传统索引较难支持多条件查询的问题。
BKDTree
如果是多维,kdtree 的建立流程会发生一些变化。
下图是一个建立例子:
BKDTree 是 KDTree 的变种,因为可以看出来,KDTree 如果有新的节点加入,或者节点修改起来,消耗还是比较大。类似于 LSM 的 merge 思路,BKD 也是多个 KDTREE,然后持续 merge 最终合并成一个。不过我们可以看到如果你某个 term 类型使用了 BKDTree 的索引类型,那么在和普通倒排链 merge 的时候就没那么高效了所以这里要做一个平衡,一种思路是把另一类 term 也作为一个维度加入 BKDTree 索引中。 如何实现返回结果进行排序聚合通过之前介绍可以看出 lucene 通过倒排的存储模型实现 term 的搜索,那对于有时候我们需要拿到另一个属性的值进行聚合,或者希望返回结果按照另一个属性进行排序。在 lucene4 之前需要把结果全部拿到再读取原文进行排序,这样效率较低,还比较占用内存,为了加速 lucene 实现了 fieldcache,把读过的 field 放进内存中。这样可以减少重复的 IO,但是也会带来新的问题,就是占用较多内存。新版本的 lucene 中引入了 DocValues,DocValues 是一个基于 docid 的列式存储。当我们拿到一系列的 docid 后,进行排序就可以使用这个列式存储,结合一个堆排序进行。当然额外的列式存储会占用额外的空间,lucene 在建索引的时候可以自行选择是否需要 DocValue 存储和哪些字段需要存储。 Lucene 的代码目录结构介绍了 lucene 中几个主要的数据结构和查找原理后,我们在来看下 lucene 的代码结构,后续可以深入代码理解细节。lucene 的主要有下面几个目录:
最后本文介绍了 lucene 中的一些主要数据结构,以及如何利用这些数据结构实现高效的查找。我们希望通过这些介绍可以加深理解倒排索引和传统数据库索引的区别,数据库有时候也可以借助于搜索引擎实现更丰富的查询语意。除此之外,做为一个搜索库,如何进行打分,query 语句如何进行 parse 这些我们没有展开介绍,有兴趣的同学可以深入 lucene 的源码进一步了解。 |
时间:2018-10-12 21:29 来源: 转发量:次
声明:本站部分作品是由网友自主投稿和发布、编辑整理上传,对此类作品本站仅提供交流平台,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,不为其版权负责。如果您发现网站上有侵犯您的知识产权的作品,请与我们取得联系,我们会及时修改或删除。