Lucene学习总结之九:Lucene的查询对象
转自:http://www.cnblogs.com/forfuture1978/archive/2010/05/19/1738803.html
Lucene除了支持查询语法以外,还可以自己构造查询对象进行搜索。
从上一节的Lucene的语法一章可以知道,能与查询语句对应的查询对象有:BooleanQuery,FuzzyQuery,MatchAllDocsQuery,MultiTermQuery,MultiPhraseQuery,PhraseQuery,PrefixQuery,TermRangeQuery,TermQuery,WildcardQuery。
Lucene还支持一些查询对象并没有查询语句与之对应,但是能够实现相对高级的功能,本节主要讨论这些高级的查询对象。
它们中间最主要的一些层次结构如下,我们将一一解析。
Query
BoostingQuery
CustomScoreQuery
MoreLikeThisQuery
MultiTermQuery
NumericRangeQuery<T>
TermRangeQuery
SpanQuery
FieldMaskingSpanQuery
SpanFirstQuery
SpanNearQuery
PayloadNearQuery
SpanNotQuery
SpanOrQuery
SpanRegexQuery
SpanTermQuery
PayloadTermQuery
FilteredQuery
1、BoostingQuery
BoostingQuery包含三个成员变量:
Query match:这是结果集必须满足的查询对象
Query context:此查询对象不对结果集产生任何影响,仅在当文档包含context查询的时候,将文档打分乘上boost
float boost
在BoostingQuery构造函数中:
public BoostingQuery(Query match, Query context, float boost) {
this.match = match;
this.context = (Query)context.clone();
this.boost = boost;
this.context.setBoost(0.0f);
}
在BoostingQuery的rewrite函数如下:
public Query rewrite(IndexReader reader) throws IOException {
BooleanQuery result = new BooleanQuery() {
@Override
public Similarity getSimilarity(Searcher searcher) {
return new DefaultSimilarity() {
@Override
public float coord(int overlap, int max) {
switch (overlap) {
case 1:
return 1.0f;
case 2:
return boost;
default:
return 0.0f;
}
}
};
}
};
result.add(match, BooleanClause.Occur.MUST);
result.add(context, BooleanClause.Occur.SHOULD);
return result;
}
由上面实现可知,BoostingQuery最终生成一个BooleanQuery,第一项是match查询,是MUST,即required,第二项是context查询,是SHOULD,即optional
然而由查询过程分析可得,即便是optional的查询,也会影响整个打分。
所以在BoostingQuery的构造函数中,设定context查询的boost为零,则无论文档是否包含context查询,都不会影响最后的打分。
在rewrite函数中,重载了DefaultSimilarity的coord函数,当仅包含match查询的时候,其返回1,当既包含match查询,又包含context查询的时候,返回boost,也即会在最后的打分中乘上boost的值。
下面我们做实验如下:
索引如下文件:
file01: apple other other other boyfile02: apple apple other other other
file03: apple apple apple other other
file04: apple apple apple apple other
对于如下查询(1):
TermQuery must = new TermQuery(new Term("contents","apple"));TermQuery context = new TermQuery(new Term("contents","boy"));BoostingQuery query = new BoostingQuery(must, context, 1f);
或者如下查询(2):
TermQuery query = new TermQuery(new Term("contents","apple"));
两者的结果是一样的,如下:
docid : 3 score : 0.67974937docid : 2 score : 0.58868027docid : 1 score : 0.4806554docid : 0 score : 0.33987468
自然是包含apple越多的文档打分越高。
然而他们的打分计算过程却不同,用explain得到查询(1)打分细节如下:
docid : 0 score : 0.339874680.33987468 = (MATCH) fieldWeight(contents:apple in 0), product of: 1.0 = tf(termFreq(contents:apple)=1) 0.7768564 = idf(docFreq=4, maxDocs=4) 0.4375 = fieldNorm(field=contents, doc=0)
explain得到的查询(2)的打分细节如下:
docid : 0 score : 0.339874680.33987468 = (MATCH) sum of: 0.33987468 = (MATCH) fieldWeight(contents:apple in 0), product of: 1.0 = tf(termFreq(contents:apple)=1) 0.7768564 = idf(docFreq=4, maxDocs=4) 0.4375 = fieldNorm(field=contents, doc=0) 0.0 = (MATCH) weight(contents:boy^0.0 in 0), product of: 0.0 = queryWeight(contents:boy^0.0), product of: 0.0 = boost 1.6931472 = idf(docFreq=1, maxDocs=4) 1.2872392 = queryNorm 0.74075186 = (MATCH) fieldWeight(contents:boy in 0), product of: 1.0 = tf(termFreq(contents:boy)=1) 1.6931472 = idf(docFreq=1, maxDocs=4) 0.4375 = fieldNorm(field=contents, doc=0)
可以知道,查询(2)中,boy的部分是计算了的,但是由于boost为0被忽略了。
让我们改变boost,将包含boy的文档打分乘以10:
TermQuery must = new TermQuery(new Term("contents","apple"));TermQuery context = new TermQuery(new Term("contents","boy"));BoostingQuery query = new BoostingQuery(must, context, 10f);
结果如下:
docid : 0 score : 3.398747docid : 3 score : 0.67974937docid : 2 score : 0.58868027docid : 1 score : 0.4806554
explain得到的打分细节如下:
docid : 0 score : 3.3987473.398747 = (MATCH) product of: 0.33987468 = (MATCH) sum of: 0.33987468 = (MATCH) fieldWeight(contents:apple in 0), product of: 1.0 = tf(termFreq(contents:apple)=1) 0.7768564 = idf(docFreq=4, maxDocs=4) 0.4375 = fieldNorm(field=contents, doc=0) 0.0 = (MATCH) weight(contents:boy^0.0 in 0), product of: 0.0 = queryWeight(contents:boy^0.0), product of: 0.0 = boost 1.6931472 = idf(docFreq=1, maxDocs=4) 1.2872392 = queryNorm 0.74075186 = (MATCH) fieldWeight(contents:boy in 0), product of: 1.0 = tf(termFreq(contents:boy)=1) 1.6931472 = idf(docFreq=1, maxDocs=4) 0.4375 = fieldNorm(field=contents, doc=0) 10.0 = coord(2/2)
2、CustomScoreQuery
CustomScoreQuery主要包含以下成员变量:
Query subQuery:子查询
ValueSourceQuery[] valSrcQueries:其他信息源
ValueSourceQuery主要包含ValueSource valSrc成员变量,其代表一个信息源。
ValueSourceQuery会在查询过程中生成ValueSourceWeight并最终生成ValueSourceScorer,ValueSourceScorer在score函数如下:
public float score() throws IOException {
return qWeight * vals.floatVal(termDocs.doc());
}
其中vals = valSrc.getValues(reader)类型为DocValues,也即可以根据文档号得到值。
也即CustomScoreQuery会根据子查询和其他的信息源来共同决定最后的打分,而且公式可以自己实现,以下是默认实现:
public float customScore(int doc, float subQueryScore, float valSrcScores[]) {
if (valSrcScores.length == 1) {
return customScore(doc, subQueryScore, valSrcScores[0]);
}
if (valSrcScores.length == 0) {
return customScore(doc, subQueryScore, 1);
}
float score = subQueryScore;
for(int i = 0; i < valSrcScores.length; i++) {
score *= valSrcScores[i];
}
return score;
}
一般是什么样的信息源会对文档的打分有影响的?
比如说文章的作者,可能被保存在Field当中,我们可以认为名人的文章应该打分更高,所以可以根据此Field的值来影响文档的打分。
然而我们知道,如果对每一个文档号都用reader读取域的值会影响速度,所以Lucene引入了FieldCache来进行缓存,而FieldCache并非在存储域中读取,而是在索引域中读取,从而不必构造Document对象,然而要求此索引域是不分词的,有且只有一个Token。
所以有FieldCacheSource继承于ValueSource,而大多数的信息源都继承于FieldCacheSource,其最重要的一个函数即:
public final DocValues getValues(IndexReader reader) throws IOException {
return getCachedFieldValues(FieldCache.DEFAULT, field, reader);
}
我们举ByteFieldSource为例,其getCachedFieldValues函数如下:
public DocValues getCachedFieldValues (FieldCache cache, String field, IndexReader reader) throws IOException {
final byte[] arr = cache.getBytes(reader, field, parser);
return new DocValues() {
@Override
public float floatVal(int doc) {
return (float) arr[doc];
}
@Override
public int intVal(int doc) {
return arr[doc];
}
@Override
public String toString(int doc) {
return description() + '=' + intVal(doc);
}
@Override
Object getInnerArray() {
return arr;
}
};
}
其最终可以用DocValues根据文档号得到一个float值,并影响打分。
还用作者的例子,假设我们给每一个作者一个float的评级分数,保存在索引域中,用CustomScoreQuery可以将此评级融入到打分中去。
FieldScoreQuery即是ValueSourceQuery的一个实现。
举例如下:
索引如下文件:
file01: apple other other other boyfile02: apple apple other other other
file03: apple apple apple other other
file04: apple apple apple apple other
在索引过程中,对file01的"scorefield"域中索引"10",而其他的文件"scorefield"域中索引"1",代码如下:
Document doc = new Document();doc.add(new Field("contents", new FileReader(file)));if(file.getName().contains("01")){ doc.add(new Field("scorefield", "10", Field.Store.NO, Field.Index.NOT_ANALYZED));} else { doc.add(new Field("scorefield", "1", Field.Store.NO, Field.Index.NOT_ANALYZED));}writer.addDocument(doc);
对于建好的索引,如果进行如下查询TermQuery query = new TermQuery(new Term("contents", "apple"));
则得到如下结果:
docid : 3 score : 0.67974937docid : 2 score : 0.58868027docid : 1 score : 0.4806554docid : 0 score : 0.33987468
自然是包含"apple"多的文档打分较高。
然而如果使用CustomScoreQuery进行查询:
TermQuery subquery = new TermQuery(new Term("contents","apple"));FieldScoreQuery scorefield = new FieldScoreQuery("scorefield", FieldScoreQuery.Type.BYTE);CustomScoreQuery query = new CustomScoreQuery(subquery, scorefield);
则得到如下结果:
docid : 0 score : 1.6466033docid : 3 score : 0.32932067docid : 2 score : 0.28520006docid : 1 score : 0.23286487
显然文档0因为设置了数据源评分为10而跃居首位。
如果进行explain,我们可以看到,对于普通的查询,文档0的打分细节如下:
docid : 0 score : 0.339874680.33987468 = (MATCH) fieldWeight(contents:apple in 0), product of: 1.0 = tf(termFreq(contents:apple)=1) 0.7768564 = idf(docFreq=4, maxDocs=4) 0.4375 = fieldNorm(field=contents, doc=0)
如果对于CustomScoreQuery,文档0的打分细节如下:
docid : 0 score : 1.64660331.6466033 = (MATCH) custom(contents:apple, byte(scorefield)), product of: 1.6466033 = custom score: product of: 0.20850874 = (MATCH) weight(contents:apple in 0), product of: 0.6134871 = queryWeight(contents:apple), product of: 0.7768564 = idf(docFreq=4, maxDocs=4) 0.7897047 = queryNorm 0.33987468 = (MATCH) fieldWeight(contents:apple in 0), product of: 1.0 = tf(termFreq(contents:apple)=1) 0.7768564 = idf(docFreq=4, maxDocs=4) 0.4375 = fieldNorm(field=contents, doc=0) 7.897047 = (MATCH) byte(scorefield), product of: 10.0 = byte(scorefield)=10 1.0 = boost 0.7897047 = queryNorm 1.0 = queryBoost
3、MoreLikeThisQuery
在分析MoreLikeThisQuery之前,首先介绍一下MoreLikeThis。
在实现搜索应用的时候,时常会遇到"更多相似文章","更多相关问题"之类的需求,也即根据当前文档的文本内容,在索引库中查询相类似的文章。
我们可以使用MoreLikeThis实现此功能:
IndexReader reader = IndexReader.open(……);
IndexSearcher searcher = new IndexSearcher(reader);
MoreLikeThis mlt = new MoreLikeThis(reader);
Reader target = ... //此是一个io reader,指向当前文档的文本内容。
Query query = mlt.like( target); //根据当前的文本内容,生成查询对象。
Hits hits = searcher.search(query); //查询得到相似文档的结果。
MoreLikeThis的Query like(Reader r)函数如下:
public Query like(Reader r) throws IOException {
return createQuery(retrieveTerms(r)); //其首先从当前文档的文本内容中抽取term,然后利用这些term构建一个查询对象。
}
public PriorityQueue <Object[]> retrieveTerms(Reader r) throws IOException {
Map<String,Int> words = new HashMap<String,Int>();
//根据不同的域中抽取term,到底根据哪些域抽取,可用函数void setFieldNames(String[] fieldNames)设定。
for (int i = 0; i < fieldNames.length; i++) {
String fieldName = fieldNames[i];
addTermFrequencies(r, words, fieldName);
}
//将抽取的term放入优先级队列中
return createQueue(words);
}
private void addTermFrequencies(Reader r, Map<String,Int> termFreqMap, String fieldName) throws IOException
{
//首先对当前的文本进行分词,分词器可以由void setAnalyzer(Analyzer analyzer)设定。
TokenStream ts = analyzer.tokenStream(fieldName, r);
int tokenCount=0;
TermAttribute termAtt = ts.addAttribute(TermAttribute.class);
//遍历分好的每一个词
while (ts.incrementToken()) {
String word = termAtt.term();
tokenCount++;
//如果分词后的term的数量超过某个设定的值,则停止,可由void setMaxNumTokensParsed(int i)设定。
if(tokenCount>maxNumTokensParsed)
{
break;
}
//如果此词小于最小长度,或者大于最大长度,或者属于停词,则属于干扰词。
//最小长度由void setMinWordLen(int minWordLen)设定。
//最大长度由void setMaxWordLen(int maxWordLen)设定。
//停词表由void setStopWords(Set<?> stopWords)设定。
if(isNoiseWord(word)){
continue;
}
// 统计词频tf
Int cnt = termFreqMap.get(word);
if (cnt == null) {
termFreqMap.put(word, new Int());
}
else {
cnt.x++;
}
}
}
private PriorityQueue createQueue(Map<String,Int> words) throws IOException {
//根据统计的term及词频构造优先级队列。
int numDocs = ir.numDocs();
FreqQ res = new FreqQ(words.size()); // 优先级队列,将按tf*idf排序
Iterator<String> it = words.keySet().iterator();
//遍历每一个词
while (it.hasNext()) {
String word = it.next();
int tf = words.get(word).x;
//如果词频小于最小词频,则忽略此词,最小词频可由void setMinTermFreq(int minTermFreq)设定。
if (minTermFreq > 0 && tf < minTermFreq) {
continue;
}
//遍历所有域,得到包含当前词,并且拥有最大的doc frequency的域
String topField = fieldNames[0];
int docFreq = 0;
for (int i = 0; i < fieldNames.length; i++) {
int freq = ir.docFreq(new Term(fieldNames[i], word));
topField = (freq > docFreq) ? fieldNames[i] : topField;
docFreq = (freq > docFreq) ? freq : docFreq;
}
//如果文档频率小于最小文档频率,则忽略此词。最小文档频率可由void setMinDocFreq(int minDocFreq)设定。
if (minDocFreq > 0 && docFreq < minDocFreq) {
continue;
}
//如果文档频率大于最大文档频率,则忽略此词。最大文档频率可由void setMaxDocFreq(int maxFreq)设定。
if (docFreq > maxDocFreq) {
continue;
}
if (docFreq == 0) {
continue;
}
//计算打分tf*idf
float idf = similarity.idf(docFreq, numDocs);
float score = tf * idf;
//将object的数组放入优先级队列,只有前三项有用,按照第三项score排序。
res.insertWithOverflow(new Object[]{word, // 词
topField, // 域
Float.valueOf(score), // 打分
Float.valueOf(idf), // idf
Integer.valueOf(docFreq), // 文档频率
Integer.valueOf(tf) //词频
});
}
return res;
}
private Query createQuery(PriorityQueue q) {
//最后生成的是一个布尔查询
BooleanQuery query = new BooleanQuery();
Object cur;
int qterms = 0;
float bestScore = 0;
//不断从队列中优先取出打分最高的词
while (((cur = q.pop()) != null)) {
Object[] ar = (Object[]) cur;
TermQuery tq = new TermQuery(new Term((String) ar[1], (String) ar[0]));
if (boost) {
if (qterms == 0) {
//第一个词的打分最高,作为bestScore
bestScore = ((Float) ar[2]).floatValue();
}
float myScore = ((Float) ar[2]).floatValue();
//其他的词的打分除以最高打分,乘以boostFactor,得到相应的词所生成的查询的boost,从而在当前文本文档中打分越高的词在查询语句中也有更高的boost,起重要的作用。
tq.setBoost(boostFactor * myScore / bestScore);
}
try {
query.add(tq, BooleanClause.Occur.SHOULD);
}
catch (BooleanQuery.TooManyClauses ignore) {
break;
}
qterms++;
//如果超过了设定的最大的查询词的数目,则停止,最大查询词的数目可由void setMaxQueryTerms(int maxQueryTerms)设定。
if (maxQueryTerms > 0 && qterms >= maxQueryTerms) {
break;
}
}
return query;
}
MoreLikeThisQuery只是MoreLikeThis的封装,其包含了MoreLikeThis所需要的参数,并在rewrite的时候,由MoreLikeThis.like生成查询对象。
String likeText;当前文档的文本
String[] moreLikeFields;根据哪个域来抽取查询词
Analyzer analyzer;分词器
float percentTermsToMatch=0.3f;最后生成的BooleanQuery之间都是SHOULD的关系,其中至少有多少比例必须得到满足
int minTermFrequency=1;最少的词频
int maxQueryTerms=5;最多的查询词数目
Set<?> stopWords=null;停词表
int minDocFreq=-1;最小的文档频率
public Query rewrite(IndexReader reader) throws IOException
{
MoreLikeThis mlt=new MoreLikeThis(reader);
mlt.setFieldNames(moreLikeFields);
mlt.setAnalyzer(analyzer);
mlt.setMinTermFreq(minTermFrequency);
if(minDocFreq>=0)
{
mlt.setMinDocFreq(minDocFreq);
}
mlt.setMaxQueryTerms(maxQueryTerms);
mlt.setStopWords(stopWords);
BooleanQuery bq= (BooleanQuery) mlt.like(new ByteArrayInputStream(likeText.getBytes()));
BooleanClause[] clauses = bq.getClauses();
bq.setMinimumNumberShouldMatch((int)(clauses.length*percentTermsToMatch));
return bq;
}
举例,对于http://topic.csdn.net/u/20100501/09/64e41f24-e69a-40e3-9058-17487e4f311b.html?1469中的帖子
我们姑且将相关问题中的帖子以及其他共20篇文档索引。
File indexDir = new File("TestMoreLikeThisQuery/index");
IndexReader reader = IndexReader.open(indexDir);
IndexSearcher searcher = new IndexSearcher(reader);
//将《IT外企那点儿事》作为likeText,从文件读入。
StringBuffer contentBuffer = new StringBuffer();
BufferedReader input = new BufferedReader(new InputStreamReader(new FileInputStream("TestMoreLikeThisQuery/IT外企那点儿事.txt"), "utf-8"));
String line = null;
while((line = input.readLine()) != null){
contentBuffer.append(line);
}
String content = contentBuffer.toString();
//分词用中科院分词
MoreLikeThisQuery query = new MoreLikeThisQuery(content, new String[]{"contents"}, new MyAnalyzer(new ChineseAnalyzer()));
//将80%都包括的词作为停词,在实际应用中,可以有其他的停词策略。
query.setStopWords(getStopWords(reader));
//至少包含5个的词才认为是重要的
query.setMinTermFrequency(5);
//只取其中之一
query.setMaxQueryTerms(1);
TopDocs docs = searcher.search(query, 50);
for (ScoreDoc doc : docs.scoreDocs) {
Document ldoc = reader.document(doc.doc);
String title = ldoc.get("title");
System.out.println(title);
}
static Set<String> getStopWords(IndexReader reader) throws IOException{
HashSet<String> stop = new HashSet<String>();
int numOfDocs = reader.numDocs();
int stopThreshhold = (int) (numOfDocs*0.7f);
TermEnum te = reader.terms();
while(te.next()){
String text = te.term().text();
if(te.docFreq() >= stopThreshhold){
stop.add(text);
}
}
return stop;
}
结果为:
揭开外企的底儿(连载六)——外企招聘也有潜规则.txt
去央企还是外企,帮忙分析下.txt
哪种英语教材比较适合英语基础差的人.txt
有在达内外企软件工程师就业班培训过的吗.txt
两个月的“骑驴找马”,面试无数家公司的深圳体验.txt
一个看了可能改变你一生的小说《做单》,外企销售经理做单技巧大揭密.txt
HR的至高机密:20个公司绝对不会告诉你的潜规则.txt
4、MultiTermQuery
此类查询包含一到多个Term的查询,主要包括FuzzyQuery,PrefixQuery,WildcardQuery,NumericRangeQuery<T>,TermRangeQuery。
本章主要讨论后两者。
4.1、TermRangeQuery
在较早版本的Lucene,对一定范围内的查询所对应的查询对象是RangeQuery,然而其仅支持字符串形式的范围查询,因为Lucene 3.0提供了数字形式的范围查询NumericRangeQuery,所以原来的RangeQuery变为TermRangeQuery。
其包含的成员变量如下:
String lowerTerm; 左边界字符串
String upperTerm; 右边界字符串
boolean includeLower; 是否包括左边界
boolean includeUpper; 是否包含右边界
String field; 域
Collator collator; 其允许用户实现其函数int compare(String source, String target)来决定怎么样算是大于,怎么样算是小于
其提供函数FilteredTermEnum getEnum(IndexReader reader)用于得到属于此范围的所有Term:
protected FilteredTermEnum getEnum(IndexReader reader) throws IOException {
return new TermRangeTermEnum(reader, field, lowerTerm, upperTerm, includeLower, includeUpper, collator);
}
FilteredTermEnum不断取下一个Term的next函数如下:
public boolean next() throws IOException {
if (actualEnum == null) return false;
currentTerm = null;
while (currentTerm == null) {
if (endEnum()) return false;
if (actualEnum.next()) {
Term term = actualEnum.term();
if (termCompare(term)) {
currentTerm = term;
return true;
}
}
else return false;
}
currentTerm = null;
return false;
}
其中调用termCompare来判断此Term是否在范围之内,TermRangeTermEnum的termCompare如下:
protected boolean termCompare(Term term) {
if (collator == null) {
//如果用户没有设定collator,则使用字符串比较。
boolean checkLower = false;
if (!includeLower)
checkLower = true;
if (term != null && term.field() == field) {
if (!checkLower || null==lowerTermText || term.text().compareTo(lowerTermText) > 0) {
checkLower = false;
if (upperTermText != null) {
int compare = upperTermText.compareTo(term.text());
if ((compare < 0) ||
(!includeUpper && compare==0)) {
endEnum = true;
return false;
}
}
return true;
}
} else {
endEnum = true;
return false;
}
return false;
} else {
//如果用户设定了collator,则使用collator来比较字符串。
if (term != null && term.field() == field) {
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;
}
}
由前面分析的MultiTermQuery的rewrite可以知道,TermRangeQuery可能生成BooleanQuery,然而当此范围过大,或者范围内的Term过多的时候,可能出现TooManyClause异常。
另一种方式可以用TermRangeFilter,并不变成查询对象,而是对查询结果进行过滤,在Filter一节详细介绍。
4.2、NumericRangeQuery
从Lucene 2.9开始,提供对数字范围的支持,然而欲使用此查询,必须使用NumericField添加域:
document.add(new NumericField(name).setIntValue(value));
或者使用NumericTokenStream添加域:
Field field = new Field(name, new NumericTokenStream(precisionStep).setIntValue(value));
field.setOmitNorms(true);
field.setOmitTermFreqAndPositions(true);
document.add(field);
NumericRangeQuery可因不同的类型用如下方法生成:
newDoubleRange(String, Double, Double, boolean, boolean)
newFloatRange(String, Float, Float, boolean, boolean)
newIntRange(String, Integer, Integer, boolean, boolean)
newLongRange(String, Long, Long, boolean, boolean)
public static NumericRangeQuery<Integer> newIntRange(final String field, Integer min, Integer max, final boolean minInclusive, final boolean maxInclusive) {
return new NumericRangeQuery<Integer>(field, NumericUtils.PRECISION_STEP_DEFAULT, 32, min, max, minInclusive, maxInclusive);
}
其提供函数FilteredTermEnum getEnum(IndexReader reader)用于得到属于此范围的所有Term:
protected FilteredTermEnum getEnum(final IndexReader reader) throws IOException {
return new NumericRangeTermEnum(reader);
}
NumericRangeTermEnum的termCompare如下:
protected boolean termCompare(Term term) {
return (term.field() == field && term.text().compareTo(currentUpperBound) <= 0);
}
另一种方式可以使用NumericRangeFilter,下面会详细论述。
举例,我们索引id从0到9的十篇文档到索引中:
Document doc = new Document();
doc.add(new Field("contents", new FileReader(file)));
String name = file.getName();
Integer id = Integer.parseInt(name);
doc.add(new NumericField("id").setIntValue(id));
writer.addDocument(doc);
搜索的时候,生成NumericRangeQuery:
File indexDir = new File("TestNumericRangeQuery/index");
IndexReader reader = IndexReader.open(FSDirectory.open(indexDir));
IndexSearcher searcher = new IndexSearcher(reader);
NumericRangeQuery<Integer> query = NumericRangeQuery.newIntRange("id", 3, 6, true, false);
TopDocs docs = searcher.search(query, 50);
for (ScoreDoc doc : docs.scoreDocs) {
System.out.println("docid : " + doc.doc + " score : " + doc.score);
}
结果如下:
docid : 3 score : 1.0docid : 4 score : 1.0docid : 5 score : 1.0
5、SpanQuery
所谓SpanQuery也即在查询过程中需要考虑进Term的位置信息的查询对象。
SpanQuery中最基本的是SpanTermQuery,其只包含一个Term,与TermQuery所不同的是,其提供一个函数来得到位置信息:
public Spans getSpans(final IndexReader reader) throws IOException {
return new TermSpans(reader.termPositions(term), term);
}
Spans有以下方法:
next() 得到下一篇文档号,不同的SpanQuery此方法实现不同
skipTo(int) 跳到指定的文档
doc() 得到当前的文档号
start() 得到起始位置,不同的SpanQuery此方法实现不同
end() 得到结束位置,不同的SpanQuery此方法实现不同
isPayloadAvailable() 是否有payload
getPayload() 得到payload
SpanScorer的nextDoc函数如下:
public int nextDoc() throws IOException {
if (!setFreqCurrentDoc()) {
doc = NO_MORE_DOCS;
}
return doc;
}
protected boolean setFreqCurrentDoc() throws IOException {
if (!more) {
return false;
}
doc = spans.doc();
freq = 0.0f;
do {
//根据结束位置和起始位置来计算freq从而影响打分
int matchLength = spans.end() - spans.start();
freq += getSimilarity().sloppyFreq(matchLength);
more = spans.next();
} while (more && (doc == spans.doc()));
return true;
}
5.1、SpanFirstQuery
SpanFirstQuery仅取在开头部分包含查询词的文档,其包含如下成员变量:
SpanQuery match; 需要满足的查询
int end; 如何定义开头
其getSpans函数如下:
public Spans getSpans(final IndexReader reader) throws IOException {
return new Spans() {
private Spans spans = match.getSpans(reader);
@Override
public boolean next() throws IOException {
while (spans.next()) {
//仅查询词的位置在设定的end之前的文档才返回。
if (end() <= end)
return true;
}
return false;
}
@Override
public boolean skipTo(int target) throws IOException {
if (!spans.skipTo(target))
return false;
return spans.end() <= end || next();
}
@Override
public int doc() { return spans.doc(); }
@Override
public int start() { return spans.start(); }
@Override
public int end() { return spans.end(); }
};
}
5.2、SpanNearQuery
SpanNearQuery包含以下成员变量:
List<SpanQuery> clauses; 一个列表的子SpanQuery
int slop; 设定这些字SpanQuery之间的距离的最大值,大于此值则文档不返回。
boolean inOrder; 是否按顺序计算子SpanQuery之间的距离
String field; 域
boolean collectPayloads; 是否收集payload
其getSpans函数如下:
public Spans getSpans(final IndexReader reader) throws IOException {
if (clauses.size() == 0)
return new SpanOrQuery(getClauses()).getSpans(reader);
if (clauses.size() == 1)
return clauses.get(0).getSpans(reader);
return inOrder
? (Spans) new NearSpansOrdered(this, reader, collectPayloads)
: (Spans) new NearSpansUnordered(this, reader);
}
是否inorder,举例如下:
假设索引了文档"apple boy cat",如果将SpanNearQuery的clauses依次设为"apple","cat","boy",如果inorder=true,则文档不会被搜索出来,即便slop设为很大,如果inorder=false,则文档会被搜出来,而且slop设为0就能被搜出来。
因为在NearSpansOrdered的next函数如下:
public boolean next() throws IOException {
if (firstTime) {
firstTime = false;
for (int i = 0; i < subSpans.length; i++) {
//每个子SpanQuery都取第一篇文档
if (! subSpans[i].next()) {
more = false;
return false;
}
}
more = true;
}
if(collectPayloads) {
matchPayload.clear();
}
return advanceAfterOrdered();
}
private boolean advanceAfterOrdered() throws IOException {
//如果各子SpanQuery指向同一文档
while (more && (inSameDoc || toSameDoc())) {
//stretchToOrder要保证各子SpanQuery一定是按照顺序排列的
//shrinkToAfterShortestMatch保证各子SpanQuery之间的距离不大于slop
if (stretchToOrder() && shrinkToAfterShortestMatch()) {
return true;
}
}
return false;
}
private boolean stretchToOrder() throws IOException {
matchDoc = subSpans[0].doc();
for (int i = 1; inSameDoc && (i < subSpans.length); i++) {
//docSpansOrdered要保证第i-1个子SpanQuery的start和end都应在第i个之前,否则取下一篇文档。
while (! docSpansOrdered(subSpans[i-1], subSpans[i])) {
if (! subSpans[i].next()) {
inSameDoc = false;
more = false;
break;
} else if (matchDoc != subSpans[i].doc()) {
inSameDoc = false;
break;
}
}
}
return inSameDoc;
}
static final boolean docSpansOrdered(Spans spans1, Spans spans2) {
assert spans1.doc() == spans2.doc() : "doc1 " + spans1.doc() + " != doc2 " + spans2.doc();
int start1 = spans1.start();
int start2 = spans2.start();
return (start1 == start2) ? (spans1.end() < spans2.end()) : (start1 < start2);
}
private boolean shrinkToAfterShortestMatch() throws IOException {
//从最后一个子SpanQuery开始
matchStart = subSpans[subSpans.length - 1].start();
matchEnd = subSpans[subSpans.length - 1].end();
int matchSlop = 0;
int lastStart = matchStart;
int lastEnd = matchEnd;
for (int i = subSpans.length - 2; i >= 0; i—) {
//不断的取前一个子SpanQuery
Spans prevSpans = subSpans[i];
int prevStart = prevSpans.start();
int prevEnd = prevSpans.end();
while (true) {
if (! prevSpans.next()) {
inSameDoc = false;
more = false;
break;
} else if (matchDoc != prevSpans.doc()) {
inSameDoc = false;
break;
} else {
int ppStart = prevSpans.start();
int ppEnd = prevSpans.end();
if (! docSpansOrdered(ppStart, ppEnd, lastStart, lastEnd)) {
break;
} else {
prevStart = ppStart;
prevEnd = ppEnd;
}
}
}
assert prevStart <= matchStart;
if (matchStart > prevEnd) {
//总是从下一个的开始位置,减去前一个的结束位置,所以上面的例子中,如果将SpanNearQuery的clauses依次设为"apple","boy","cat",inorder=true, slop=0,是能够搜索的出的。
matchSlop += (matchStart - prevEnd);
}
matchStart = prevStart;
lastStart = prevStart;
lastEnd = prevEnd;
}
boolean match = matchSlop <= allowedSlop;
return match;
}
NearSpansUnordered的next函数如下:
public boolean next() throws IOException {
if (firstTime) {
//将一个Spans生成一个SpansCell,既放入链表中,也放入优先级队列中,在队列中按照第一篇文档号由小到大排列,若文档号相同,则按照位置顺序排列。
initList(true);
listToQueue();
firstTime = false;
} else if (more) {
if (min().next()) { //最上面的取下一篇文档,并调整队列。
queue.updateTop();
} else {
more = false;
}
}
while (more) {
boolean queueStale = false;
if (min().doc() != max.doc()) { //如果队列中最小的文档号和最大的文档号不相同,将队列生成链表。
queueToList();
queueStale = true;
}
//应该不断的skip每个子SpanQuery直到最小的文档号和最大的文档号相同,不同的是在文档中的位置。
while (more && first.doc() < last.doc()) {
more = first.skipTo(last.doc());
firstToLast();
queueStale = true;
}
if (!more) return false;
//调整完毕后,将链表写回队列。
if (queueStale) {
listToQueue();
queueStale = false;
}
//判断是否匹配
if (atMatch()) {
return true;
}
more = min().next();
if (more) {
queue.updateTop();
}
}
return false;
}
private boolean atMatch() {
//匹配有两个条件,一个是最小和最大的文档号相同,一个是最大的结束位置减去最小的开始位置再减去最大和最小的自身的长度之和小于等于slop。
//在上面的例子中,如果将SpanNearQuery的clauses依次设为"cat","apple",inorder=false,则slop设为1可以搜索的出来。因为"cat".end = 3, "apple".start=0, totalLength = ("cat".end – "cat".start) + ("apple".end – "apple.start") = 2,所以slop=1即可。
return (min().doc() == max.doc())
&& ((max.end() - min().start() - totalLength) <= slop);
}
5.3、SpanNotQuery
SpanNotQuery包含如下两个成员变量:
SpanQuery include; 必须满足的SpanQuery
SpanQuery exclude; 必须不能满足的SpanQuery
其next函数从include中取出文档号,如果exclude也包括此文档号,则过滤掉。
其getSpans函数如下:
public Spans getSpans(final IndexReader reader) throws IOException {
return new Spans() {
private Spans includeSpans = include.getSpans(reader);
private boolean moreInclude = true;
private Spans excludeSpans = exclude.getSpans(reader);
private boolean moreExclude = excludeSpans.next();
@Override
public boolean next() throws IOException {
//得到下一个include的文档号
if (moreInclude)
moreInclude = includeSpans.next();
//此循环查看此文档号是否被exclude,如果是则取下一个include的文档号。
while (moreInclude && moreExclude) {
//将exclude跳到include文档号
if (includeSpans.doc() > excludeSpans.doc())
moreExclude = excludeSpans.skipTo(includeSpans.doc());
//当include和exclude文档号相同的时候,不断取得下一个exclude,如果exclude的end大于include的start,则说明当前文档号应该被exclude。
while (moreExclude
&& includeSpans.doc() == excludeSpans.doc()
&& excludeSpans.end() <= includeSpans.start()) {
moreExclude = excludeSpans.next();
}
//如果是因为没有exclude了,或者文档号不相同,或者include的end小于exclude的start,则当前文档不应该被exclude。
if (!moreExclude
|| includeSpans.doc() != excludeSpans.doc()
|| includeSpans.end() <= excludeSpans.start())
break;
//否则此文档应该被exclude,include取下一篇文档号。
moreInclude = includeSpans.next();
}
return moreInclude;
}
@Override
public int doc() { return includeSpans.doc(); }
@Override
public int start() { return includeSpans.start(); }
@Override
public int end() { return includeSpans.end(); }
};
}
5.4、SpanOrQuery
SpanOrQuery包含一个列表的子SpanQuery,并对它们取OR的关系,用于满足"apple和boy临近或者cat和dog临近的文档"此类的查询。
其OR的合并算法同BooleanQuery的OR关系的算法DisjunctionSumScorer类似。
public boolean next() throws IOException {
if (queue == null) {
return initSpanQueue(-1);
}
if (queue.size() == 0) {
return false;
}
//在优先级队列顶部取下一篇文档或者下一位置,并重新排列队列
if (top().next()) {
queue.updateTop();
return true;
}
//如果最顶部的SpanQuery没有下一篇文档或者下一位置,则弹出
queue.pop();
return queue.size() != 0;
}
5.5、FieldMaskingSpanQuery
在SpanNearQuery中,需要进行位置比较,相互比较位置的Term必须要在同一个域中,否则报异常IllegalArgumentException("Clauses must have same field.").
然而有时候我们需要对不同的域中的位置进行比较,例如:
文档一:
teacherid: 1
studentfirstname: james
studentsurname: jones
我们建索引如下:
Document doc = new Document();
doc.add(new Field("teacherid", "1", Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("studentfirstname", "james", Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("studentsurname", "jones", Field.Store.YES, Field.Index.NOT_ANALYZED));
writer.addDocument(doc);
文档二:
teacherid: 2
studenfirstname: james
studentsurname: smith
studentfirstname: sally
studentsurname: jones
我们建索引如下:
doc = new Document();
doc.add(new Field("teacherid", "2", Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("studentfirstname", "james", Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("studentsurname", "smith", Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("studentfirstname", "sally", Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("studentsurname", "jones", Field.Store.YES, Field.Index.NOT_ANALYZED));
writer.addDocument(doc);
现在我们想找firstname是james,surname是jones的学生的老师,显然如果搜索"studenfirstname: james AND studentsurname: jones",显然上面两个老师都能够搜索出来,可以辨别james和jones属于同一学生的一种方法是位置信息,也即当james和jones处于两个域的同一位置的时候,其属于同一个学生。
这时我们如果声明两个SpanTermQuery:
SpanQuery q1 = new SpanTermQuery(new Term("studentfirstname", "james"));
SpanQuery q2 = new SpanTermQuery(new Term("studentsurname", "jones"));
然后构建SpanNearQuery,子SpanQuery为上述q1, q2,因为在同一位置inorder=false,slop设为-1,因为
"jones".end – "james".start – totallength = 1 – 0 – 2 = -1,这样就能够搜的出来。
然而在构建SpanNearQuery的时候,其构造函数如下:
public SpanNearQuery(SpanQuery[] clauses, int slop, boolean inOrder, boolean collectPayloads) {
this.clauses = new ArrayList<SpanQuery>(clauses.length);
for (int i = 0; i < clauses.length; i++) {
SpanQuery clause = clauses[i];
if (i == 0) {
field = clause.getField();
} else if (!clause.getField().equals(field)) { //要求所有的子SpanQuery都属于同一个域
throw new IllegalArgumentException("Clauses must have same field.");
}
this.clauses.add(clause);
}
this.collectPayloads = collectPayloads;
this.slop = slop;
this.inOrder = inOrder;
}
所以我们引入FieldMaskingSpanQuery,SpanQuery q2m = new FieldMaskingSpanQuery(q2, "studentfirstname");
FieldMaskingSpanQuery.getField()得到的是你指定的假的域信息"studentfirstname",从而通过了审核,就可以计算位置信息了。
我们的查询过程如下:
File indexDir = new File("TestFieldMaskingSpanQuery/index");
IndexReader reader = IndexReader.open(FSDirectory.open(indexDir));
IndexSearcher searcher = new IndexSearcher(reader);
SpanQuery q1 = new SpanTermQuery(new Term("studentfirstname", "james"));
SpanQuery q2 = new SpanTermQuery(new Term("studentsurname", "jones"));
SpanQuery q2m = new FieldMaskingSpanQuery(q2, "studentfirstname");
Query query = new SpanNearQuery(new SpanQuery[]{q1, q2m}, -1, false);
TopDocs docs = searcher.search(query, 50);
for (ScoreDoc doc : docs.scoreDocs) {
System.out.println("docid : " + doc.doc + " score : " + doc.score);
}
5.6、PayloadTermQuery及PayloadNearQuery
带Payload前缀的查询对象不会因为payload的存在而使得结果集发生改变,而仅仅改变其评分。
欲使用Payload系列的查询语句:
首先在索引阶段,要将payload存入到索引中去:PayloadAttribute..setPayload(new Payload(byte[] b));
其次是实现自己的Similarity,并实现其接口float scorePayload(int docId, String fieldName, int start, int end, byte [] payload, int offset, int length),可以指定如何根据读出的二进制payload计算payload的打分。
最后在构建PayloadTermQuery及PayloadNearQuery的时候传入PayloadFunction function
PayloadFunction需要实现两个接口:
float currentScore(int docId, String field, int start, int end, int numPayloadsSeen, float currentScore, float currentPayloadScore)是在上一步用Similarity根据二进制payload计算出payload打分后,此打分作为currentPayloadScore传入,此次计算前的原分数作为currentScore传入,此处可以指定payload如何影响原来的打分。
float docScore(int docId, String field, int numPayloadsSeen, float payloadScore)当所有的payload都被计算完毕后,如何调整最终的打分。
PayloadFunction有三种实现:
AveragePayloadFunction,其在currentScore函数中,总是将payload的打分加到原分数中,currentPayloadScore + currentScore,然后在所有的payload都计算完毕后,在docScore函数中,对这些打分取平均值,return numPayloadsSeen > 0 ? (payloadScore / numPayloadsSeen) : 1
MaxPayloadFunction,其在currentScore函数中,总是取两者的最大值Math.max(currentPayloadScore, currentScore),最后在docScore函数中将最大值返回,return numPayloadsSeen > 0 ? payloadScore : 1
MinPayloadFunction,其在currentScore函数中,总是取两者的最小值Math.min(currentPayloadScore, currentScore),最后在docScore函数中将最小值返回,return numPayloadsSeen > 0 ? payloadScore : 1
对于PayloadTermQuery来讲,在其生成的PayloadTermSpanScorer中:
首先计算出payloadScore
payloadScore = function.currentScore(doc, term.field(), spans.start(), spans.end(), payloadsSeen, payloadScore, similarity.scorePayload(doc, term.field(), spans.start(), spans.end(), payload, 0, positions.getPayloadLength()));
然后在score函数中调用getSpanScore() * getPayloadScore()
protected float getPayloadScore() {
return function.docScore(doc, term.field(), payloadsSeen, payloadScore);
}
对于PayloadNearQuery来讲,在其生成的PayloadNearSpanScorer中:
首先计算出payloadScore
payloadScore = function.currentScore(doc, fieldName, start, end, payloadsSeen, payloadScore, similarity.scorePayload(doc, fieldName, spans.start(), spans.end(), thePayload, 0, thePayload.length) );
然后在score函数中
public float score() throws IOException {
return super.score() * function.docScore(doc, fieldName, payloadsSeen, payloadScore);
}
6、FilteredQuery
FilteredQuery包含两个成员变量:
Query query:查询对象
Filter filter:其有一个函数DocIdSet getDocIdSet(IndexReader reader) 得到一个文档号集合,结果文档必须出自此文档集合,注此处的过滤器所包含的文档号并不是要过滤掉的文档号,而是过滤后需要的文档号。
FilterQuery所得到的结果集同两者取AND查询相同,只不过打分的时候,FilterQuery只考虑query的部分,不考虑filter的部分。
Filter包含很多种如下:
6.1、TermsFilter
其包含一个成员变量Set<Term> terms=new TreeSet<Term>(),所有包含terms集合中任一term的文档全部属于文档号集合。
其getDocIdSet函数如下:
public DocIdSet getDocIdSet(IndexReader reader) throws IOException
{
//生成一个bitset,大小为索引中文档总数
OpenBitSet result=new OpenBitSet(reader.maxDoc());
TermDocs td = reader.termDocs();
try
{
//遍历每个term的文档列表,将文档号都在bitset中置一,从而bitset包含了所有的文档号。
for (Iterator<Term> iter = terms.iterator(); iter.hasNext();)
{
Term term = iter.next();
td.seek(term);
while (td.next())
{
result.set(td.doc());
}
}
}
finally
{
td.close();
}
return result;
}
6.2、BooleanFilter
其像BooleanQuery相似,包含should的filter,must的filter,not的filter,在getDocIdSet的时候,先将所有满足should的文档号集合之间取OR的关系,然后同not的文档号集合取NOT的关系,最后同must的文档号集合取AND的关系,得到最后的文档集合。
其getDocIdSet函数如下:
public DocIdSet getDocIdSet(IndexReader reader) throws IOException
{
OpenBitSetDISI res = null;
if (shouldFilters != null) {
for (int i = 0; i < shouldFilters.size(); i++) {
if (res == null) {
res = new OpenBitSetDISI(getDISI(shouldFilters, i, reader), reader.maxDoc());
} else {
//将should的filter的文档号全部取OR至bitset中
DocIdSet dis = shouldFilters.get(i).getDocIdSet(reader);
if(dis instanceof OpenBitSet) {
res.or((OpenBitSet) dis);
} else {
res.inPlaceOr(getDISI(shouldFilters, i, reader));
}
}
}
}
if (notFilters!=null) {
for (int i = 0; i < notFilters.size(); i++) {
if (res == null) {
res = new OpenBitSetDISI(getDISI(notFilters, i, reader), reader.maxDoc());
res.flip(0, reader.maxDoc());
} else {
//将not的filter的文档号全部取NOT至bitset中
DocIdSet dis = notFilters.get(i).getDocIdSet(reader);
if(dis instanceof OpenBitSet) {
res.andNot((OpenBitSet) dis);
} else {
res.inPlaceNot(getDISI(notFilters, i, reader));
}
}
}
}
if (mustFilters!=null) {
for (int i = 0; i < mustFilters.size(); i++) {
if (res == null) {
res = new OpenBitSetDISI(getDISI(mustFilters, i, reader), reader.maxDoc());
} else {
//将must的filter的文档号全部取AND至bitset中
DocIdSet dis = mustFilters.get(i).getDocIdSet(reader);
if(dis instanceof OpenBitSet) {
res.and((OpenBitSet) dis);
} else {
res.inPlaceAnd(getDISI(mustFilters, i, reader));
}
}
}
}
if (res !=null)
return finalResult(res, reader.maxDoc());
return DocIdSet.EMPTY_DOCIDSET;
}
6.3、DuplicateFilter
DuplicateFilter实现了如下的功能:
比如说我们有这样一批文档,每篇文档都分成多页,每篇文档都有一个id,然而每一页是按照单独的Document进行索引的,于是进行搜索的时候,当一篇文档的两页都包含关键词的时候,此文档id在结果集中出现两次,这是我们不想看到的,DuplicateFilter就是指定一个域如id,在此域相同的文档仅取其中一篇。
DuplicateFilter包含以下成员变量:
String fieldName:域的名称
int keepMode:KM_USE_FIRST_OCCURRENCE表示重复的文档取第一篇,KM_USE_LAST_OCCURRENCE表示重复的文档取最后一篇。
int processingMode:
PM_FULL_VALIDATION是首先将bitset中所有文档都设为false,当出现同组重复文章的第一篇的时候,将其设为1
PM_FAST_INVALIDATION是首先将bitset中所有文档都设为true,除了同组重复文章的第一篇,其他的的全部设为0
两者在所有的文档都包含指定域的情况下,功能一样,只不过后者不用处理docFreq=1的文档,速度加快。
然而当有的文档不包含指定域的时候,后者由于都设为true,则没有机会将其清零,因而会被允许返回,当然工程中应避免这种情况。
其getDocIdSet函数如下:
public DocIdSet getDocIdSet(IndexReader reader) throws IOException
{
if(processingMode==PM_FAST_INVALIDATION)
{
return fastBits(reader);
}
else
{
return correctBits(reader);
}
}
private OpenBitSet correctBits(IndexReader reader) throws IOException
{
OpenBitSet bits=new OpenBitSet(reader.maxDoc());
Term startTerm=new Term(fieldName);
TermEnum te = reader.terms(startTerm);
if(te!=null)
{
Term currTerm=te.term();
//如果属于指定的域
while((currTerm!=null)&&(currTerm.field()==startTerm.field()))
{
int lastDoc=-1;
//则取出包含此term的所有的文档
TermDocs td = reader.termDocs(currTerm);
if(td.next())
{
if(keepMode==KM_USE_FIRST_OCCURRENCE)
{
//第一篇设为true
bits.set(td.doc());
}
else
{
do
{
lastDoc=td.doc();
}while(td.next());
bits.set(lastDoc); //最后一篇设为true
}
}
if(!te.next())
{
break;
}
currTerm=te.term();
}
}
return bits;
}
private OpenBitSet fastBits(IndexReader reader) throws IOException
{
OpenBitSet bits=new OpenBitSet(reader.maxDoc());
bits.set(0,reader.maxDoc()); //全部设为true
Term startTerm=new Term(fieldName);
TermEnum te = reader.terms(startTerm);
if(te!=null)
{
Term currTerm=te.term();
//如果属于指定的域
while((currTerm!=null)&&(currTerm.field()==startTerm.field()))
{
if(te.docFreq()>1)
{
int lastDoc=-1;
//取出所有的文档
TermDocs td = reader.termDocs(currTerm);
td.next();
if(keepMode==KM_USE_FIRST_OCCURRENCE)
{
//除了第一篇不清零
td.next();
}
do
{
lastDoc=td.doc();
bits.clear(lastDoc); //其他全部清零
}while(td.next());
if(keepMode==KM_USE_LAST_OCCURRENCE)
{
bits.set(lastDoc); //最后一篇设为true
}
}
if(!te.next())
{
break;
}
currTerm=te.term();
}
}
return bits;
}
举例,我们索引如下的文件:
File indexDir = new File("TestDuplicateFilter/index");IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);Document doc = new Document();doc.add(new Field("id", "1", Field.Store.YES, Field.Index.NOT_ANALYZED));doc.add(new Field("contents", "page 1: hello world", Field.Store.YES, Field.Index.ANALYZED));writer.addDocument(doc);
doc = new Document();doc.add(new Field("id", "1", Field.Store.YES, Field.Index.NOT_ANALYZED));doc.add(new Field("contents", "page 2: hello world", Field.Store.YES, Field.Index.ANALYZED));writer.addDocument(doc);
doc = new Document();doc.add(new Field("id", "1", Field.Store.YES, Field.Index.NOT_ANALYZED));doc.add(new Field("contents", "page 3: hello world", Field.Store.YES, Field.Index.ANALYZED));writer.addDocument(doc);
doc = new Document();doc.add(new Field("id", "2", Field.Store.YES, Field.Index.NOT_ANALYZED));doc.add(new Field("contents", "page 1: hello world", Field.Store.YES, Field.Index.ANALYZED));writer.addDocument(doc);
doc = new Document();doc.add(new Field("id", "2", Field.Store.YES, Field.Index.NOT_ANALYZED));doc.add(new Field("contents", "page 2: hello world", Field.Store.YES, Field.Index.ANALYZED));writer.addDocument(doc);writer.close();
如果搜索TermQuery tq = new TermQuery(new Term("contents","hello")),则结果为:
id : 1id : 1id : 1id : 2id : 2
如果按如下进行搜索:
File indexDir = new File("TestDuplicateFilter/index");IndexReader reader = IndexReader.open(FSDirectory.open(indexDir));IndexSearcher searcher = new IndexSearcher(reader);TermQuery tq = new TermQuery(new Term("contents","hello"));DuplicateFilter filter = new DuplicateFilter("id");FilteredQuery query = new FilteredQuery(tq, filter);TopDocs docs = searcher.search(query, 50);for (ScoreDoc doc : docs.scoreDocs) { Document ldoc = reader.document(doc.doc); String id = ldoc.get("id"); System.out.println("id : " + id);}
则结果为:
id : 1id : 2
6.4、FieldCacheRangeFilter<T>及FieldCacheTermsFilter
在介绍与FieldCache相关的Filter之前,先介绍FieldCache。
FieldCache缓存的是不是存储域的内容,而是索引域中term的内容,索引中的term是String的类型,然而可以将其他的类型作为String类型索引进去,例如"1","2.3"等,然后搜索的时候将这些信息取出来。
FieldCache支持如下类型:
byte[] getBytes (IndexReader reader, String field, ByteParser parser)
double[] getDoubles(IndexReader reader, String field, DoubleParser parser)
float[] getFloats (IndexReader reader, String field, FloatParser parser)
int[] getInts (IndexReader reader, String field, IntParser parser)
long[] getLongs(IndexReader reader, String field, LongParser parser)
short[] getShorts (IndexReader reader, String field, ShortParser parser)
String[] getStrings (IndexReader reader, String field)
StringIndex getStringIndex (IndexReader reader, String field)
其中StringIndex包含两个成员:
String[] lookup:按照字典顺序排列的所有term。
int[] order:其中位置表示文档号,order[i]第i篇文档包含的term在lookup中的位置。
FieldCache默认的实现FieldCacheImpl,其中包含成员变量Map<Class<?>,Cache> caches保存从类型到Cache的映射。
private synchronized void init() {
caches = new HashMap<Class<?>,Cache>(7);
caches.put(Byte.TYPE, new ByteCache(this));
caches.put(Short.TYPE, new ShortCache(this));
caches.put(Integer.TYPE, new IntCache(this));
caches.put(Float.TYPE, new FloatCache(this));
caches.put(Long.TYPE, new LongCache(this));
caches.put(Double.TYPE, new DoubleCache(this));
caches.put(String.class, new StringCache(this));
caches.put(StringIndex.class, new StringIndexCache(this));
}
其实现接口getInts 如下,即先得到Integer类型所对应的IntCache然后,再从其中根据reader和由field和parser组成的Entry得到整型值。
public int[] getInts(IndexReader reader, String field, IntParser parser) throws IOException {
return (int[]) caches.get(Integer.TYPE).get(reader, new Entry(field, parser));
}
各类缓存的父类Cache包含成员变量Map<Object, Map<Entry, Object>> readerCache,其中key是IndexReader,value是一个Map,此Map的key是Entry,也即是field,value是缓存的int[]的值。(也即在这个reader的这个field中有一个数组的int,每一项代表一篇文档)。
Cache的get函数如下:
public Object get(IndexReader reader, Entry key) throws IOException {
Map<Entry,Object> innerCache;
Object value;
final Object readerKey = reader.getFieldCacheKey(); //此函数返回this,也即IndexReader本身
synchronized (readerCache) {
innerCache = readerCache.get(readerKey); //通过IndexReader得到Map
if (innerCache == null) { //如果没有则新建一个Map
innerCache = new HashMap<Entry,Object>();
readerCache.put(readerKey, innerCache);
value = null;
} else {
value = innerCache.get(key); //此Map的key是Entry,value即是缓存的值
}
//如果缓存不命中,则创建此值
if (value == null) {
value = new CreationPlaceholder();
innerCache.put(key, value);
}
}
if (value instanceof CreationPlaceholder) {
synchronized (value) {
CreationPlaceholder progress = (CreationPlaceholder) value;
if (progress.value == null) {
progress.value = createValue(reader, key); //调用此函数创建缓存值
synchronized (readerCache) {
innerCache.put(key, progress.value);
}
}
}
return progress.value;
}
return value;
}
Cache的createValue函数根据类型的不同而不同,我们仅分析IntCache和StringIndexCache的实现.
IntCache的createValue函数如下:
protected Object createValue(IndexReader reader, Entry entryKey) throws IOException {
Entry entry = entryKey;
String field = entry.field;
IntParser parser = (IntParser) entry.custom;
int[] retArray = null;
TermDocs termDocs = reader.termDocs();
TermEnum termEnum = reader.terms (new Term (field));
try {
//依次将域中所有的term都取出来,用IntParser进行解析,缓存retArray[]位置即文档号,retArray[i]即第i篇文档所包含的int值.
do {
Term term = termEnum.term();
if (term==null || term.field() != field) break;
int termval = parser.parseInt(term.text());
if (retArray == null)
retArray = new int[reader.maxDoc()];
termDocs.seek (termEnum);
while (termDocs.next()) {
retArray[termDocs.doc()] = termval;
}
} while (termEnum.next());
} catch (StopFillCacheException stop) {
} finally {
termDocs.close();
termEnum.close();
}
if (retArray == null)
retArray = new int[reader.maxDoc()];
return retArray;
}
};
StringIndexCache的createValue函数如下:
protected Object createValue(IndexReader reader, Entry entryKey) throws IOException {
String field = StringHelper.intern(entryKey.field);
final int[] retArray = new int[reader.maxDoc()];
String[] mterms = new String[reader.maxDoc()+1];
TermDocs termDocs = reader.termDocs();
TermEnum termEnum = reader.terms (new Term (field));
int t = 0;
mterms[t++] = null;
try {
do {
Term term = termEnum.term();
if (term==null || term.field() != field) break;
mterms[t] = term.text(); //mterms[i]保存的是按照字典顺序第i个term所对应的字符串。
termDocs.seek (termEnum);
while (termDocs.next()) {
retArray[termDocs.doc()] = t; //retArray[i]保存的是第i篇文档所包含的字符串在mterms中的位置。
}
t++;
} while (termEnum.next());
} finally {
termDocs.close();
termEnum.close();
}
if (t == 0) {
mterms = new String[1];
} else if (t < mterms.length) {
String[] terms = new String[t];
System.arraycopy (mterms, 0, terms, 0, t);
mterms = terms;
}
StringIndex value = new StringIndex (retArray, mterms);
return value;
}
FieldCacheRangeFilter的可以是各种类型的Range,其中Int类型用下面的函数生成:
public static FieldCacheRangeFilter<Integer> newIntRange(String field, FieldCache.IntParser parser, Integer lowerVal, Integer upperVal, boolean includeLower, boolean includeUpper) {
return new FieldCacheRangeFilter<Integer>(field, parser, lowerVal, upperVal, includeLower, includeUpper) {
@Override
public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
final int inclusiveLowerPoint, inclusiveUpperPoint;
//计算左边界
if (lowerVal != null) {
int i = lowerVal.intValue();
if (!includeLower && i == Integer.MAX_VALUE)
return DocIdSet.EMPTY_DOCIDSET;
inclusiveLowerPoint = includeLower ? i : (i + 1);
} else {
inclusiveLowerPoint = Integer.MIN_VALUE;
}
//计算右边界
if (upperVal != null) {
int i = upperVal.intValue();
if (!includeUpper && i == Integer.MIN_VALUE)
return DocIdSet.EMPTY_DOCIDSET;
inclusiveUpperPoint = includeUpper ? i : (i - 1);
} else {
inclusiveUpperPoint = Integer.MAX_VALUE;
}
if (inclusiveLowerPoint > inclusiveUpperPoint)
return DocIdSet.EMPTY_DOCIDSET;
//从cache中取出values,values[i]表示第i篇文档在此域中的值
final int[] values = FieldCache.DEFAULT.getInts(reader, field, (FieldCache.IntParser) parser);
return new FieldCacheDocIdSet(reader, (inclusiveLowerPoint <= 0 && inclusiveUpperPoint >= 0)) {
@Override
boolean matchDoc(int doc) {
//仅在文档i所对应的值在区间内的时候才返回。
return values[doc] >= inclusiveLowerPoint && values[doc] <= inclusiveUpperPoint;
}
};
}
};
}
FieldCacheRangeFilter同NumericRangeFilter或者TermRangeFilter功能类似,只不过后两者取得docid的bitset都是从索引中取出,而前者是缓存了的,加快了速度。
同样FieldCacheTermsFilter同TermFilter功能类似,也是前者进行了缓存,加快了速度。
6.5、MultiTermQueryWrapperFilter<Q>
MultiTermQueryWrapperFilter包含成员变量Q query,其getDocIdSet得到满足此query的文档号bitset。
public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
final TermEnum enumerator = query.getEnum(reader);
try {
if (enumerator.term() == null)
return DocIdSet.EMPTY_DOCIDSET;
final OpenBitSet bitSet = new OpenBitSet(reader.maxDoc());
final int[] docs = new int[32];
final int[] freqs = new int[32];
TermDocs termDocs = reader.termDocs();
try {
int termCount = 0;
//遍历满足query的所有term
do {
Term term = enumerator.term();
if (term == null)
break;
termCount++;
termDocs.seek(term);
while (true) {
//得到每个term的文档号列表,放入bitset
final int count = termDocs.read(docs, freqs);
if (count != 0) {
for(int i=0;i<count;i++) {
bitSet.set(docs[i]);
}
} else {
break;
}
}
} while (enumerator.next());
query.incTotalNumberOfTerms(termCount);
} finally {
termDocs.close();
}
return bitSet;
} finally {
enumerator.close();
}
}
MultiTermQueryWrapperFilter有三个重要的子类:
NumericRangeFilter<T>:以NumericRangeQuery作为query
PrefixFilter:以PrefixQuery作为query
TermRangeFilter:以TermRangeQuery作为query
6.6、QueryWrapperFilter
其包含一个查询对象,getDocIdSet会获得所有满足此查询的文档号:
public DocIdSet getDocIdSet(final IndexReader reader) throws IOException {
final Weight weight = query.weight(new IndexSearcher(reader));
return new DocIdSet() {
public DocIdSetIterator iterator() throws IOException {
return weight.scorer(reader, true, false); //Scorer的next即返回一个个文档号。
}
};
}
6.7、SpanFilter
6.7.1、SpanQueryFilter
其包含一个SpanQuery query,作为过滤器,其除了通过getDocIdSet得到文档号之外,bitSpans函数得到的SpanFilterResult还包含位置信息,可以用于在FilterQuery中起过滤作用。
public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
SpanFilterResult result = bitSpans(reader);
return result.getDocIdSet();
}
public SpanFilterResult bitSpans(IndexReader reader) throws IOException {
final OpenBitSet bits = new OpenBitSet(reader.maxDoc());
Spans spans = query.getSpans(reader);
List<SpanFilterResult.PositionInfo> tmp = new ArrayList<SpanFilterResult.PositionInfo>(20);
int currentDoc = -1;
SpanFilterResult.PositionInfo currentInfo = null;
while (spans.next())
{
//将docid放入bitset
int doc = spans.doc();
bits.set(doc);
if (currentDoc != doc)
{
currentInfo = new SpanFilterResult.PositionInfo(doc);
tmp.add(currentInfo);
currentDoc = doc;
}
//将start和end信息放入PositionInfo
currentInfo.addPosition(spans.start(), spans.end());
}
return new SpanFilterResult(bits, tmp);
}
6.7.2、CachingSpanFilter
由Filter的接口DocIdSet getDocIdSet(IndexReader reader)得知,一个docid的bitset是同一个reader相对应的。
有前面对docid的描述可知,其仅对一个打开的reader有意义。
CachingSpanFilter有一个成员变量Map<IndexReader,SpanFilterResult> cache保存从reader到SpanFilterResult的映射,另一个成员变量SpanFilter filter用于缓存不命中的时候得到SpanFilterResult。
其getDocIdSet如下:
public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
SpanFilterResult result = getCachedResult(reader);
return result != null ? result.getDocIdSet() : null;
}
private SpanFilterResult getCachedResult(IndexReader reader) throws IOException {
lock.lock();
try {
if (cache == null) {
cache = new WeakHashMap<IndexReader,SpanFilterResult>();
}
//如果缓存命中,则返回缓存中的结果。
final SpanFilterResult cached = cache.get(reader);
if (cached != null) return cached;
} finally {
lock.unlock();
}
//如果缓存不命中,则用SpanFilter直接从reader中得到结果。
final SpanFilterResult result = filter.bitSpans(reader);
lock.lock();
try {
//将新得到的结果放入缓存
cache.put(reader, result);
} finally {
lock.unlock();
}
return result;
}