Lucene4.3进阶开发之柳暗花明( 六)

转载请务必注明,原创地址,谢谢配合!
http://qindongliang1922.iteye.com/blog/1999154


上篇文章,散仙介绍了IndexWriter的作用,它的最大价值体现在对索引的创建,管理和维护上,通过与IndexWriterConfig这个配置管理类的组合,可以实现最佳的索引策略,当然前提是你得了解IndexWriterConfig里一些重要的参数的配置含义。


本篇文章散仙要介绍的是IndexSearcher这个类,这个类是Lucene在进行检索时必不可少的一个组件,可以称为是检索的入口,通过这个入口之后,我们就可以获取与我们检索的关键词相关的一系列Doc,然后我们就可以进行后续相关的业务处理。

所以我们经常会在代码里这样写:

Directory directory=FSDirectory.open(new File("D:\\索引测试"));//获取一个索引目录
IndexReader reader=DirectoryReader.open(directory);//返回一个复合Reader=》DirectoryReader
		 //构造IndexSearcher 检索环境
IndexSearcher searcher=new IndexSearcher(reader);

大多数,情况下,我们的代码都是这样写的,实际上IndexSearcher的构造函数有4个,我们最常用的一般有2个,部分源码如下:
final IndexReader reader; // package private for testing!
  
  // NOTE: these members might change in incompatible ways
  // in the next release
  protected final IndexReaderContext readerContext;
  protected final List<AtomicReaderContext> leafContexts;
  /** used with executor - each slice holds a set of leafs executed within one thread */
  protected final LeafSlice[] leafSlices;

  // These are only used for multi-threaded search
  private final ExecutorService executor;

  // the default Similarity
  private static final Similarity defaultSimilarity = new DefaultSimilarity();
  
  /**
   * Expert: returns a default Similarity instance.
   * In general, this method is only called to initialize searchers and writers.
   * User code and query implementations should respect
   * {@link IndexSearcher#getSimilarity()}.
   * @lucene.internal
   */
  public static Similarity getDefaultSimilarity() {
    return defaultSimilarity;
  }
  
  /** The Similarity implementation used by this searcher. */
  private Similarity similarity = defaultSimilarity;

  /** Creates a searcher searching the provided index. */
  public IndexSearcher(IndexReader r) {
   //调用的是2参的构造函数
    this(r,null);
  }

  /** Runs searches for each segment separately, using the
   *  provided ExecutorService.  IndexSearcher will not
   *  shutdown/awaitTermination this ExecutorService on
   *  close; you must do so, eventually, on your own.  NOTE:
   *  if you are using {@link NIOFSDirectory}, do not use
   *  the shutdownNow method of ExecutorService as this uses
   *  Thread.interrupt under-the-hood which can silently
   *  close file descriptors (see <a 
   *  href="https://issues.apache.org/jira/browse/LUCENE-2239">LUCENE-2239</a>).
   * 
   * @lucene.experimental */
  public IndexSearcher(IndexReader r, ExecutorService executor) {
	 
    this(r.getContext(), executor);
  }


看了源码,我们就会发现,我们常用的构造函数实际上是会调用含有线程池并行检索的2参的构造方法,只不过,把线程池设置为null而已,这其实是一个优化的操作,在某些时候能够带来极大的性能提升,这个稍后散仙会详细分析。下面先来看下IndexSearcher里面的一些常用的API方法
方法名 描述
IndexSearcher(IndexReader r) 构建一个搜索实例,使用指定的Reader
IndexSearcher(IndexReader r, ExecutorService executor) 创建一个并行的检索实例,使用ExecutorService 提供的线程池
doc(int docID) 通过一个docid获取一个对应的doc
explain(Query query, int doc) 获取query详细的评分依据信息
getIndexReader() 获取IndexReader实例
search(Query query, int n) 获取前N个检索的结果
search(Query query, Collector results) 通过collector对检索结果进行自定义控制
search(Query query, Filter filter, Collector results) 通过检索,过滤,以及收集,获取一个特定的检索结果
search(Query query, Filter filter, int n) 经过滤后 的前N个结果
search(Query query, Filter filter, int n, Sort sort) 经过滤,排序后的前n个结果
search(Query query, Filter filter, int n, Sort sort, boolean doDocScores, boolean doMaxScore) 对排序后的结果,是否开启评分策略
searchAfter(ScoreDoc after, Query query, int n) 检索上一次query后的数据,通常用来分页使用
setSimilarity(Similarity similarity) 设置自定义的打分策略
search(Weight weight, int nDocs, Sort sort, boolean doDocScores, boolean doMaxScore) 检索指定分数以上的结果


IndexSearcher类里面提供了大量的方法,用来对检索的数据集的限制和过滤,从而达到我们业务需要的一部分数据,当然我们也可也通过setSimilarity方法来设置我们的自定义的打分策略,还可以通过其他的一些方法,来实现排序,过滤,收集,调试打分信息等等。

