Lucene采用了基于倒排表的设计原理,可以非常高效的实现文本查找,在底层采用了分段的存储模式,使它在读写时几乎完全避免锁的出现,大大提升了读写性能。
核心模块
lucene的写流程和读流程如图所示。
其中,虚线箭头(a,b,c,d)表示写索引的主要过程,实线箭头(1-9)表示查询的主要过程。
lucene的主要模块(可结合上图)
- analysis模块:主要负责词法分析及语言处理,也就是常说的分词,通过该模块可最终形成存储或搜索的最小单元Term。
- index模块:主要负责索引的创建工作。
- store模块:主要负责索引的读写,主要是对文件的一些操作,其主要目的是抽象出和平台文件系统无关的存储。
- queryParser:主要负责语法分析,把我们的查询语句生成Lucene底层可以识别的条件。
- search模块:主要负责对索引的搜索工作
- similarity模块:主要负责相差性打分和排序的实现
核心术语
- Term:是索引里最小的存储和查询单元,对于英文来说一般指一个单词,对于中文来说一般指一个分词后的词。
- 词典(Term Dictionary):是Term的集合。词典的数据结构有多种,比如排序数组通过二分查找来检索数据;HashMap较排序数据快但占用空间更多;fst(finite-state transducer有限状态转换器)有更高的数据压缩率与查询效率。fst是默认的数据结构。附lucene里的数据结构介绍:elasticsearch中的数据结构
- 倒排表:一篇文章通常由多个词组成,倒排表记录的是某个词在哪些文章中出现过
- 正向信息:原始的文档信息,可以用来做排序,聚合与展示等。
- 段(segment):索引中最小的独立存储单元。一个索引文件由一个或多个段组成。在Lucene中的段有不变性,段一旦生成,在其上只能有读操作,不能有写操作
Lucene的底层存储格式如下图所示。其中的字典就是Term集合。词典中的Term指向的文档链表的集合叫倒排表。词典与倒排表是实现快速检索的重要基础,它们是分两部分存储的,在倒排表中不但存储了文档编号,还存储了词频等信息。
检索方式
在Lucene的查询过程中主要检索方式有下面四种。
- 单个词查询
指对一个Term进行查询,比如查找包含字符串‘lucene'的文档,则只需要词典中找到Term 'lucene',再获得在倒排表中对应的文档链表即可。 - AND
批对多个集合求交集。比如查找既包含字符串‘lucene'又包含字符串‘solr‘的文档,则查找步骤如下:
1) 在词典中找到Term 'lucene',得到‘lucene‘对应的文档链表。
2) 在词典中找到Term 'solr',得到‘solr‘对应的文档链表。
3) 合并链表,对两个文档链表做交集运算。 - OR
指对多个集合求并集。比如,若要查找包含字符串“lucene”或者包含字符串“solr”的文档,则查找步骤如下。
1) 在词典中找到Term“lucene”,得到“lucene”对应的文档链表。
2) 在词典中找到Term“solr”,得到“solr”对应的文档链表。
3) 合并链表,对两个文档链表做并集运算,合并后的结果包含“lucene”或者包含“solr”。 - NOT
指对多个集合求差集。比如,若要查找包含字符串“solr”但不包含字符串“lucene”的文档,则查找步骤如下。
1) 在词典中找到Term“lucene”,得到“lucene”对应的文档链表。
2) 在词典中找到Term“solr”,得到“solr”对应的文档链表。
3) 合并链表,对两个文档链表做差集运算,用包含“solr”的文档集减去包含“lucene”的文档集,运算后的结果就是包含“solr”但不包含“lucene”.
通过上述四种查询方式,我们可以知道,由于Lucene是以倒排表的形式存储的,所以在Lucene的查找过程中只需在词典中找到这些Term,根据Term获得文档链表,然后根据具体的查询条件对链表进行交,并,差等操作,就可以准确地查到我们想要的结果。
分段存储
早期全文检索为整个文档集合建立一个很大的倒排索引并将其写入磁盘,如果有更新,就需要重新创建一个索引来代替原来的索引。显然这种方式在数据量大的时候效率很低。
现在,在搜索中引入了段的概念,每个段都是一个独立的可被搜索的数据集,并且段具有不可变性,一旦索引的数据被写入磁盘就不可再修改。
在分段思想下,对数据写操作的过程如下:
- 新增。当有新的数据需要创建索引时,由于段的不变性,所以选择新建一个段来存储新增的数据。
- 删除。当需要删除数据时,由于数据的在的段只可读不可写,所以Lucene在索引文件下新增了了一个.del文件,用来专门存储被删除的数据id,当查询时,被删除的数据还是可以被查到,只是在进行文档链表合并时,才将已经删除的数据过滤掉。被删除的数据在进行段合并时才会真正被移除。
- 更新。更新操作其实就是删除和新增的组合,先在.del文件中记录旧数据,再在新的段中添加一条更新后的数据。
段不变的优点
- 不需要锁。因为数据不会更新,所以不用考虑多线程下的读写不一致问题
- 可以常驻内在。段在被加载到内在后,由于不变性,所以只要内在的空间足够大,就可以长时间驻存,大部分查询请求会直接访问内存而不需要访问磁盘
- 缓存友好。在段的声明周期内始终有效,不需要在每次数据更新时被重建
- 增量创建。分段可以做到增量创建索引,可以轻量级对数据进行更新,由于每次创建的成本很低,所以可以频繁地更新数据,使系统接近实时更新。
段不变的缺点
- 空间浪费。对数据进行删除时,旧数据不会被马上删除,只有到段合并时才会移除,这样会浪费大量空间
- 更新时浪费空间。更新是由新建与删除两个动作组成,也会浪费不少空间
- 服务器资源消耗大。由于索引具有不变性,所以每次更新数据时都需要新增一个段来存储数据。当段的数量过多时,对服务器的资源(如文件句柄)的消耗非常大,也会影响到查询性能。
- 查询时需要过滤删除数据。在查询后需要对已经删除的旧数据进行过滤,会增加查询的负担。
为了提升写的性能。Lucene并没有每新增一条数据就增加一个段,而是采用延迟写的策略,每当有新增的数据时,就将其先写入内存中,然后批量写入磁盘中。若有一个段被写到磁盘,就会生成一个提交点,提交点就是一个用来记录所有提交后的段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读的权限没写的权限;相反,当段在内存中时,就只有写数据权限而没有读数据权限,所以也就不能被检索了。因此从严格意义上来说Lucene或ES只能称为准实时的搜索引擎。
写索引流程
- 数据被写入时,并没有直接写到磁盘,而是被暂时写到内存中,默认是1秒,或当内存中的数据量达到一定阶段,再批量提交到磁盘中。通过延迟写策略可以提升整体写入性能。
- 在达到触发条件后,会将内存中缓存的数据一次性写入磁盘,并生成提交点。
- 清空内存,等待新的数据写入。
需要注意的是,正因为有延迟写,如果出现断电等情况会出现丢失数据,为此ES使用了事务日志来保证事务安全。
段合并策略
每次新增数据时都会新增一个段,所以时间长了后会导致索引中存在大量的段,这样会严重消耗服务资源,也会影响逵性能。
我们知道索引检索的过程是:查询所有段中满足查询条件的数据,然后对每个段里查询结果集进行合并,所以为了控制索引里段的数量,我们需要定期进行段合并操作。Lucene合并段的思路是:根据段的大小将段分组,再将属于同一组的段进行合并。由于对那些特别大的段进行合并需要消耗更多的资源,所以Lucene会在段的大小达到一定规模或段里面的数据达到一定条数时,不会再进行合并。
相似度打分
Lucene的查询过程是:首先在词典中查找每个Term,根据Term获得每个Term所存在的文档链表,然后根据查询条件对链表做交,并,差等操作,链表合并后的结果就是我们需要查找的数据。但是,我们一次查询出很多数据时,这些数据和我们的查询条件又有多大关系呢?其文本相似度是多少呢?它们是在similarity模块完成的。Lucene最经典的两文本相似度算法:基于向量空间模型的算法和基于概率的算法。。。后面的内容看不太懂了就写到这里,有兴趣可以看原文。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。