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

根据起始区间值获取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();
}

(图一)具体见MultiTermQuery.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(newMultiTermQueryWrapperFilter(query));那么从下面的代码实现结构可以看到生成的weight:

ConstantScoreQuery.createWeight()

|

|–newConstantScoreQuery.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);
}
}

而nextDoc由ConstantScorer实现:

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

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

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

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


MultiTermQueryWrapperFilter.getDocIdSet(reader);

|//得到TermRangeQuery的Term枚举器

|–finalTermEnumenumerator=query.getEnum(reader);

|

|–newTermRangeTermEnum(reader,field,lowerTerm,upperTerm,includeLower,includeUpper,collator);

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

_StringlowerTerm;左边界字符串

_StringupperTerm;右边界字符串

_booleanincludeLower;是否包括左边界

_booleanincludeUpper;是否包含右边界

_Stringfield;域

_Collatorcollator;其允许用户实现其函数intcompare(Stringsource,Stringtarget)来决定怎么样算是大于,怎么样算是小于。

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]区间范围内

而从termRanageTermEnum中endEnum其实也是由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的次数也会更多。

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