TermRangeQuery源码解析

简单介绍下 在较早版本的 Lucene 中对一定范围内的查询RanageQuery 。该Query 继承于 MulitTermQuery,在重写(rewriteQuery 树的时候将会遵从一个原则:

根据起始区间值获取term, 然后遍历,根据满足条件的term 的数目来决定重写Query 的类型

如下代码所示:

FilteredTermEnum enumerator = query.getEnum(reader);//PrefixTermEnum
      try {
        while(true) {一个循环,取出与MultiTermQuery相关的所有的Term。
          Term t = enumerator.term();
          if (t != null) {
            pendingTerms.add(t);//将满足条件的term搜集
            // Loading the TermInfo from the terms dict here
            // should not be costly, because 1) the// query/filter will load the TermInfo when it  runs, and 2) the terms dict has a cache:
            docVisitCount += reader.docFreq(t);//获取词频
          }
          // 方式一 :如果Term数目超限,或者文档数目超限,则可能非常影响倒排表合并的性能,采用Filter方式
          if (pendingTerms.size() >= termCountLimit || docVisitCount >= docCountCutoff) {
            // Too many terms -- make a filter.
            Query result = new ConstantScoreQuery(new MultiTermQueryWrapperFilter(query));
            result.setBoost(query.getBoost());
            return result;
            //方式二:如果Term 数目不太多,而且文档数目也不太多,不会影响倒排表合并的性能
          } else  if (!enumerator.next()) {
            // Enumeration is done, and we hit a small
            // enough number of terms & docs -- just make a
            // BooleanQuery, now
            Iterator it = pendingTerms.iterator();
            BooleanQuery bq = new BooleanQuery(true);//构建BooleanQuery
            while(it.hasNext()) {//将区间Query分解成独立的TermQuery后构造成BooleanQuery,最终通过每个TermQuery的结果合并的方式来达到区间查询的目的
              TermQuery tq = new TermQuery((Term) it.next());
              bq.add(tq, BooleanClause.Occur.SHOULD);
            }
            // Strip scores
            Query result = new ConstantScoreQuery(new QueryWrapperFilter(bq));
            result.setBoost(query.getBoost());
            query.incTotalNumberOfTerms(pendingTerms.size());
            return result;
          }
        }
      } finally {
        enumerator.close();
      }
 

  (图一)具体见M ultiTermQuery.ConstantScoreAutoRewrite.rewrite() 方法


两种方式区别:


方式一:如果区间范围较大,获取terms 较多则采取Filter 过滤的方式遍历以start 开始的term ,获取[start,end] 的范围内的 TermEnum 从而取出docIDSet

 

方式二:如果区间范围不大,获取terms 不多,将区间Query 分解成多个termQuery 独立查询,然后根据BooleanQuery 来合并docId

缺点: 

方式一:只支持字符串形式的范围查询,区间满足的term 数据越多,查询性能越差。

方式二:会构造太多termQuery 很可能造成 TooManyClause 异常,而且获取结果再合并将极大影响性能。

    因为方式二其实现和普通BooleanQuery   --> termQuery 查询方式一致,而本文主要阐述Range 查询,所以将不会方式二实现原理。

    OK,那我们看看TermRangeQuery如何实现查询的,我们知道重写Query树后 ,接下来就是生成weight 树,从图一中可以看到方式一中重写的RangeQuey 被包装成 ConstantScoreQuery(new MultiTermQueryWrapperFilter(query)); 那么从下面的代码实现结构可以看到生成的 weight:

  ConstantScoreQuery . createWeight()

|

|-- new ConstantScoreQuery.ConstantWeight(searcher);

生成 Weight 树后, weight 树将负责 Scorer 树的生成,如下代码实现结构所示 :

ConstantWeight. Scorer()

           |

           |-- new ConstantScorer ( similarity , reader, this );

                  |

                 |-- DocIdSet docIdSet  =  MultiTermQueryWrapperFilter .getDocIdSet(reader) ;

                        |--    DocIdSetIterator iter = docIdSet.iterator();

                        |--      docIdSetIterator = iter;

 

Query->weight->Scorer 树生成后,将开始打分并收集 docId 的过程。如下所示:

Scorer scorer = weight.scorer();

          |

          |-- scorer.score(collector);//scorer= ConstantScorer

整个 score 过程是遍历直到取出的值 == NO_MORE_DOCS 。见如下代码所示:

 

  public void score(Collector collector) throws IOException {
    collector.setScorer(this);
    int doc;
    while ((doc = nextDoc()) != NO_MORE_DOCS) {
      collector.collect(doc);
    }
  }
 

 

nextDocConstantScorer 实现:


 public int nextDoc() throws IOException {
      return docIdSetIterator.nextDoc();
    }
 

 

结合ConstantScorer的构造函数可以看到整个docId的范围过滤都在:

 

// filter= MultiTermQueryWrapperFilter得到所有的文档号,形成统一的倒排表,参与倒排表合并。      
DocIdSet docIdSet = filter.getDocIdSet(reader);

  完成,接下来在看看该方法的具体实现:


MultiTermQueryWrapperFilter .getDocIdSet(reader)

               |                   // 得到 TermRangeQueryTerm 枚举器

               |-- final TermEnum enumerator = query .getEnum(reader);

                                                            |         

                                                           |-- new TermRangeTermEnum(reader, field , lowerTerm , upperTerm , includeLower , includeUpper , collator );

TermRangeTermEnum的构造函数 其包含的成员变量如下:

_ String lowerTerm; 左边界字符串

_ String upperTerm; 右边界字符串

_ boolean includeLower; 是否包括左边界

_ boolean includeUpper; 是否包含右边界

_ String field;

_ Collator collator; 其允许用户实现其函数 int compare(String source, String target) 来决定怎么样算是大于,怎么样算是小于。

TermRangeTermEnum 来保证满足区间条件的 term 能被 MultiTermQueryWrapperFilter. TermGenerator.generate() 方法收集到OpenBitSet中,如下所示:

public DocIdSet getDocIdSet(IndexReader reader) throws IOException {  
    final TermEnum enumerator = query.getEnum(reader);得到MultiTermQuery的Term枚举器  
    try {  
      // if current term in enum is null, the enum is empty -> shortcut  
      if (enumerator.term() == null)  
        return DocIdSet.EMPTY_DOCIDSET;  
      // else fill into a OpenBitSet  
      final OpenBitSet bitSet = new OpenBitSet(reader.maxDoc());//创建包含多个Term的文档号集合  
      new TermGenerator() {  
        public void handleDoc(int doc) {  
          bitSet.set(doc);  
        }  
      }.generate(reader, enumerator);  
      return bitSet;  
    } finally {  
      enumerator.close();  
    }  
  }
 

 而generate()具体实现为:

  public void generate(IndexReader reader, TermEnum enumerator) throws IOException {
      final int[] docs = new int[32];
      final int[] freqs = new int[32];
      TermDocs termDocs = reader.termDocs();
      try {
        int termCount = 0;
        do {一个循环,取出对应的所有的Term,取出他们的文档号,加入集合
          Term term = enumerator.term();
          if (term == null)
            break;
          termCount++;
          termDocs.seek(term);//term定位到搜索条件的指定的Term开始往后遍历
          while (true) {
            final int count = termDocs.read(docs, freqs);
            if (count != 0) {
              for(int i=0;i<count;i++) {
                handleDoc(docs[i]);
              }
            } else {
              break;
            }
          }
        } while (enumerator.next());

        query.incTotalNumberOfTerms(termCount);

      } finally {
        termDocs.close();
      }
    }
 

  从上述代码可以看出TermRangeTermEnum最关键的2个方法就是term()和next()方法,

     (1) TermRangeTermEnum.term()方法是获取遍历过程中当前的term。

     (2) TermRangeTermEnum.next()方法是遍历Term的枚举列表

TermRangeTermEnum 并没有重写 next() 方法,所以从父类 FilteredTermEnum 中的 next 可以看到:

 

public boolean next() throws IOException {
        if (actualEnum == null) return false; // the actual enumerator is not initialized!
        currentTerm = null;
        while (currentTerm == null) {
            if (endEnum()) return false;
            if (actualEnum.next()) {
                Term term = actualEnum.term();
                if (termCompare(term)) {// 满足前缀就OK
                    currentTerm = term;
                    return true;
                }
            }
            else return false;
        }
        currentTerm = null;
        return false;
    }
 

 

从上述代码可以得知遍历结束取决:

(1)  endEnum()==true;// 结束枚举遍历

(2)  actualEnum.next()==false;// 整个term 的枚举遍历完毕

(3)  termCompare(term)==false;// 当前的term 字符串不在[start,end] 区间范围内

而从termRanageTermEnumendEnum 其实也是由termCompare(term) 方法来影响,所以人为能影响的区间查询都由termCompare 方法来决定,代码如下所示:

 protected boolean termCompare(Term term) {
    if (collator == null) {//如果用户没有设定collator,则使用字符串比较。
      // Use Unicode code point ordering
      boolean checkLower = false;
      if (!includeLower) // make adjustments to set to exclusive
        checkLower = true;
      if (term != null && term.field() == field) { // interned comparison
        if (!checkLower || null==lowerTermText || term.text().compareTo(lowerTermText) > 0) {
          checkLower = false;
          if (upperTermText != null) {
            int compare = upperTermText.compareTo(term.text());
            最大的termText小于当前的termText值则标示不在区间内所以返回false,
            //同时置endEnum=true;立即结束整个遍历查询过程
            if ((compare < 0) ||
                (!includeUpper && compare==0)) {
              endEnum = true;
              return false;
            }
          }
          return true;
        }
      } else {
        // break
        endEnum = true;
        return false;
      }
      return false;
    } else {//用户设定自己订阅的collator,从而人为控制term值的大小比较
      if (term != null && term.field() == field) { // interned comparison
        if ((lowerTermText == null
            || (includeLower
                ? collator.compare(term.text(), lowerTermText) >= 0
                : collator.compare(term.text(), lowerTermText) > 0))
           && (upperTermText == null
               || (includeUpper
                   ? collator.compare(term.text(), upperTermText) <= 0
                   : collator.compare(term.text(), upperTermText) < 0))) {
          return true;
        }
        return false;
      }
      endEnum = true;
      return false;
    }
  }
 

如上代码所述, 区间查询的时间是和区间范围内term 的个数有关系的,也就是说如果区间范围越大,意味着查询的next() term 的次数也会更多。

另外该范围查询只支持字符串范围查询,并不支持数值型的范围查询。所以 从 Lucene 2.9 开始,Lucene 提供对数字范围的支持,但是然而欲使用此查询,必须使用 NumericField 来添加域。 这也是NumericField和numericRangeQuery结合起来的数值型范围查询。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值