最后,回到文章开始,散仙来分析下,IndexSearcher的并行构造,如何使用多线程来提升检索性能。

大多数时候,我们默认使用的都是单线程检索,这时候的检索总耗时是顺序检索所有段文件的时间之和,而如果我们使用了并行检索,这时候,我们的检索总耗时,其实就是检索段文件里,耗时最大的那个线程的时间,因为我们是并行检索,所以影响耗时的其实就是检索耗时最长的那个线程的耗时,这有点像“木桶效应”,决定木桶装水的多少,不是由最长的木板决定的,而是由最短的那块木板决定的,反映到这里,其实就是散仙刚提及的耗时可能最长的那个线程,决定了检索的总耗时。

首先呢,这个功能,并不是说所有的场景下,都有明显的作用,比如,我的索引里就只有一个段文件,那么你开启再多的线程也没用,因为这个并行检索,是一个线程对应一个段文件。
另外一种情况,我的索引非常小,然后我又压缩成多个段文件,然后使用这个并行检索去检索数据,其实这时候的性能可能连一个单线程都不如,这也就是单线程与多线程的使用场景的区分,只要正确的理解了什么时候使用单线程,什么时候使用多线程,才有可能达到我们最想要的结果。

所以,这个并行优化的功能,最适合的场景就是我的索引非常大,然后我们把这份索引,压缩成了多个段文件,可能有5个,或者10个以上的段文件,这时候利用这个功能,检索就有很大优势了,下面我们在来看下源码里具体处理:


if (executor == null) {
      return search(leafContexts, weight, after, nDocs);
    } else {
    	//通过一个公用的队列,来合并结果集
      final HitQueue hq = new HitQueue(nDocs, false);
      final Lock lock = new ReentrantLock();//锁
      final ExecutionHelper<TopDocs> runner = new ExecutionHelper<TopDocs>(executor);
    
      for (int i = 0; i < leafSlices.length; i++) { // search each sub
        runner.submit(new SearcherCallableNoSort(lock, this, leafSlices[i], weight, after, nDocs, hq));
      }

      int totalHits = 0;
      float maxScore = Float.NEGATIVE_INFINITY;
      for (final TopDocs topDocs : runner) {
        if(topDocs.totalHits != 0) {
          totalHits += topDocs.totalHits;
          maxScore = Math.max(maxScore, topDocs.getMaxScore());
        }
      }

       //最后从队列里,取值给ScoreDoc进行返回
      final ScoreDoc[] scoreDocs = new ScoreDoc[hq.size()];
      for (int i = hq.size() - 1; i >= 0; i--) // put docs in array
        scoreDocs[i] = hq.pop();



然后在具体的线程类里的实现:
  private static final class SearcherCallableNoSort implements Callable<TopDocs> {

    private final Lock lock;
    private final IndexSearcher searcher;
    private final Weight weight;
    private final ScoreDoc after;
    private final int nDocs;
    private final HitQueue hq;
    private final LeafSlice slice;

    public SearcherCallableNoSort(Lock lock, IndexSearcher searcher, LeafSlice slice,  Weight weight,
        ScoreDoc after, int nDocs, HitQueue hq) {
      this.lock = lock;
      this.searcher = searcher;
      this.weight = weight;
      this.after = after;
      this.nDocs = nDocs;
      this.hq = hq;
      this.slice = slice;
    }

    @Override
    public TopDocs call() throws IOException {
      final TopDocs docs = searcher.search(Arrays.asList(slice.leaves), weight, after, nDocs);
      final ScoreDoc[] scoreDocs = docs.scoreDocs;
      //it would be so nice if we had a thread-safe insert 
      lock.lock();
      try {
        for (int j = 0; j < scoreDocs.length; j++) { // merge scoreDocs into hq
          final ScoreDoc scoreDoc = scoreDocs[j];
          if (scoreDoc == hq.insertWithOverflow(scoreDoc)) {
            break;
          }
        }
      } finally {
        lock.unlock();
      }
      return docs;
    }
  }

通过源码,我们大概可以看出,这个提升,其实是利用了多线程的方式来完成的,通过实现Callable接口,以及重写其的call方法,最后通过公用的全局锁,来控制把检索到的结果集添加到公用的命中队列里,这样一来,一个检索,就被并行的分散到多个线程里,然后最后通过一个全局的容器,来获取所有线程检索的结果,由此以来,在某些场合就能大大提升检索性能。

当然这种提升是否,还跟我们的硬件环境有关系,如果我们的机器CPU不够强劲,或者我们在单核或双核上的机器上跑,可能会出现预期之外的结果,不过,现在的服务器基本都是配置很好的,一般不会出现这种情况。


好了,今天散仙要写的,就到此为止了,感谢各位道友的光临,如有什么不足之处,欢迎指正交流。




转载请务必注明,原创地址,谢谢配合!
http://qindongliang1922.iteye.com/blog/1999154

猜你喜欢

转载自qindongliang.iteye.com/blog/1999154