而尽可能减少IndexSearcher的创建和对搜索结果的前台的缓存也是必要的。

Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而是只将所有结果中匹配度最高的头
100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定
要把所有记录内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应
用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。

如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果
构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索
再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果
记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。

Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。

刚刚开始学Lucene,看的是Lucene in
Action。顺着看下去,很自然的就是使用Hits来访问Search的结果。但是使用起来,发现Search的速度是很快,不过如果结果很多的话(比 如1W个),通过Hits访问所有的结果速度非常慢,就是简单地从每个结果中读一个Field,在我的机器上用了接近2分钟。因为我的应用索引的只是我的 数据的两个域包含文本信息的域,我本希望通过Lucene查找出符合需求的数据ID,再通过ID去判断数据库中的其他域来决定最终的结果。这样连取ID就 需要2分钟,我的应用可受不了。

第一个想到的方法是把我的全部数据域都做成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 {
    weight =
q.weight(s);
    searcher =
s;
    filter =
f;
    nDeletions =
countDeletions(s);
   
getMoreDocs(50); // retrieve 100 initially
   
lengthAtStart = length;
  }

  Hits(Searcher s, Query q, Filter f, Sort o)
throws IOException {
    weight =
q.weight(s);
    searcher =
s;
    filter =
f;
    sort =
o;
    nDeletions =
countDeletions(s);
   
getMoreDocs(50); // retrieve 100 initially
   
lengthAtStart = length;
  }
通过这两个函数,应该看出Hits初始化的时候只调入了前100个文档。

一般我们是通过Document doc(int
n)函数来访问的。这个函数里面先判断了有多少数据已经被调入了,如果要访问的数据不在,就去调用getMoreDocs函数,getMoreDocs会 取得需要的2倍文档进来。

但是getMoreDocs的代码比较让人疑惑,里面一段代码是这样的:
    int n = min
* 2;    //
double # retrieved
    TopDocs
topDocs = (sort == null) ? searcher.search(weight, filter, n) :
searcher.search(weight, filter, n, sort);
这不成了每次翻倍的时候都要去调search重新查找吗?除非search里面有缓存,否则性能一定指数下降啊!
实际上Hits最终使用的也是TopDocs,Searcher组合来实现输出结果,那不如我们来直接使用下层一点的对象了。我原来的代码是:

Hits hits = searcher.search(query);
for( int i=0;i<hits .length();i++) {
    Document doc
= hits .doc(i );
   
szTest.add(doc);
}
现在改为:
TopDocs topDoc = searcher.search(query.weight(searcher), null,
100000);//注意最后一个参数,是search返回的结果数量,应该比你最大可能返回的数量大,否则ScoreDoc里面就是你设置的数量。

ScoreDoc[] scoreDocs = topDoc.scoreDocs;
for( int i=0;i<scoreDocs.length;i++) {
    Document doc
= searcher.doc(scoreDocs[i].doc );
   
szTest.add(doc);
}
结果把12000个ID加入ArrayList用时0.4秒,快了几百倍。

等等,还没完。
我只需要ID字段,但是返回整个Doc,其他两个文本Field也返回了。因为Lucene是倒索引保存信息的,每一个文本Field需要重新组合成原始 的字符串,这也是要耗时间的。searcher的doc函数有一个可以限定只取部分域的:

Document doc(int n, FieldSelector fieldSelector)

我下面定义一个FieldSelector,只取某一个给定名字的Field
class SpecialFieldSelector implements FieldSelector {
    protected
String m_szFieldName;
    public
SpecialFieldSelector( String szFieldName ) {
       
m_szFieldName = szFieldName;
    }
   
    public
FieldSelectorResult accept(String fieldName) {
       
if( fieldName.equalsIgnoreCase(m_szFieldName)) {
           
return  FieldSelectorResult.LOAD;
       
}
       
else {
           
return  FieldSelectorResult.NO_LOAD;
       
}
   
}   
}
再修改我的代码:
ScoreDoc[] scoreDocs = topDoc.scoreDocs;
ArrayList<Document> szTest = new
ArrayList<Document>();
FieldSelector fieldSelector = new
SpecialFieldSelector(FIELD_ID);
for( int i=0;i<scoreDocs.length;i++) {
       
Document doc = searcher.doc(scoreDocs[i].doc, fieldSelector);
       
szTest.add(doc);
}
现在返回1.2W个ID耗时0.25秒。虽然比前面只少了大约150毫秒,但是是接近40%的提高了,在负载比较大的应用中还是很重要的。



注:
   有些可以借鉴的