Lucene内核索引实现方式简析

Posted 计算广告检索技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lucene内核索引实现方式简析相关的知识,希望对你有一定的参考价值。

Lucene是apache软件基金会的一个子项目,是一个开源的的全文检索引擎工具包,是现在检索技术领域非常优秀的开源项目。本文针对Lucene倒排索引技术的特点进行了初步解析与分享。


1 索引优化

Lucene的倒排索引技术实现比关系型数据库要快很多,尤其是对多条件的过滤支持的非常好。传统关系型数据库采用B+树的方式存储索引,其主要目的是为了提高数据的更新效率。当实际业务场景并不需要快速的更新时,可以采用“预先排序”的方式进行索引的存储,牺牲更新效率,去换取更小的存储空间,更快的检索速度。

1.1 倒排索引简介

假设有如下正排表

docid

姓名

年龄

性别

1

Carla

18

2

Ada

20

3

Selena

18

这里每一行是一个document,每个document都有一个docid

若我们对年龄这个属性建倒排索引,则如下表:

年龄(term)

documents(posting list)

18

[1,3]

20

[2]

相应的,若按照性别进行构建,则如下表:

性别(term)

documents(posting list)

[1,2]

[3]


倒排表中,字段的一个取值(如年龄倒排的18,20)叫做一个term,而该取值下对应的document list (如:[1,3])叫做posting list,存储了所有符合某个term的文档id。

1.2 term dictionary 与 term index

假设在一次检索的过程中,我们需要检索出所有年龄属性为18的document,首先我们需要定位“年龄属性为18”的这一个term的存储位置,以便于从索引中取出posting list。

term dictionary

在实际的使用场景中,term的数量往往是非常庞大的,存储term所需的空间就可能达到TB的级别。为此,我们必须把term存放在磁盘中。假设我们有n个term,比如:15,16,13,20,25,18 ...... ,为了从中找到18,需要遍历所有term的取值,直到找到18,时间复杂度为O(n)。为了提高查找速度,我们需要设计一个term dictionary,方便定位term的存储位置。有几种思路可以参考:

  • Hash Table
    查找term的存储位置本质上是一次“term值->存储位置”的映射,时间复杂度最低的方式就是哈希表,仅为O(1)。但是由于上面提到的,存储term所需的空间就可能达到TB的级别,如采用哈希方式存储,其开销将十分巨大。哈希表存储一般仅用于“内存级数据量”的场景。

  • 排序
    对于“超内存数据量”的场景,另一个常见的优化方式是将term进行排序,然后采用二分查找的方式。如,排序后,term变为13,15,16,18,20,25  ...... ,从中找到18,时间复杂度仅为log(n)。

  • B+树
    B+树是最常用的索引数据结构。在检索维度,它对磁盘IO友好,可以提供逻辑有序的term dictionary(与排序类似,不过term间物理上不一定连续);在更新维度,它又支持高效的数据插入。

一些大型数据库技术中,大多采用B+ Tree的方案。

term index

有了term dictionary之后,可以用log(n)次查找从磁盘查找得到目标term。但是由于磁盘的顺序读取特性,二分查找的“随机读操作”的时间开销仍然是不能接受的。为了尽量少的读磁盘,最容易想到方法是将一些数据缓存到内存里,但是term dictionary所占用的空间巨大,无法全部存放到内存当中。

为了解决这个问题,Lucene做了一个特别的优化,那就是用Trie Tree的数据结构构建了内存型的tree index,来辅助查找term dictionary。

Trie Tree示意图如下:


可以看到,Trie Tree的特点是可以根据“前缀匹配原则”将前缀相同的词汇聚到一起。结合检索场景,Trie Tree实际可以将排序后相邻的term汇聚到一起,提供一个“大致”的存储位置。

假设我们需要对上述表中的姓名属性的term进行检索,排序之后的姓名取值:Ada, Carla, Elin, Kate, Patty, Sara, Selena。此时查找姓名为Sara的term,可以分为以下两步:

  • 定位以S开头的term的的存储区间

  • 在区间内进行二分查找

Lucene内核索引实现方式简析

实际情况中,term未必都是英文字符,term可以是任意的byte数组。通过term index可以快速的定位到term diectionary的某个位置,然后从这个位置上在进行二分查找。再加上一些压缩技术,term index的尺寸可以只有所有term尺寸的几十分之一,使得用内存缓存整个term index成为可能。


mysql和Lucene的检索性能对比中,会发现Lucene的检索性能由于mysql。这是因为MySQL只有term dictionary这一层,是以B+tree排序的方式存储在磁盘上的,检索一个term需要若干次磁盘的随机访存。而Lucene在term dictionary的基础上添加了term index来加速检索,term index以树的形式缓存在内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘的随机访存次数。

Lucene还对term index做了的如下两个优化:

•  term index在内存中是以FST(Finite State Transducers)的形式保存的,其特点是非常节省内存

•  term dictionary在磁盘上是以分Block的方式保存的,一个Block内部利用term的公共前缀压缩,比如都是S开头的单词就可以把S省略,这样term dictionary可以比B+ Tree更节约磁盘空间


2 存储优化

Lucene采用了DocValues按列组织的存储格式,其目的主要是为了降低随机读取的成本。docValues理解为数据,对于列存储来说,DocValues指的表中某一列的所有数据。

传统的行存储如下图所示:

Lucene内核索引实现方式简析

1和2代表的是docid,不同颜色代表的是不同的字段。可见在行存储中,同一个doc中的不同字段是物理连续的。

Lucene中采用的列存储如下图所示:

可见在列存储中,所有doc中的同一个字段,是按照docid物理连续存储的。

列存储会把一个表的存储按照字段(列)拆分成多个文件,每个字段一个文件,而每个文件内部都是按照docid排序的。这样一来,只要知道docid,就可以计算出这个docid在这个文件里的偏移量。也就是对于每个docid需要一次随机读操作。

Lucene采用列存储的主要有以下两个原因:

  • Lucene读取文件的方式基于mmap
    这种文件访问的方式是由操作系统代理将部分文件内容缓存到内存里,并根据实际访问情况“换入换出”。在内存足够的情况下,访问文件就相当于访问内存;但是当数据量庞大,内存不足以缓存所有数据时,访问缓存意外的数据会触发“磁盘与内存页的置换”,本质上是磁盘访问。若使用场景中仅仅需要检索出“年龄信息”,由于行存储数据按照doc聚合,检索时不得不置换大量我们并不需要的列,降低检索效率。但列存储可以很好的解决这个问题。

  • 性能优化
    按列存储之后,所有用于term dictionary的优化都可以作用于posting list的读取中,可以起到降低存储成本,提高检索效率的效果


值得一提的是,列存储相对于行存储的优化是在检索规模庞大时,才能体现出来。庞大可以理解为数据量的规模超越了内存存储的规模。事实上,由于内存和磁盘访存特性的差异,内存型数据库和磁盘型数据库在设计理念上也有很大差异。在具体场景需要具体分析,不可生搬硬套。

 


如果您感兴趣,欢迎转发!


以上是关于Lucene内核索引实现方式简析的主要内容,如果未能解决你的问题,请参考以下文章

Lucene初探之索引文件格式

lucene5.3.1的排序是怎么实现的?

Lucene建立索引库

lucene创建索引的几种方式

Lucene及全文搜索实现原理

搭建 SolrCloud 集群服务