在Lucene中,如何完成一个搜索的过程

关在Lucene中,如何完成一个搜索的过程,用过Lucene的朋友都会经常用到如下的一段代码:

Java代码 复制代码 收藏代码
1.Query query=parser.parse(searchText);//解析构建query树 
2. TopDocs td=search.search(query, 100);//检索的入口,限制返回结果集100 
3.  ScoreDoc[] sd=td.scoreDocs;//加载所有的Documnet文档 



以上的代码,可能是我们基本的检索中用得最多的一段了,但事实上,除了最基础的检索外,search方法,还有大量极其丰富的重载方法,API方法如下:


Java代码 复制代码 收藏代码
1.TopDocs search(Query query, int n); 
2. 
3.TopDocs search(Query query, Filter filter, int n); 
4. 
5.TopFieldDocs search(Query query, int n,Sort sort); 
6. 
7.void search(Query query, Filter filter, Collector results); 
8.void search(Query query, Collector results); 
9. 
10.TopFieldDocs search(Query query, Filter filter, int n,Sort sort); 
11. 
12.TopFieldDocs search(Query query, Filter filter, int n, Sort sort, boolean doDocScores, boolean doMaxScore); 
13. 
14.//以下是lucene支持的深度分页检索方式 
15. 
16.TopDocs searchAfter(ScoreDoc after, Query query, Filter filter, int n, Sort sort); 
17. 
18.TopDocs searchAfter(ScoreDoc after, Query query, int n, Sort sort); 
19. 
20.TopDocs searchAfter(ScoreDoc after, Query query, Filter filter, int n, Sort sort, boolean doDocScores, boolean doMaxScore); 



那么lucene是如何组织并执行这些重载的参数,如filter,collector,sort,以及topN和最终的打分呢?

下面我们详细看下这个流程是如何进行的.
1,首先一段文本将会被当成关键词进行检索,在这之前会由QueryParser这个类进行语法解析,经过分词解析后关键词会被QueryParser这个类组装成一个BooleanQuery对象,里面可能涵盖了多种经过解析后生成的Query对象,从而构成了一颗query树,这也是QueryParser这个类的功能强大之处,支持各种交,并,补,查,模糊,通配,范围,距离等等一系列简化写法。

整个检索在水平方向上主要有3个类完成即Query,Weight, Scorer,其各自的用处是,Quer负责生成组织查询对象,Weight负责计算权重,Score负责打分,垂直方向上依靠BooleanQuery构成的一颗树结构,其非叶子节点就是BooleanQuery,叶子节点是其他Query,形成Query后,Weight对象的组织就依靠Query树递归一步一步构建起来的,Scorer也是类似的。


2,第二步就开是进行检索了,跟踪源码我们发现,如果在search方法里的filter的值不为null的情况下,那么第一件事就是使用FilteredQuery包装原来的Query,过滤掉在检索不需要的结果集 Filter中有个重要的方法getDocIdSet,这个方法过滤对应的文档,然后将结果集返回,当然我们也可以自定义filter来实现其他一些额外的功能。


3,第三步,就开始使用上一步过滤后的Query对象,首先,重写Query树。重写的主要目的是将整棵树上一些需要改变搜索关键词的地方重新改变。比如,整个索引建立时有这样几个term,"算法","算术",在搜索"算*"时QueryParser将其解释为PrefixQuery,在重写这步便会搜索所有前缀为"算"的term,并用ConstantScoreQuery替换掉原来的PrefixQuery,在ConstantScorer中会将"算*"替换为"算法", "算术"两个实际的term,进而转化成求解一般term评分,这是典型的将复杂问题转换成已知问题求解的思想。

然后,通过代码Weight weight = query.createWeight(this);
根据Query树创建Weight树,这个创建过程是一个递归的过程。调用顶层query.createWeight,就会将整棵Weight树构建起来,接着
计算ValueForNormalization,然后根据ValueForNormalization计算queryNorm
,接着计算公共部分打分公式。

接下来通过TopScoreDocCollector collector = TopScoreDocCollector.create(nDocs, after, !weight.scoresDocsOutOfOrder());

这行代码,查询文档数topN,会被用于创建符合n以内的collector,

然后会遍历基于段对象的List<AtomicReaderContext> leaves,集合,来收集每个原子reader中符合条件的信息,接着就会由Weight对象,生成Score对象
      Scorer scorer = weight.scorer(ctx, !collector.acceptsDocsOutOfOrder(), true, ctx.reader().getLiveDocs());

在这步中,会根据具体情况生个多个XXXWeight对象,最基本的是TermWeight对象,然后再生成XXXWeight对应的XXXScore对象,

最后再调用 scorer.score(collector);方法遍历所有结果文档,并将结果集保存在一个优先级队列里面,并排序,topN参数,会被初始化成这个队列的大小。

FieldValueHitQueue<Entry> queue = FieldValueHitQueue.create(sort.fields, numHits);

关于排序的具体实现,也是通过类似Comparator和Comparatorable这样灵活高效的关系完成的。


在scorer方法的里,也加入了similarity来影响评分,similarity的默认实现类
DefaultSimilarity更是让我们控制评分方便多了,通常情况下我们只需要重写这个类,加入自己的评分逻辑,就可以在结果集中,影响评分。


最后从collector中拿到最终的TopDocs,至此一个完成的检索流程就结束了,当然我们
拿到TopDocs后,就可以为我们的业务做各种显示和处理了。



最后简单总结一下各个步骤的顺序:


步骤 描述
一 首先根据检索文本生成query树
二 如果有filter的话,会先执行filter过滤我们需要的文档集合
三 创建Weight和Scorer,并通过Scorer.score()进行评分
四 最后在队列里进行排序,默认是按照相关性得分排序

猜你喜欢

转载自weitao1026.iteye.com/blog/2266801