Lucene概述

前言

从所周知Lucene是一个强大的搜索库。它是现在搜索产品(Solr,ElasticSearch)的核心.反正关系数据搜索一慢,就往上面搬。但有没想过Lucene倒排索引为什么比B+索引快?Lucenne是怎么索引的。

Lucene概述

lucene是只完成发搜索程序的索引模块搜索模块而已。在这里插入图片描述
上图为整个搜索程序全部组件。深绿色为lucence实现的部分。从图中可知:

  1. 直接的Raw Content不存在Lucene中
  2. 用户的查询最终会转化用Query进入Lucene

索引的文件结构

Lucene经多年演进优化,一个索引文件结构(不同版本可能文件不一样)如图所示,基本可以分为四个部分:词典、倒排表、正向文件、列式存储DocValues,词项向量TermVector。
在这里插入图片描述

正向索引

按顺序层次保存了索引:索引(Index) –> 段(segment) –> 文档(Document) –> 域(Field) –> 词(Term)。大致文件如下

  • segments_N:
    1. 保存了此索引包含多少个段
    2. 每个段包含多少篇文档。
    3. N表示版本号
  • .del:记录被删除的文档
  • .fnm:主要存元信息,即文档的每个域的名称,索引方式
  • .fdt:文档的数据,按域维度存,结合fnm,可以读出文档
  • .fdt:文档索引,记录了每篇文档在fdt的偏移量(加载到内存)

正向的查找文档过程:

  1. 找到些文件的索引目录中的segments_N
  2. segments_N根据之中的分段信息加载各段的fdt
  3. 在根据文档ID号内存fdt中找到文档的fdt偏移量
  4. 结合fnm和fdt偏移量,取出对应的文档
列式存储 DocValues

Lucene在构建索引时,会额外建立一个基于域(Field) 顺序的文档(Document)列表。

主要解决排序,分组操作

  1. 遍历提取所有出现在文档集合的排序字段
  2. 构建一个最终的排好序的文档集合list

这个步骤的过程全部维持在内存中操作,而且如果排序数据量巨大的话,非常容易就造成solr内存溢出和性能缓慢。

  • dvm:主要排序域的元信息及dvd文件的偏移量
  • dvd:根据排序域的文档ID,结合fdt可快速定位文档
词项向量TermVector

为了实现,快速对搜索关键字标红的功能。如下图所示在这里插入图片描述
对文档内“Lucene” Term快速标红。大致索引文件:

  • tvf: 保存每一个文档的所有域的所有TermVector的具体信息。
  • tvd: 记录了每一个文档的所有域在tvf文件中相对首域的偏移量
  • tvx: 记录了每篇文档在tvf的偏移量,在tvd的偏移量, 同fdt功能类似,

把所的term提到tvf中,tvd记录每个域的长度,tvx记录每个文档长度

倒排索引

按倒序层次保存了索引:词(Term) –> 文档(Document)。大致文件如下:

  • tip:针对不同的域建立不同的FST前缀(单词的公共部分)并加载到内存中,叶子节点指向tim中后缀的偏移量
  • tim: 针对不同的域建立不同的FST后缀(单词的独立部分),叶子指向doc中倒排表的偏移量
  • doc: 通过tip,tim组用的term与文档列表形成关联

逆向的查找文档过程:

  1. 根据term,在tip文件中查找到后缀文件tim偏移量
  2. 根据term, 在tim文件中查找倒排表doc的偏移量
  3. 如果域有排序,交集dvd文件转交为有序文档ID集合
  4. 根据倒排表中的文档ID,找到文档的fdt偏移量
  5. 结合fnm和fdt偏移量,取出对应的文档

索引的构建与使用

索引的创建是个很复杂的过程,正向索引不做介绍,这是主要介绍倒排的过程。

  1. 现有一员工数据(employee)如下
docIdnameagegenderdept
0a20Femalesales
1b1Malesales
2c3Malesales
3d5Malesales
4e7Femalesales
  1. 明确搜索需要对字段进行配置
