论坛首页 移动开发技术论坛

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

浏览 2354 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2014-01-05  
上篇文章,散仙介绍了IndexWriter的作用,它的最大价值体现在对索引的创建,管理和维护上,通过与IndexWriterConfig这个配置管理类的组合,可以实现最佳的索引策略,当然前提是你得了解IndexWriterConfig里一些重要的参数的配置含义。


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

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

<pre name="code" class="java">
Directory directory=FSDirectory.open(new File("D:\\索引测试"));//获取一个索引目录
IndexReader reader=DirectoryReader.open(directory);//返回一个复合Reader=》DirectoryReader
//构造IndexSearcher 检索环境
IndexSearcher searcher=new IndexSearcher(reader);</pre>
大多数,情况下,我们的代码都是这样写的,实际上IndexSearcher的构造函数有4个,我们最常用的一般有2个,部分源码如下:
<pre name="code" class="java">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&lt;AtomicReaderContext&gt; 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 &lt;a
   *  href="https://issues.apache.org/jira/browse/LUCENE-2239"&gt;LUCENE-2239&lt;/a&gt;).
   *
   * @lucene.experimental */
  public IndexSearcher(IndexReader r, ExecutorService executor) {

    this(r.getContext(), executor);
  }</pre>

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

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

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

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

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

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


<pre name="code" class="java">if (executor == null) {
      return search(leafContexts, weight, after, nDocs);
    } else {
    //通过一个公用的队列,来合并结果集
      final HitQueue hq = new HitQueue(nDocs, false);
      final Lock lock = new ReentrantLock();//锁
      final ExecutionHelper&lt;TopDocs&gt; runner = new ExecutionHelper&lt;TopDocs&gt;(executor);
   
      for (int i = 0; i &lt; 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 &gt;= 0; i--) // put docs in array
        scoreDocs[i] = hq.pop();</pre>


然后在具体的线程类里的实现:
<pre name="code" class="java">  private static final class SearcherCallableNoSort implements Callable&lt;TopDocs&gt; {

    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 &lt; scoreDocs.length; j++) { // merge scoreDocs into hq
          final ScoreDoc scoreDoc = scoreDocs[j];
          if (scoreDoc == hq.insertWithOverflow(scoreDoc)) {
            break;
          }
        }
      } finally {
        lock.unlock();
      }
      return docs;
    }
  }
</pre>
通过源码,我们大概可以看出,这个提升,其实是利用了多线程的方式来完成的,通过实现Callable接口,以及重写其的call方法,最后通过公用的全局锁,来控制把检索到的结果集添加到公用的命中队列里,这样一来,一个检索,就被并行的分散到多个线程里,然后最后通过一个全局的容器,来获取所有线程检索的结果,由此以来,在某些场合就能大大提升检索性能。

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


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




论坛首页 移动开发技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics