Lucene (v4.5) 深入学习 (十) ―― 搜索
Lucene的搜索大致按照下面的顺序进行:
1. 首先会从QueryParser得到一颗Query对象树。
2. 接下来计算打分公式中的公共部分,同时得到了weight对象树
3. 过滤可用的文档,得到scorer
4. 调用scorer的score方法开始真正的评分
5. 在需要分页的地方进行过滤,最后做排序
其中,Query根据查询语句生成查询对象,Weight负责计算查询对象的权重,Scorer负责计算打分。对于每个对象,依靠BooleanQuery组织成树结构,其非叶子节点就是BooleanQuery,叶子节点是其他Query,形成Query后,Weight对象的组织就依靠Query树递归一步一步构建起来的,Scorer也是类似的。
Lucene的搜索大致按照下面的顺序进行:
1. 首先会从QueryParser得到一颗Query对象树。
2. 接下来计算打分公式中的公共部分,同时得到了weight对象树
3. 过滤可用的文档,得到scorer
4. 调用scorer的score方法开始真正的评分
5. 在需要分页的地方进行过滤,最后做排序
1.2搜索使用的树结构
搜索过程主要是在3种类上实现的,即Query,Weight, Scorer。其中,Query根据查询语句生成查询对象,Weight负责计算查询对象的权重,Scorer负责计算打分。对于每个对象,依靠BooleanQuery组织成树结构,其非叶子节点就是BooleanQuery,叶子节点是其他Query,形成Query后,Weight对象的组织就依靠Query树递归一步一步构建起来的,Scorer也是类似的。
2搜索的详细流程
2.1搜索总流程
更详细的搜索流程如下:
1. 通过createNormalizedWeight从Query创建Weight,Lucene通过Weight来计算查询评分的权重。从下图可以看到底层的TermQuery建立对应的TermWeight。
2. 通过TopFieldCollector.create生成Collector,Collector的主要作用是用来搜集原始的评分结果,在结果的基础上可以进行排序,过滤等操作。代码如下:
1. 通过createNormalizedWeight从Query创建Weight,Lucene通过Weight来计算查询评分的权重。从下图可以看到底层的TermQuery建立对应的TermWeight。
2. 通过TopFieldCollector.create生成Collector,Collector的主要作用是用来搜集原始的评分结果,在结果的基础上可以进行排序,过滤等操作。代码如下:
1
2
3
4
5
6
7
8
9
10
| protected TopDocs search(List<AtomicReaderContext> leaves, Weight weight, ScoreDoc after, int nDocs) throws IOException { int limit = reader.maxDoc(); if (limit == 0 ) { limit = 1 ; } nDocs = Math.min(nDocs, limit); TopScoreDocCollector collector = TopScoreDocCollector.create(nDocs, after, !weight.scoresDocsOutOfOrder()); search(leaves, weight, collector); return collector.topDocs(); } |
3. 从weight中生成Scorer,Scorer的目的是用于计算评分并生成结果。
4. 调用Scorer的score方法计算评分结果并用collector搜集文档结果集。以上两步的代码如下:
4. 调用Scorer的score方法计算评分结果并用collector搜集文档结果集。以上两步的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| protected void search(List<AtomicReaderContext> leaves, Weight weight, Collector collector) throws IOException { for (AtomicReaderContext ctx : leaves) { // search each subreader try { collector.setNextReader(ctx); } catch (CollectionTerminatedException e) { continue ; } Scorer scorer = weight.scorer(ctx, !collector.acceptsDocsOutOfOrder(), true , ctx.reader().getLiveDocs()); if (scorer != null ) { try { scorer.score(collector); } catch (CollectionTerminatedException e) { } } } } |
5. 从collector的结果中得到topDocs(参考2中的代码)
2.2构造Query树
通过QueryParser构造Query树。具体来说,就是用多层的BooleanQuery将底层的TermQuery组织起来。
2.3构造weight树
createNormalizedWeight方法的代码如下:
1
2
3
4
5
6
7
8
9
10
11
| public Weight createNormalizedWeight(Query query) throws IOException { query = rewrite(query); Weight weight = query.createWeight( this ); float v = weight.getValueForNormalization(); float norm = getSimilarity().queryNorm(v); if (Float.isInfinite(norm) || Float.isNaN(norm)) { norm = 1 .0f; } weight.normalize(norm, 1 .0f); return weight; } |
上面的代码大致可以划分为下面的步骤:
1.重写Query树。重写的主要目的是将整棵树上一些需要改变搜索关键词的地方重新改变。比如,整个索引建立时有这样几个term,”apple”,”apply”,在搜索”appl*”时QueryParser将其解释为 PrefixQuery,在重写这步便会用ConstantScoreQuery替换掉原来的 PrefixQuery。
2.根据Query树创建Weight树,这个创建过程是一个递归的过程。调用顶层query.createWeight,就会将整棵Weight树构建起来。
3. 计算ValueForNormalization
4. 根据ValueForNormalization计算queryNorm
5. 计算每个文档共用的那部分分数,也就是query本身的分数。
1.重写Query树。重写的主要目的是将整棵树上一些需要改变搜索关键词的地方重新改变。比如,整个索引建立时有这样几个term,”apple”,”apply”,在搜索”appl*”时QueryParser将其解释为 PrefixQuery,在重写这步便会用ConstantScoreQuery替换掉原来的 PrefixQuery。
2.根据Query树创建Weight树,这个创建过程是一个递归的过程。调用顶层query.createWeight,就会将整棵Weight树构建起来。
3. 计算ValueForNormalization
4. 根据ValueForNormalization计算queryNorm
5. 计算每个文档共用的那部分分数,也就是query本身的分数。
2.4构造Score树
如何构建底层的TermScorer?见下面的代码:
1
2
3
4
5
6
7
8
9
10
11
| public Scorer scorer(AtomicReaderContext context, boolean scoreDocsInOrder, boolean topScorer, Bits acceptDocs) throws IOException { assert termStates.topReaderContext == ReaderUtil.getTopLevelContext(context) : "The top-reader used to create Weight (" + termStates.topReaderContext + ") is not the same as the current reader's top-reader (" + ReaderUtil.getTopLevelContext(context); final TermsEnum termsEnum = getTermsEnum(context); if (termsEnum == null ) { return null ; } DocsEnum docs = termsEnum.docs(acceptDocs, null ); assert docs != null ; return new TermScorer( this , docs, similarity.simScorer(stats, context)); } |
可以将TermsEnum和DocsEnum当作迭代器,TermsEnum的包含了Term和Freq的信息,而DocsEnum则包含了docs的信息。
获取下一个需要处理的文件使用advance(int target)方法。target是文档ID,advance找到了等于或大于target的最小的文档ID,下面是advance方法的调用链:
不同的Scorer会根据不同的规则获取“下一篇”文件,也就是后面的评分过程用到的文件。
在Scorer树中,叶子除了基本的TermScorer,也包括重写之后得到的ConstantScorer。之后,Lucene会根据链接关系词,递归生成对应的高层的Scorer。比如,对于形如“A OR B”的查询,Term A和Term B分别用TermScorer包装起来,并生成DisjunctionScorer,这两个TermScorer是DisjunctionScorer的SubScorer。如果是“A AND B”这样的查询,则会生成ConjunctionScorer作为上层节点。
下面是ConjunctionScorer的doNext方法,用于得到下一篇文档的文档ID:
获取下一个需要处理的文件使用advance(int target)方法。target是文档ID,advance找到了等于或大于target的最小的文档ID,下面是advance方法的调用链:
不同的Scorer会根据不同的规则获取“下一篇”文件,也就是后面的评分过程用到的文件。
在Scorer树中,叶子除了基本的TermScorer,也包括重写之后得到的ConstantScorer。之后,Lucene会根据链接关系词,递归生成对应的高层的Scorer。比如,对于形如“A OR B”的查询,Term A和Term B分别用TermScorer包装起来,并生成DisjunctionScorer,这两个TermScorer是DisjunctionScorer的SubScorer。如果是“A AND B”这样的查询,则会生成ConjunctionScorer作为上层节点。
下面是ConjunctionScorer的doNext方法,用于得到下一篇文档的文档ID:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| private int doNext( int doc) throws IOException { for (;;) { advanceHead: for (;;) { for ( int i = 1 ; i < docsAndFreqs.length; i++) { if (docsAndFreqs[i].doc < doc) { docsAndFreqs[i].doc = docsAndFreqs[i].scorer.advance(doc); if (docsAndFreqs[i].doc > doc) { doc = docsAndFreqs[i].doc; break advanceHead; } } } return doc; } doc = lead.doc = lead.scorer.advance(doc); } } |
这个方法将从第一个SubScorer(docsAndFreqs[0].scorer)得到的doc(docID)作为输入,获得第一个满足所有SubScorer的doc(docID)作为输出。
相比之下,如果是ConjunctionScorer,则会寻找下一个满足任一一个SubScorer的doc。
相比之下,如果是ConjunctionScorer,则会寻找下一个满足任一一个SubScorer的doc。
2.5 评分并获取文档
后面的部分比较简单,http://blog.csdn.net/liweisnake/article/details/11528711 上已经写得很详细了。
Read full article from Lucene (v4.5) 深入学习 (十) ―― 搜索 | 远游即归途
No comments:
Post a Comment