Jack Frost

Lucene总结系列(三)–总述优化方案和呈现实时内存索引实现(结合RAMDirectory源码解析)

前两篇讲清楚基础和基本api调用,接下来我们就是要进入优化篇章了。

本系列:

(1)SSM框架构建积分系统和基本商品检索系统(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)(1)框架整合构建

(2)SSM框架构建积分系统和基本商品检索系统(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)(2)建立商品数据库和Lucene的搭建

(3)Redis系列(一)–安装、helloworld以及读懂配置文件

(4)Redis系列(二)–缓存设计(整表缓存以及排行榜缓存方案实现)

(5) Lucene总结系列(一)–认识、helloworld以及基本的api操作。

(6)Lucene总结系列(二)–商品检索系统的文字检索业务(lucene项目使用)


文章结构:(1)总述优化方案;(2)呈现实时内存目录索引(结合源码解析);


一、总述优化方案:

(1)索引创建优化:

1. 先将索引写入RAMDirectory,再批量写 入FSDirectory,不管怎样,目的都是尽量少的文件IO,因为创建索引的最大瓶颈在于磁盘IO。

2. 通过设置IndexWriter的参数优化索引建立

IndexWriter的forceMerge方法。当小文件达到多少个时,就自动合并多个小文件为一个大文件,因为它的使用代价较高不意见使用此方法,默认情况下lucene会自己合并。合并cfs文件。比如设定10,就是当小文件达到10个就自动合并成一个索引cfs文件。(而且只能在close前一步使用)

打开 IndexWriter 的时候,设置 autoCommit = false同传统的数据库操作一样,批量提交事务性能总是比每个操作一个事务的性能能好很多。

3.在建立索引过程中,使用单例的 IndexWriter基于内存执行 Flush 而不是基于 document count–也就是内存消耗flush代替文档数量flush。indexWriter可以自动根据内存消耗调用flush()。可以使用indexWriterConfig.setRAMBufferSizeMB(double)设置缓冲区大小。测试表明48MB为叫合适值。

4. 重用Document和Field。创建Document单一实例,使用Field的setValue方法重用Field。而通过 setValue 实现,这将有助于更有效的减少GC开销而改善性能。

5.创建单例的IndexWriter。

6.关闭复合文件格式(Compound file format)调用setUseCompoundFile(false),可以关闭。建立复合文件,将可能使得索引建立时间被拉长,有可能达到7%-33%。而关闭复合文件格式,将可能大大增加文件数量,而由于减少了文件合并操作,索引性能被明显增强。

7.不要使用太多的小字段,如果字段过多,尝试将字段合并到一个更大的字段中,以便于查询和索引适当增加 mergeFactor,但是不要增加的太多。关闭所有不需要的特性使用更快的 Analyzer特别是对于中文分词而言,分词器对于性能的影响更加明显。

(2)搜索索引时优化:

1. 建立实时内存索引,将索引放入内存(注意:针对数量小型的索引,当索引大于1G就要考虑分布式索引了)

通过RAMDirectory内存读写缓写提高性能

2. 合适使用api选择适合的范围索引:

[一]RangeQuery范围搜索。设置范围,但是RangeQuery的实现实际上是将时间范围内的时间点展开,组成一个个BooleanClause加入 到 BooleanQuery中查询,因此时间范围不可能设置太大,经测试,范围超过一个月就会抛 BooleanQuery.TooManyClauses,可以通过设 置 BooleanQuery.setMaxClauseCount(int maxClauseCount)扩大,但是扩大也是有限的,并且随着 maxClauseCount扩大,占用内存也扩大。

[二]RangeFilter替代。用RangeFilter代替RangeQuery,经测试速度不会比RangeQuery慢,但是仍然有性能瓶颈,查询的90%以上时间耗费在 RangeFilter,研究其源码发现RangeFilter实际上是首先遍历所有索引,生成一个BitSet,标记每个document,在时间范围内的标记为true,不在的标记为false,然后将结果传递给Searcher查找,这是十分耗时的。

针对Filter再进一步的优化:

