`
suichangkele
  • 浏览: 201443 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

solr(lucene)的reRank的核心实现源码解读

阅读更多

换公司了,公司的solr使用的是4.10,使用了ReRankQuery,我自己看了下源码。

 

先介绍一下solr的reRank,他的意思是进行两轮查找,第一轮对所有的doc进行查找,指定要查找多少个doc,第二轮是在第一轮中查找到的所有的doc中在进行一遍查找,使用一个不同的查询逻辑(也就是另一个query),重新打分,可以指定两次得分的最终处理的策略,最后返回需要查找的结果。说白了,他就是一个query,只不过他的查询是分两次的,这样做有一个好处,如果我们的查询的方式很麻烦,也就是需要的计算量很大,那么在文档很多的时候,进行大量的计算是很耗时的,但是如果我们对于大量的文档只进行一个很小的运算,然后再获得的topN中再进行一个很复杂的运算以实现我们的需求,那么就快多了。

我们以一个查询作为例子来讲解源码,比如我们的查询条件是

q=name:xiaoming&sort=age desc&rq={!rerank reRankQuery=$rrq reRankDocs=100 reRankWeight=3}&rrq=desc:很好 

 先说一下这个请求的最终被解析的意思吧,第一轮查询是在所有的doc中查询name是小明,按照age排序,查找钱100个,第二轮使用desc:很好这个请求再次查找,最后每个doc的得分是第一轮的得分+第二轮的得分*3,最后的排序就是按照得分排序的。  有一点比较重要,这里的sort是在第一轮查找中使用的,第二轮不使用,第二轮仅仅使用得分排序。这里的{!rerank指定使用的reRank的queryParser是ReRankQParserPlugin。

下面是摘自于org.apache.solr.handler.component.QueryComponent.prepare(ResponseBuilder)的方法

 

String defType = params.get(QueryParsing.DEFTYPE, QParserPlugin.DEFAULT_QTYPE);//查找defType,也就是指定的queryParser
			
QParser parser = QParser.getParser(rb.getQueryString(), defType, req);//根据defType获得QParser,这个parser解析的是rb.getQueryString,也就是q的属性
Query q = parser.getQuery();//解析q的属性形成一个query
if (q == null) {
        q = new BooleanQuery();
}
rb.setQuery(q);
//解析rq属性,
String rankQueryString = rb.req.getParams().get(CommonParams.RQ);
if (rankQueryString != null) {
	QParser rqparser = QParser.getParser(rankQueryString, defType, req);//解析rq的queryParser,如果rq没有指定qparser,则使用上面的defType
	Query rq = rqparser.getQuery();//解析的query,使用我们自己的那个请求参数的话,是生辰一个ReRankQuery,里面有个属性是reRankQuery,就是解析的rrq属性生成的query,这里是termQuery,desc:很好。当然也可以在rrq中继续使用localparam,比如{!parserName }xyz,指定使用的queryParser,如果不指定的话使用默认的queryParser。
	if (rq instanceof RankQuery) {//生产的一定是一个RankQuery,不然就没有意义了。
		RankQuery rankQuery = (RankQuery) rq;
		rb.setRankQuery(rankQuery);//设置解析rqq生产的query,也就是这里的termQuery,desc:很好
		MergeStrategy mergeStrategy = rankQuery.getMergeStrategy();
		if (mergeStrategy != null) {
			rb.addMergeStrategy(mergeStrategy);
			if (mergeStrategy.handlesMergeFields()) {
				rb.mergeFieldHandler = mergeStrategy;
			}
		}
	} else {
		throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "rq parameter must be a RankQuery");
	}
}

rb.setSortSpec(parser.getSort(true));
rb.setQparser(parser);

 从上面的代码中我们可以得出一个几轮,在4.10的solr中,rq这个参数和q已经同等重要了,或者说solr又添加了一个新的参数rq,可以说rq已经成了solr的一个标配了。这在4.7.2的solr中还是不存在,我搜索了一下,4.7.2的solr中根本没有ReRank这个类。

 

上面的代码的意思很简单,就是先根据q生成一个query,然后对rq也生成一个query,rq的{!reran 指定了使用的qParser是ReRankQParserPlugin,然后这个qparser解析reRankQuery所表示的rrq的字符串,生成一个TermQuery,当然也可以在rrq中继续使用localparam,比如{!parserName }xyz。

 

然后我们继续向下看,看一下org.apache.solr.handler.component.ResponseBuilder.getQueryCommand()这个方法,

public SolrIndexSearcher.QueryCommand getQueryCommand() {
	SolrIndexSearcher.QueryCommand cmd = new SolrIndexSearcher.QueryCommand();
	cmd.setQuery(wrap(getQuery()))//就看这一点,wrap的方法,传入的参数是q解析的query
			.setFilterList(getFilters())
			.setSort(getSortSpec().getSort())
			.setOffset(getSortSpec().getOffset())
			.setLen(getSortSpec().getCount())
			.setFlags(getFieldFlags())
			.setNeedDocSet(isNeedDocSet())
			.setCursorMark(getCursorMark());
	return cmd;
}

 

  Query wrap(Query q) {
    if(this.rankQuery != null) {//如果有了rankQuery,则将q解析的query作为参数传递到rankQuery的wrap方法中,我们再看一下RankQuery的wrap方法吧
      return this.rankQuery.wrap(q);
    } else {//如果没有rankQuery,则直接返回q,和原来的版本一样。
      return q;
    }
  }

 这里是ReRankQuery的代码截图片段,

 

 

private class ReRankQuery extends RankQuery {
	
	private Query mainQuery = defaultQuery;//这就是被包装的第一层查询时使用的query,也就是这里的name:xiaoming
	private Query reRankQuery;//这里就是解析的rrq形成的queyr,也就是第二轮查询使用的query
	private int reRankDocs;//第一轮查询要保留多少个doc
	private int length;//start + rows   这个是最后的返回结果,也就是第二轮需要返回多少个doc,是分页查找的缘故
	private double reRankWeight;//这个是第二轮的结果的分数的权重,也就是第二轮的结果的查找的得分要乘以这个权重作为第二轮的得分
	private Map<BytesRef, Integer> boostedPriority;//没看

	
	public RankQuery wrap(Query _mainQuery) {//包装方法就是包装第一轮查询使用的query,
		if (_mainQuery != null) {
			this.mainQuery = _mainQuery;
		}
		return this;
	}

 上面就是最终生成的ReRankQuery的参数,我们继续看一下他的weight和collector的生成方法:

 

 

public Weight createWeight(IndexSearcher searcher) throws IOException {
        return new ReRankWeight(mainQuery, reRankQuery, reRankWeight, searcher);
}

 他会生成一个ReRankWeight,在这个类中,所有的操作都是使用了mainQuery的方法,也就是将所有的方法都委托给mainQuery,很显然他是为了第一轮查询做准备,即使用mainQuery的条件进行第一轮的查询。最重要的方法为:

public Scorer scorer(AtomicReaderContext context, Bits bits) throws IOException {
	return mainWeight.scorer(context, bits);
}

 在创建scorer的时候,也是用mainQuery的weight生成的scorer,即收集的时候也是使用mainquery的逻辑进行收集。

 

那么他是如何做第二轮查询的呢?答案是在ReRankQuery的收集器上:org.apache.solr.search.ReRankQParserPlugin.ReRankQuery.getTopDocsCollector(int, QueryCommand, IndexSearcher)

public TopDocsCollector getTopDocsCollector(int len, SolrIndexSearcher.QueryCommand cmd, IndexSearcher searcher)
				throws IOException {

	if (this.boostedPriority == null) {//经过debug发现,不进入这个
		SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
		if (info != null) {
			Map context = info.getReq().getContext();
			this.boostedPriority = (Map<BytesRef, Integer>) context
					.get(QueryElevationComponent.BOOSTED_PRIORITY);
		}
	}
	return new ReRankCollector(reRankDocs, length, reRankQuery, reRankWeight, cmd, searcher, boostedPriority);
}

他是返回了一个ReRankCollector,代码如下:

public ReRankCollector(int reRankDocs, int length, Query reRankQuery, double reRankWeight,
				SolrIndexSearcher.QueryCommand cmd, IndexSearcher searcher, Map<BytesRef, Integer> boostedPriority) throws IOException {
			
	super(null);
	
	this.reRankQuery = reRankQuery;//第二轮的query
	this.reRankDocs = reRankDocs;//第一轮收集多少个
	this.length = length;//第二轮收集多少个
	this.boostedPriority = boostedPriority;
	Sort sort = cmd.getSort();//url中指定的sort
	if (sort == null) {//如果没有sort,
		this.mainCollector = TopScoreDocCollector.create(Math.max(this.reRankDocs, length), true);//
	} else {//如果有sort
		sort = sort.rewrite(searcher);
		this.mainCollector = TopFieldCollector.create(sort, Math.max(this.reRankDocs, length), false, true, true, true);
	}
	this.searcher = searcher;
	this.reRankWeight = reRankWeight;
}

  可以发现,sort也是全部用在了mainQuery上面了,即sort也是在第一轮查询的时候有用的。我们看一下这个collector的其他方法:

public void collect(int doc) throws IOException {
	mainCollector.collect(doc);
}

public void setScorer(Scorer scorer) throws IOException {
	mainCollector.setScorer(scorer);
}

public void setNextReader(AtomicReaderContext context) throws IOException {
	mainCollector.setNextReader(context);
}

public int getTotalHits() {
	return mainCollector.getTotalHits();
}

 所有的方法全部委托给mainQuery的collector,即当ReRankQuery在进行收集的时候,和使用mainQuery进行收集的逻辑是一样的,第二轮收集的逻辑体现在下面的方法上:当在获得最后的doc的时候,将第一轮收集获取的那些doc进行第二次排序:

第一个参数表示分页查找时的偏移量,也就是从第几个开始返回,第一个参数表示返回多少个。
public TopDocs topDocs(int starts, int howMany) {

		try {
			
			TopDocs mainDocs = mainCollector.topDocs(0, Math.max(reRankDocs, length));//从第一轮的手机中获得reRankDocs和lenght较大个的doc,
			if (mainDocs.totalHits == 0 || mainDocs.scoreDocs.length == 0) {
				return mainDocs;
			}

			if (boostedPriority != null) {//这个不进入,忽略
				忽略这个。
			} else {

				ScoreDoc[] mainScoreDocs = mainDocs.scoreDocs;

				/*
				 * Create the array for the reRankScoreDocs. </br>  
				 * 收集到的所有的doc中要重新排序的部分,因为在第一轮排序的时候,可能收集不够length数量的doc,所以要去最小值
				 */
				ScoreDoc[] reRankScoreDocs = new ScoreDoc[Math.min(mainScoreDocs.length, reRankDocs)];//这个表示要进行reRank(也就是第二轮重排)的docs
				System.arraycopy(mainScoreDocs, 0, reRankScoreDocs, 0, reRankScoreDocs.length);
				

				mainDocs.scoreDocs = reRankScoreDocs;

				// 进行重排,重排后的都在返回的rescoredDocs里面,重排的逻辑在QueryRescorer中,下面有这个的讲解
				TopDocs rescoredDocs = new QueryRescorer(reRankQuery) {
					@Override
					protected float combine(float firstPassScore, boolean secondPassMatches,float secondPassScore) {//第一轮的得分和第二轮的分的处理
						float score = firstPassScore;
						if (secondPassMatches) {//如果第一轮的某个doc也被第二轮命中了,
							score += reRankWeight * secondPassScore;//结果是加上第二轮的得分乘以第二轮的权重。
						}
						return score;
					}
				}.rescore(searcher, mainDocs, mainDocs.scoreDocs.length);
				
				// Lower howMany to return if we've collected fewer documents. 
				howMany = Math.min(howMany, mainScoreDocs.length);//这里的howMany就是start + rows,可能第一轮就没有搜到足够多
				
                                //往下走是比较费解的,为啥没有用到start呢?下面有答案。可以先把howMany理解为start + rows,事实上就是这样的
				if (howMany == rescoredDocs.scoreDocs.length) {//如果第二轮收集的doc的数量正好是howmay,则直接返回
					return rescoredDocs; // Just return the rescoredDocs
				} else if (howMany > rescoredDocs.scoreDocs.length) {//如果第一轮收集的doc的数量不够howmay,我现在猜测下面的额处理是sorl的一个bug(当然不会造成什么错误结果,只是下面的处理没有任何影响)。

					// We need to return more then we've reRanked, so create the combined page.
					ScoreDoc[] scoreDocs = new ScoreDoc[howMany];
					// lay down the initial docs
					System.arraycopy(mainScoreDocs, 0, scoreDocs, 0, scoreDocs.length);
					// overlay the rescoreds docs 
					System.arraycopy(rescoredDocs.scoreDocs, 0, scoreDocs, 0, rescoredDocs.scoreDocs.length);
					rescoredDocs.scoreDocs = scoreDocs;
					return rescoredDocs;
				} else {//
					// We've rescored more then we need to return.
					ScoreDoc[] scoreDocs = new ScoreDoc[howMany];
					System.arraycopy(rescoredDocs.scoreDocs, 0, scoreDocs, 0, howMany);
					rescoredDocs.scoreDocs = scoreDocs;
					return rescoredDocs;
				}
			}
		} catch (Exception e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
		}
	}
}

 

 

最后就是那个重排的方法了,也就是org.apache.lucene.search.QueryRescorer.rescore(IndexSearcher, TopDocs, int)方法: 

 

@Override
  public TopDocs rescore(IndexSearcher searcher, TopDocs firstPassTopDocs, int topN) throws IOException {
    ScoreDoc[] hits = firstPassTopDocs.scoreDocs.clone();
//对第一轮的结果先按照docid排序,
    Arrays.sort(hits,
                new Comparator<ScoreDoc>() {
                  @Override
                  public int compare(ScoreDoc a, ScoreDoc b) {
                    return a.doc - b.doc;
                  }
                });

    List<AtomicReaderContext> leaves = searcher.getIndexReader().leaves();

    // 使用第二轮排序的query进行重排
    Weight weight = searcher.createNormalizedWeight(query);

    // Now merge sort docIDs from hits, with reader's leaves:
    int hitUpto = 0;
    int readerUpto = -1;
    int endDoc = 0;
    int docBase = 0;
    Scorer scorer = null;

    while (hitUpto < hits.length) {//循环所有的第一轮收集到的doc

      ScoreDoc hit = hits[hitUpto];
      int docID = hit.doc;
      AtomicReaderContext readerContext = null;
      //这个判断的目的是现从一个段中查找,只有当这个段查找完了才查找下一个。
      while (docID >= endDoc) {
        readerUpto++;
        readerContext = leaves.get(readerUpto);
        endDoc = readerContext.docBase + readerContext.reader().maxDoc();//endDoc表示当前正在查找的段的最大的docid。
      }

      if (readerContext != null) {
        // We advanced to another segment:
        docBase = readerContext.docBase;
        scorer = weight.scorer(readerContext, null);//在当前的段中查找倒排表
      }

      if(scorer != null) {//如果可以找到结果(此时是有可能找不到的,因为现在使用的第二轮的query,他和第一轮的query是不同的)
        int targetDoc = docID - docBase;//当前的doc在当前段的id(也就是减去docBase)
        int actualDoc = scorer.docID();//当前的scorer所在的docid
        if (actualDoc < targetDoc) {
          actualDoc = scorer.advance(targetDoc);//移动到不小于指定的id的位置
        }

        if (actualDoc == targetDoc) {//如果相等,表示第二次查询也命中了这个doc
          // Query did match this doc:
          hit.score = combine(hit.score, true, scorer.score());//两次得分的处理,可以实现自己的处理方式,模式是加上去,不过要乘以权重!
        } else {//第二轮查找没有命中这个doc
          // Query did not match this doc:
          assert actualDoc > targetDoc;
          hit.score = combine(hit.score, false, 0.0f);
        }
      } else {//如果在这个段中第二轮的qurry没有命中任何的结果,则一定不会命中这个doc了
        // Query did not match this doc:
        hit.score = combine(hit.score, false, 0.0f);
      }
      hitUpto++;
    }

   //使用第二轮之后的得分重新排序,得分优先
    Arrays.sort(hits, new Comparator<ScoreDoc>() {
                  @Override
                  public int compare(ScoreDoc a, ScoreDoc b) {
                    // Sort by score descending, then docID ascending:
                    if (a.score > b.score) {
                      return -1;
                    } else if (a.score < b.score) {
                      return 1;
                    } else {
                      // This subtraction can't overflow int
                      // because docIDs are >= 0:
                      return a.doc - b.doc;
                    }
                  }
                });

    if (topN < hits.length) {//这个的意思是如果最后返回的额topN个(也就是start+rows)小于收集到的doc,则只取topN个。比如第一阶段收集了100个,但是分页显示每一页10个,只要第二页的10个,则topN是20,只要前面的20个即可。
      ScoreDoc[] subset = new ScoreDoc[topN];
      System.arraycopy(hits, 0, subset, 0, topN);
      hits = subset;
    }
    //将结果用topDocs包装。
    return new TopDocs(firstPassTopDocs.totalHits, hits, hits[0].score);
  }

 从这里可以看出,即使在第一轮查找中指定了sort也是没有用的,因为在第二轮排序的时候,会先使用id重排,最后的排序结果和第一轮的sort没有任何关系,全部是靠得分。

 

最后还有一个问题,为什么上面没有使用start,而是使用了0呢?因为在SolrIndexSearcher的getDocListNC(或者是getDocListAndSetNC)方法中,查找的时候的代码是这样的:

final TopDocsCollector topCollector = buildTopDocsCollector(len, cmd);//创建收集器
DocSetCollector setCollector = new DocSetDelegateCollector(maxDoc >> 6, maxDoc, topCollector);//创建代理收集器,实现更多的功能,比如限制时间,比如返回docSet,默认的收集器只是返回排好序的list
Collector collector = setCollector;

buildAndRunCollectorChain(qr, query, luceneFilter, collector, cmd, pf.postFilter);//从lucene的索引中查找。

set = setCollector.getDocSet();//获得docSet,这个就是代理收集器的作用。

totalHits = topCollector.getTotalHits();//获得所有的命中数量

TopDocs topDocs = topCollector.topDocs(0, len);//这里就是我得问题的答案:他在使用topDocs方法的时候,没有使用start,而是仅仅使用了0,因为solr在分页的处理中是自己处理的,没有交给lucene!所以之前的那个howmany就是len,也就是start + rows


忽略下面的

 

完了,ReRank就是这么简单。。。。。。。

 

一个fruitfull的周末的时间

 

 

 

 

分享到:
评论

相关推荐

    Apache Solr lucene 搜索模块设计实现

    Apache Solr 和 Lucene 是两个在全文...总的来说,Apache Solr 和 Lucene 提供了一套完整的解决方案,能够设计和实现复杂的企业级搜索系统。通过理解这些核心概念和技术,开发者可以构建出满足各种需求的高效搜索引擎。

    lucene-solr源码,编译成的idea项目源码

    本人用ant idea命令花了214分钟,35秒编译的lucene-solr源码,可以用idea打开,把项目放在D:\space\study\java\lucene-solr路径下,再用idea打开就行了

    Solr reRank简介

    ### Solr reRank 简介 在Solr搜索系统中,`reRank`功能是一项高级特性,它允许用户在初始搜索结果的基础上进行二次排序,从而优化最终的搜索结果展示顺序。这一特性对于提高搜索质量、满足特定业务需求非常有用。 ...

    solr-6.2.0源码

    Solr是Apache软件基金会开发的一款开源全文搜索引擎,它基于Java平台,是Lucene的一个扩展,提供了更为方便和强大的搜索功能。在Solr 6.2.0版本中,这个强大的分布式搜索引擎引入了许多新特性和改进,使其在处理大...

    lucene,solr的使用

    ### Lucene与Solr的使用详解 #### 一、Lucene概述 Lucene是一款高性能、全功能的文本搜索引擎库,由Java语言编写而成。它能够为应用系统提供强大的全文检索能力,是当前最为流行的开源搜索库之一。由于其高度可...

    solr5.4.0完整包

    Solr 依存于Lucene,因为Solr底层的核心技术是使用Lucene 来实现的,Solr和Lucene的本质区别有以下三点:搜索服务器,企业级和管理。Lucene本质上是搜索库,不是独立的应用程序,而Solr是。Lucene专注于搜索底层的...

    Lucene源码解读1

    综上,Lucene源码解读涉及对上述核心组件的工作原理和交互机制的理解,包括索引构建、查询执行、结果排序等。深入研究源码有助于开发者优化性能、定制功能,以及解决特定场景下的问题。不过,此处提供的绩效管理制度...

    Lucene In Action 2源码

    7. **分布式搜索**:如果源码包含高级部分,可能会涉及到Solr或Elasticsearch(基于Lucene的搜索引擎)的使用,展示如何在分布式环境中进行大规模数据的搜索。 8. **性能调优**:源码可能还涵盖了一些性能优化技巧...

    lucene包,lucene实现核心代码

    以下是Lucene实现的核心知识点: 1. **索引过程**: - `IndexWriter`:这是创建和更新Lucene索引的主要类。通过这个类,你可以将文档添加到索引中,或者对已有索引进行修改和删除。 - `Analyzer`:用于分词和标准...

    Annotated Lucene 中文版 Lucene源码剖析

    《Annotated Lucene 中文版 Lucene源码剖析》是一本深入探讨Apache Lucene的书籍,专注于源码解析,帮助读者理解这个强大的全文搜索引擎库的工作原理。Lucene是一款开源的Java库,它提供了高效的文本搜索功能,被...

    Solr Elasticsearch lucene 搜索引擎

    Solr是基于Lucene构建的企业级搜索平台,它扩展了Lucene的功能,增加了许多高级特性,如多核心处理、分布式搜索、缓存、实时索引、丰富的文档处理(XML、JSON等)以及Web界面。Solr使得构建和维护大规模的搜索应用变...

    solr(solr-9.0.0-src.tgz)源码

    - `lucene`: 内含Lucene库源码,Solr的核心搜索引擎库。 4. **关键类与接口** - `SolrCore`: 代表Solr的单一索引实例,包含了索引、查询和其他核心功能。 - `SolrRequestHandler`: 处理Solr请求的接口,用于定义...

    solr6.6.0源码

    Solr 的核心架构基于 Lucene 库,它提供了一个可扩展、高性能的搜索平台。主要组件包括: 1. **索引库**:Solr 使用 Lucene 来创建、维护和搜索索引。 2. **请求处理**:Solr服务器接收HTTP请求,通过...

    Lucene项目的文档和源码

    源码阅读是理解任何软件内部工作原理的最好方式,通过研究Lucene的源码,我们可以深入了解其内部的数据结构、算法实现以及优化技巧。例如,可以学习到如何实现Trie数据结构进行高效查询,或者如何使用BitSet进行布尔...

    solr_lucene3.5_lukeall-3.5.0.jar.zip

    这些库文件可能包含了Lucene的核心库、Solr的服务器端组件,以及其他可能的第三方库,用于支持Solr和Luke的功能。 综上所述,这个压缩包包含了一个针对Lucene 3.5.0版本的Solr环境,以及一个完整的Luke工具,用于...

    paoding-webx3-solr-lucene

    在"paoding-webx3-solr-lucene"项目中,Solr作为后端的核心组件,负责存储和管理经过Paoding分词后的数据,同时提供高效的查询和排序功能。 在这个项目中,Webx3与Solr通过HTTP通信,实现了前后端分离。当用户发起...

    lucene简单介绍及solr搭建使用

    Solr支持多种查询类型,包括标准查询、范围查询、多字段查询等,同时还可以实现高亮显示搜索结果中的关键词。 **实战演练** "01solr企业级搜索引擎准备阶段.pdf"和"02solr企业级搜索引擎实战演练.pdf"可能涵盖了从...

    Lucene in Action 配套源码

    《Lucene in Action》是一本深受开发者欢迎的书籍,它深入浅出地介绍了Apache Lucene这个全文搜索引擎库的使用和实现原理。这本书的配套源码提供了丰富的实例,帮助读者更好地理解Lucene的工作机制,同时也为实际...

Global site tag (gtag.js) - Google Analytics