Lucene 2.9 上,终于在 Searchable 接口中支持 search(weight, filter, collector) 了,而不是像 2.4 中那样,只在 IndexSearcher 中支持,而在 MultiSearcher 中,需要自己手工的添加 Collector。 把原来的非常丑陋的 HitCollector 换成了一般丑陋的 Collector ,本来觉得还挺高兴的,但仔细一看代码前的注释:“This API is experimental and might change in incompatible ways in the next release.” 无语了。
虽然 API 还是实验性的,但看起来已经到达了可用的阶段,所以 IMobile Search 2.0 中,还是使用 Lucene 2.9 的 API ,将原来临时性质的 groupby 功能实现重构了一下,大致思路是这样的:
- 使用 Collector 实现
- 实现一个 抽象类 o.a.l.search.Collector 的子类
- 在子类中维护一个 o.a.l.search.TopFieldCollector 实例来做具体的收集动作
- 在子类 collect(int doc) 调用中,对字段值进行 groupby 聚合,丢弃重复的记录
关键点在于如何对字段值进行 groupby 聚合。上一个版本的实现里,在 collect(int doc, float score) 函数调用里调用了 reader.document(doc),而这种做法是 Lucene 强烈反对的,注释里面的原话是这样说的:
Note: This is called in an inner search loop. For good search performance,
implementations of this method should not call {@link Searcher#doc(int)} or
{@link org.apache.lucene.index.IndexReader#document(int)} on every hit.
Doing so can slow searches by an order of magnitude or more.
那么就需要一个地方缓存着所有文档中的这个字段的值,及对应的文档 id 。 因为 Lucene 每次更新后文档 id 都会重新排列,所以这个缓存还必须在每次更新后重新生成。
最开始受原来新浪爱问搜索的做法的影响,考虑可以在更新完成后,将这个字段的所有的值及对应id dump 到硬盘上,在 Search 端 reopen 索引的时候,顺便载入。但后来觉得这样太麻烦,需要改好几个文件,需要重启好几个进程,为什么不直接在 Search 端从索引中读取内容更新缓存呢?当然,读取缓存肯定不是用 reader.document() 去遍历,而是照着 o.a.l.search.DuplicateFilter 里面的做法,使用 TermEnum 遍历这个字段的所有索引值。(前提是:这个字段的 index 是 NOT_ANALYZED 的。数据库中允许 groupby 的字段也应该为数字,或短小的字符串,不是么?)
核心代码看起来是这个样子的:
[code=java]
Term startTerm = new Term(fieldName);
TermEnum te = reader.terms(startTerm);
if(te != null)
{
Term currTerm = te.term();
while((currTerm != null) && (currTerm.field() == startTerm.field()))
{
if(te.docFreq() > 0)
{
TermDocs td = reader.termDocs(currTerm);
while(td.next())
{
fcache[td.doc()] = currTerm.text();
log.debug("add " + td.doc() + " : " + currTerm.text());
}
}
if(!te.next())
{
break;
}
currTerm=te.term();
}
}
[/code]
{ 1 } Comments
group by 功能就像 taobao 的分组统计吧。
在 solr 中叫 facet(我叫它“层面浏览/搜索”)。
Post a Comment