# 查询需求为
# 统计销售部:男女分数及其平均年龄
select
 gender, count(1), avg(arg) 
from employee
where dept = 'sales'
group by gender

# 结果就是对
# 对 dept 索引
# 对 gender DocValues
  1. 构建索引,分别对dept进行倒排索引,对gender按Male,Female排序进行docValues,
sales
1,2,3,0,4
gender
0,1,2,3,4
  1. 利用索引搜索

在这里插入图片描述

  1. 排序dept域倒排得到[0,1,2,3,4]
  2. 与gender docValues交集得 Female[0,4].Male[1,2,3]
  3. 对两集合计算count(1),avg(age)为Female[2,23.5].Male[3,23]

算法的优化

在索引创建和使用的过程中用到了许许多多的数据结构与算法。

词典-FST

  • FST构建前要求 dept的字段必须排序
  • 假设输入为abd,abd,acf,acg
    在这里插入图片描述
  1. 插入abd时,没有输出。
  2. 插入abe时,计算出前缀ab,但此时不知道后续还不会有其他以ab为前缀的词,所以此时无输出。
  3. 插入acf时,因为是有序的,知道不会再有ab前缀的词了,这时就可以写tip和tim了,tim中写入后缀词块d、e和它们的倒排表位置ip_d,ip_e,tip中写入a,b和以ab为前缀的后缀词块位置(真实情况下会写入更多信息如词频等)。
  4. 插入acg时,计算出和acf共享前缀ac,这时输入已经结束,所有数据写入磁盘。tim中写入后缀词块f、g和相对应的倒排表位置,tip中写入c和以ac为前缀的后缀词块位置。

倒排表

倒排表就是文档号有序集合

  1. 数据压缩,可以看下图将6个数字从原先的24bytes压缩到7bytes。
    在这里插入图片描述

  2. 跳跃表加速合并,因为布尔查询时,and 和or 操作都需要合并倒排表,这时就需要快速定位相同文档号,所以利用跳跃表来进行相同文档号查找。如下实现跳跃表:

层级
l173332
压缩结点732272301129

分段存储

FST须要输入有顺序的term才会被构建,lucene如何实现呢?先了解下Lucene的段的不变性思想:

  • 新增。当有新的数据需要创建索引时,由于段的不变性,所以选择新建一个段来存储新增的数据。
  • 删除。当需要删除数据时,由于数据所在的段只可读,不可写,所以Lucene在索引文件下新增了一个.del的文件,用来专门存储被删除的数据id。当查询时,被删除的数据还是可以被查到的,只是在进行文档链表合并时,才把已经删除的数据过滤掉。被删除的数据在进行段合并时才会真正被移除。
  • 更新。更新的操作其实就是删除和新增的组合,先在.del文件中记录旧数据,再在新段中添加一条更新后的数据。

由于段的不变性,和FST有所冲突(PS:总不能一条数据搞一个段吧)。因此 Lucene并没有每新增一条数据就增加一个段,而是采用延迟写的策略,流程如下:

  • 新数据被写入时,并没有被直接写到硬盘中,而是被暂时写到内存中。Lucene默认是一秒钟,或者当内存中的数据量达到一定阶段时,再批量提交到磁盘中,当然,默认的时间和数据量的大小是可以通过参数控制的。通过延时写的策略,可以减少数据往磁盘上写的次数,从而提升整体的写入性能。
  • 在达到触发条件以后,会将内存中缓存的数据一次性写入磁盘中,并生成提交点。一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限;相反,当段在内存中时,就只有写数据的权限,而不具备读数据的权限,所以也就不能被检索了。
  • 清空内存,等待新的数据写入

Lucene或者Elasticsearch并不能被称为实时的搜索引擎,只能被称为准实时的搜索引擎

段合并策略

