lucene MoreLikeThis性能分析
最近使用lucene的MoreLikeThis实现一个小型的推荐系统。语料由短文本构成,本身也还算比较中小等规模:7000w左右(亿级别)的数据量,3G大小的文件。对需要的Field建完索引后的索引文件大小在4G左右。本文只是结合自己的实践列出一些注意事项,以做为参考。
一、MoreLikeThis实现原理
根据输入文本和要检索的Field,进行like判断时,整个过程如下:
- 提取出terms:分词,判断:是否大于最大出现数,是否停留词,统计tf => 返回<word,int>频率表
- 根据tf-idf分值对term进行从大到小排序: 遍历所有fields,找到最大文档频率df的值和对应field,判断一些条件,计算tf/idf值,插入到优先级队列 => FreqQ<word,topField,score,idf,docFreq,tf>
- 生成Query:使用TermQuery,设置相应boost值和Occuer.SHOULD生成BooleanQuery查询.
- 进行search.
这里说一下第3步的boost生成策略:
首先优先级队列中score(即tf/idf值)分值最高的,为bestScore. myScore为队列中的当前score值,按照以下的公式线性衰减,myScore越小,衰减的越厉害:
boost_value = boostFactor * myScore / bestScore
得到boost后,我们就可以根据原生lucene的打分公式在search后进行相关性排序:
(q为对应的查询条件,d为对应文档)
MoreLikeThis使用的是DefaultSmilarity. (对应coord, queryNorm等等,由它与BooleanQuery来决定)
二、性能分析
lucene的首次查询可忽略。简单地写个循环,文件中读取10000条不同的测试数据,存在一个map数组中。分别对其进行检索。
对相应比较的接口进行比较:
1.使用普通的接口MultiFieldQueryParse.
2.使用MoreLikeThis接口(不使用DuplicateFilter)
3.使用MoreLikeThis接口(使用DuplicateFilter)
方式3耗时相当严重(秒级别),如果真的需要去重,建议使用hashmap去重好了。这里的接口比较中,只比较方式1和方式2.
JAVA_OPTS为:-Xmx8096m -Xms8096m -server
(8g的jvm内存空间已经足够,经测试,开得更大不会在性能性获得提升。初始化时一次性分配好。)
使用mmap加载索引,使用的环境及版本:
lucene4.7.2 + jdk1.6.0-64+4core+32Gmem+linux修改版
这里使用了3个文档数,里面含有短文本,做为搜索输入,规模分别为:100条,1000条,1w条。此处的先对比下MostLike接口,相关参数如下:
- topN: 20
- MinTermFreq: 0
- MinDocFreq: 0
- MinWordLen: 1
- MaxQueryTerm: [1-4]
此处对最关键的MaxQueryTerm做了一个简单的性能测试,不同值下一个横向对比:
文档数 | MoreLike (1,20) |
平均时间 | MoreLike (2,20) |
平均时间 | MoreLike (3,20) |
平均时间 | MoreLike (4,20) |
平均时间 |
100 | 728 | 7ms | 1214 | 12ms | 2398 | 24ms | 3083 | 31ms |
1000 | 3114 | 3ms | 9651 | 10ms | 36116 | 36ms | 62432 | 62ms |
10000 | 17558 | 1.7ms | 80584 | 8ms | 305365 | 31ms | 565437 | 57ms |
MaxQueryTerm的长短会直接影响你对结果判断的好坏。因此,需要对该值做一个权衡。在不影响结果效果的前提下,当然追求速度越快越好,这会直接影响你线上的请求处理并发量。
为了再进行一次对比,这里列出了 MultiFieldParser的性能测试,包含了些简单逻辑处理单个字的词,停留词等,这里使用第一个为MUST,其余都为SHOULD(你也可以设计一定的规则,去自动设定搜索词的occur),其它参数:
topN: 20
文档数 | common(20) | 平均处理时间 |
100 | 292 | 3ms |
1000 | 989 | 1ms |
10000 | 3602 | 0.3ms |
性能方面是远远好与MoreLikeThis。但是效果方面肯定会打一些折扣。
切换到新的版本与环境:lucene-4.9.1 + jdk7,
文档数 | MostLike (1,20) |
平均时间 | MostLike (2,20) |
平均时间 | MostLike (3,20) |
平均时间 | MostLike (4,20) |
平均时间 |
100 | 755 | 7ms | 1265 | 12ms | 2459 | 24ms | 3152 | 31ms |
1000 | 3245 | 3ms | 10135 | 10ms | 36463 | 36ms | 62607 | 62ms |
10000 | 18289 | 1.8ms | 82256 | 8ms | 305795 | 31ms | 564701 | 56ms |
性能没有本质的提升。
接着再说一下多线程版本,还是jdk6的环境。性能测试表如下(此处算法有做了调整,只做横向对比,不与上面对比),分别处理100/1000/10000条的消息时的性能表现:
序号 | 线程数 | 100语料 (ms) |
100处理量(ms) | 1000语料 (ms) |
1000处理量(ms) | 10000语料 (ms) |
10000处理量(ms) |
1 | 1 | 4821 | 48 | 94900 | 94 | 782241 | 78 |
2 | 2 | 3237 | 32 | 54074 | 54 | 447454 | 45 |
3 | 4 | 2361 | 23 | 39928 | 40 | 250838 | 25 |
4 | 8 | 2310 | 23 | 37501 | 37 | 247236 | 25 |
5 | 16 | 2405 | 24 | 30930 | 31 | 261238 | 26 |
6 | 32 | 2264 | 23 | 30731 | 31 | 269937 | 27 |
相应的图为:
单cpu-4核的机器,4个线程的时候差不多是性能最好的时候。
长耗时查询
最后一个问题是长耗时查询,产生的原因也很简单。虽然MoreLikeThis会根据tf/idf取出前n个词来进行搜索,但是如果本身搜索的词就小于 n个,并且这些词还长着一张“大众脸”的话。。。duang,你一搜出来的totalHits就可能是上千万的结果集,然后根据分值做一下排序,那也会耗去不少时间。直观的感觉就是限制搜索集的数目,当满足一定量时就不再继续搜索下去。
方式一:
上面的条件中,有一个MinWordLen,如果为1的话,那么类似于单个字的倒排索引,它的文档列表会相当长(很可能几百万几千万)。将它设置为2的话,不算是查询效果以及查询性能上都有较大的提升。
4线程,MinWordLen=2的示例:
1000: 总耗时:16351ms, 平均:16ms
10000: 总耗时:392398ms, 平均:39ms
当你去跑上面的文档时,虽然降了不少,效果也提升不少。但是还是有一部分还是有些耗时(理论上问题仍存在,只是出现的机率下降了不少)。下面引出方式二。
方式二:
MoreLikeThis中还存在着一个参数,maxDocFreq。缺省值为32位的1. 我们可以设置成50w。该参数可以忽略过出现超过50w的查询。这个时候,你的查询性能将大大获得提升。完全可以满足实时查询的要求。
方式三:
由源码级别可知:TopScoreDocCollector的collect(). BooleanScorer2的score. 这两者,有能力的话自行修改代码. (通过继承等手段实现略难),就不推荐使用了。
排序公式部分:来自《annotated lucene》.