[1]缓存Filter结果。既然RangeFilter的执行是在搜索之前,那么它的输入都是一定的,就是IndexReader, 而 IndexReader是由Directory决定的,所以可以认为RangeFilter的结果是由范围的上下限决定的,也就是由具体 的 RangeFilter对象决定,所以我们只要以RangeFilter对象为键,将filter结果BitSet缓存起来即可。 lucene API已经提供了一个CachingWrapperFilter类封装了Filter及其结果,所以具体实施起来我们可以 cache CachingWrapperFilter对象,需要注意的是,不要被CachingWrapperFilter的名字及其说明误 导, CachingWrapperFilter看起来是有缓存功能,但的缓存是针对同一个filter的,也就是在你用同一个filter过滤不 同 IndexReader时,它可以帮你缓存不同IndexReader的结果,而我们的需求恰恰相反,我们是用不同filter过滤同一 个 IndexReader,所以只能把它作为一个封装类。

[2]降低时间精度。研究Filter的工作原理可以看出,它每次工作都是遍历整个索引的,所以时间粒度越大,对比越快,搜索时间越短,在不影响功能的情况下,时间精度越低越好,有时甚至牺牲一点精度也值得,当然最好的情况是根本不作时间限制。

针对上面的优化例子:
第一组,时间精度为秒:方式 直接用RangeFilter 使用cache 不用filter 。平均每个线程耗时 10s 1s 300ms
第二组,时间精度为天:方式 直接用RangeFilter 使用cache 不用filter。平均每个线程耗时 900ms 360ms 300ms。

所以:

尽量降低时间精度,将精度由秒换成天带来的性能提高甚至比使用cache还好,最好不使用filter。

在不能降低时间精度的情况下,使用cache能带了10倍左右的性能提高。

3.IndexSearcher单例化。

(3)其余零散的优化点:

1. 使用最新版本的Lucene。使用本地文件系统(尽量不使用虚拟机),使用更快的硬件设备,尤其是SSD。

2. 使用更快的分析器。主要是对磁盘空间的优化,可以将索引文件减小将近一半,相同测试数据下由600M减少到380M。但是对时间并没有什么帮助,甚至会需要更长时 间,因为较好的分析器需要匹配词库,会消耗更多cpu

3. 关键词区分大小写。or AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单词。

4.设置boost。有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先显示标题中出现关键词的文章(没有使用排序的前题下)。使用方法:Field. setBoost(float boost);默认值是1.0,也就是说要增加权重的需要设置得比1大。

部分参考

方案大致列举这些,然后后面的文章会以这个为根结点不断去扩散的,并且结合源码解读下进一步的优化方案。


二、呈现实时内存索引(结合源码解析):

(1)代码实现以及优化效果展示:

第一次索引平均时间(无内存索引)

这里写图片描述

第一次索引平均时间(无内存索引)

这里写图片描述

第一次索引平均时间(建立内存索引)。

这里写图片描述

第一次索引平均时间(建立内存索引)。比无内存快一倍多。

这里写图片描述

(2)源码分析:

RAMDirectory类是一个驻留内存的(memory-resident)Directory抽象类的实现。

在并发状态下,管理锁资源的关键点就在SingleInstanceLockFactory 类

RAMFile —内存中组织的一个File对象 ,实际上是一个byte[]的数组链表。用这个对象去操作内存中的目录。


源码下载:Lucene总结系列Demo

好了,Lucene总结系列(三)–总述优化方案和呈现实时内存目录索引讲完了,这是项目过程中针对lucene的优化思路,现在一一罗列给大家,并用这一系列的文章结合源码去深入讲解这些优化思路,这是积累的必经一步,我会继续出这个系列文章,分享经验给大家。欢迎在下面指出错误,共同学习!!你的点赞是对我最好的支持!!

更多内容,可以访问JackFrost的博客

码字很辛苦,转载请注明来自JackFrost《Lucene总结系列(三)–总述优化方案和呈现实时内存索引实现(结合RAMDirectory源码解析)》

Leave a Reply

Your email address will not be published. Required fields are marked *