上面段的设计,会造成段文件数量过多,检索使要合并多个结果集,不仅会严重消耗服务器的资源,还会影响检索的性能。因段的不变性,使合并成了可能。具体策略:

  1. 根据段的大小先将段进行分组,再将属于同一组的段进行合并。
  2. 对超级大的段的合并需要消耗更多的资源,所以Lucene会在段的大小达到一定规模,或者段里面的数据量达到一定条数时,不会再进行合并

Lucene的段合并主要集中在对中小段的合并上,这样既可以避免对大段进行合并时消耗过多的服务器资源,也可以很好地控制索引中段的数量。

段合并的主要参数如下。

  • mergeFactor:当大小几乎相当的段的数量达到此值的时候,开始合并。
  • minMergeSize:所有大小小于此值的段,都被认为是大小几乎相当,一同参与合并。
  • maxMergeSize:当一个段的大小大于此值的时候,就不再参与合并
  • maxMergeDocs:当一个段包含的文档数大于此值的时候,就不再参与合并

假设有如下的段:
在这里插入图片描述
从头开始,选择一个值最大的段,然后将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为是大小差不多的段,属于同一阶梯,此处称为第一阶梯。得
在这里插入图片描述
第一阶梯总共4个段,小于mergeFactor因而不合并,接着start=end从而选择下一阶梯。得
在这里插入图片描述
从start开始,选择一个值最大的段,然后将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为属于同一阶梯,此处称为第三阶梯。第三阶梯总共5个段,等于mergeFactor,因而进行合并。
在这里插入图片描述
第二阶段的五个段合并成一个较大的段。
然后从头开始,考察第一阶梯,因为有了新生成的段,并且大小足够属于第一阶梯,从而第一阶梯有5个段,可以合并。
在这里插入图片描述
第一阶梯的五个段合并成一个大的段。
在这里插入图片描述
以上是正向索引的合并过程,倒排索引的合并过程为:

  • 对字典的合并,词典中的Term是按照字典顺序排序的,需要对词典中的Term进行重新排序
  • 对于相同的Term,对包含此Term的文档号列表进行合并,需要对文档号重新编号。

相当于把2段的FST内容合拿出来,合成一个新的FST

在段合并时,除了需要对索引数据进行合并,还需要移除段中已经删除的数据。

高可用&扩展性

在分布式的时候,高可用和扩展性是不可避免的话题,Lucene的定位只是搜索引挚。但其产品ES,Solr帮其实现。

  1. 分片shard,支持大数据。多分片查询只要聚合结果集就行
//routing 是一个可变值,默认是文档的 _id 
//number_of_primary_shards 主分片的数量
//shard 文档所在分片的位置,0 到 numberofprimary_shards-1 之间的余数
shard = hash(routing) % number_of_primary_shards
  1. 副本Replicas,为每分片提供数据备份。备份和主数据不在同一机子上
    在这里插入图片描述
  2. 事务日志Translog,事务日志记录了所有还没有持久化到磁盘的数据。避免丢失数据.

与B+Tree的对比

  • Mysql 的 B+Tree 相当 只有 term dictionary 这一层,另外 b-tree 排序的方式存储在磁盘上的。检索一个 term 需要若干次的 random access 的磁盘操作。
  • Lucene 在 term dictionary 的基础上添加了 term index 来加速检索,term index 以树的形式缓存在内存中。从 term index 查到对应的 term dictionary 的 block 位置之后,再去磁盘上找 term,大大减少了磁盘的 random access 次数。

TODO

以上讲的是Lucene工程化的一个过程。Lucene核心是:

  1. 分词《NPL基于词典分词
  2. 打分《NLP文本相似度(TF-IDF)

主要参考

lucene DocValues——没有看懂
lucene索引结构(三)-词项向量(TermVector)索引文件结构分析
Lucene学习总结
Lucene学习总结之五:Lucene段合并(merge)过程分析
Lucene FST
Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理
全文搜索引擎Elasticsearch,这篇文章给讲透了
Elasticsearch倒排索引与B+Tree对比

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论

打赏作者

懒猫gg

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值