第一个想到的方法是把我的全部数据域都做成Lucene的索引,然后全部通过Lucene去搜索。但是由于我的很多域是数字,全部转换成Lucene能接受的字符串,感觉性能不会好。另外如果我想针对搜索的结果做统计,也没法避免需要遍历全部的搜索结果,如果1W个结果就需要2分钟的话,就算不用处理其他的域,也是不能忍受的。
开源软件的好处就是可以读代码。通过阅读Hits的代码,终于找到了解决问题的办法。
Lucene 的代码看起来并不是特别Professional。比如下面这两个Hits的初始化函数。首先里面的q,s,f什么的让人看起来就不是太舒服(其他的代码里还用i,j做循环变量)。其次这两个函数只有o那一个赋值不一样,明显应该只写一个,让另一个来调用。最后程序里面直接用了50这个常数,编程的大忌。(50在其他函数里面也有)
Hits(Searcher s, Query q, Filter f) throws IOException {
通过这两个函数,应该看出Hits初始化的时候只调入了前100个文档。
一般我们是通过Document doc(int n)函数来访问的。这个函数里面先判断了有多少数据已经被调入了,如果要访问的数据不在,就去调用getMoreDocs函数,getMoreDocs会取得需要的2倍文档进来。
但是getMoreDocs的代码比较让人疑惑,里面一段代码是这样的:
这不成了每次翻倍的时候都要去调search重新查找吗?除非search里面有缓存,否则性能一定指数下降啊!
实际上Hits最终使用的也是TopDocs,Searcher组合来实现输出结果,那不如我们来直接使用下层一点的对象了。我原来的代码是:
Hits hits = searcher.search(query);
for( int i=0;i<hits .length();i++) {
}
现在改为:
TopDocs topDoc = searcher.search(query.weight(searcher), null, 100000);//注意最后一个参数,是search返回的结果数量,应该比你最大可能返回的数量大,否则ScoreDoc里面就是你设置的数量。
ScoreDoc[] scoreDocs = topDoc.scoreDocs;
for( int i=0;i<scoreDocs.length;i++) {
}
结果把12000个ID加入ArrayList用时0.4秒,快了几百倍。
等等,还没完。
我只需要ID字段,但是返回整个Doc,其他两个文本Field也返回了。因为Lucene是倒索引保存信息的,每一个文本Field需要重新组合成原始的字符串,这也是要耗时间的。searcher的doc函数有一个可以限定只取部分域的:
Document doc(int n, FieldSelector fieldSelector)
我下面定义一个FieldSelector,只取某一个给定名字的Field
class SpecialFieldSelector implements FieldSelector {
}
再修改我的代码:
ScoreDoc[] scoreDocs = topDoc.scoreDocs;
ArrayList<Document> szTest = new ArrayList<Document>();
FieldSelector fieldSelector = new SpecialFieldSelector(FIELD_ID);
for( int i=0;i<scoreDocs.length;i++) {
}
现在返回1.2W个ID耗时0.25秒。虽然比前面只少了大约150毫秒,但是是接近40%的提高了,在负载比较大的应用中还是很重要的。