主要的类
IndexSearcher:搜索的主类。
Query(及具体子类):被传入IndexSearcher的search方法,用于逻辑上的搜索。
QueryParser:将人工输入的查询字符串转化成Query对象。
TopDocs:存储着得分最高的那些文档,由IndexSearcher的search方法返回。
ScoreDoc:TopDocs中的每一个文档,他们只保留着Document的引用。
3.1 实现简单的索引功能
可以通过符合Lucene的字符串或者Query的组合实现复杂的查询,即QueryParser接受Query对象组合或者字符串两形式。
Term
在某一个具体字段(Field)上搜索。
一个简单的搜索例子:
public class BasicSearchingTest extends TestCase {
public void testTerm() throws Exception {
IndexSearcher searcher;
Directory dir = TestUtil.getBookIndexDirectory(); //A
searcher = new IndexSearcher(dir, //B
true); //B
Term t = new Term("subject", "ant");
Query query = new TermQuery(t);
TopDocs docs = searcher.search(query, 10);
assertEquals("JDwA", 1, docs.totalHits); //C
t = new Term("subject", "junit");
docs = searcher.search(new TermQuery(t), 10);
assertEquals(2, docs.totalHits); //D
searcher.close();
}
}
构造Term是较为关键的步骤。
QueryParser
可以使用QueryParser将String类型的查询串转化成Query对象,支持OR或者+ -这种。
构造函数是
QueryParser(Version matchVersion, String field, Analyzer analyzer)
matchVersion就Version.LUCENE_CURRENT吧。
field是默认的搜索字段。
analyser:只有在QueryParser中才使用analyzer,将对查询字符串进行处理。
QueryParser的parser将解析并生成Query对象。
public Query parse(String query) throws ParseException
解析失败将抛出异常,否则返回Query对象。
如果query包含多个词,默认使用OR连接各词。
常用的String组合:
java :只搜索Java
java junit 或者 java OR junit :搜索包含java或者junit的,在默认字段
+java +junit 或者java AND junit:搜索包含java并且junit的,在默认字段
title:ant :搜索title字段包含ant的
title:extreme –subject:sports 或者 title:extreme AND NOT subject:sports:搜索title字段包含extreme并且subject不好喊sports的。
title:"junit in action":搜索title中精确包含"junit in action"的。
java*:搜索java开头的,例如javascript java.net等
java~:搜索java相近的例如lava
lastmodified: [1/1/04 TO 12/31/04] :搜索lastmodified字段在两个日期之间的。
总之是很强大的,上述String的query均可以用Query对象组合而形成。
3.2 使用IndexSearcher
使用IndexSearch需要三个步骤:
Directory dir = FSDirectory.open(new File("/path/xxx"));
IndexReader reader = IndexReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
IndexReader封装了底层的API操作,reader的open操作非常耗费资源,因此reader应该重用。
但是reader打开后便不能获悉之后更新的Index,因此可reopen:
reopen将尝试尽量重用,如果无法重用将创建新的IndexReader,因此需要判断。
IndexReader newReader = reader.reopen();
if (reader != newReader) {
reader.close();
reader = newReader;
searcher = new IndexSearcher(reader);
}
执行搜索
IndexSearcher提供了很多API,下述几个均可以。
TopDocs search(Query query, int n)
TopDocs search(Query query, Filter filter, int n)
TopFieldDocs search(Query query, Filter filter, int n, Sort sort)
TopDocs
多数search直接返回一个TopDocs作为搜索的结果(已经按照相似度排序),它包含三个属性(方法):
totalHits:有多少个Document被匹配
scoreDocs:每一个具体的搜索结果(含分、Document等)
结果的分页
在Lucene中,常用的解决方法有:
1、在第一次就把很多结果都抓取过来,然后根据用户的分页请求来显示
2、每次重新查询
一般来说,Web是“无状态协议”,重新查询可回避状态的存储,是一种较好的选择。每次用户选择后面的页后,将“n”的数值加大,即可显示后面的内容。
“实时搜索”
实时搜索的关键是:不要自己创建Directory->IndexReader,而是使用下述办法:
IndexWriter.getReader():这可以不需要重新commit 索引就立即获得更新。
IndexReader newReader = reader.reopen():重用reader,比起open非常快捷,但是注意如果reader!=oldReader,则需要关闭oldReader。
3.3 理解得分"Score"
Lucene使用得分Score来衡量Document与Query的匹配程度。
得分公式
关于分数的推导,有详细的说明,请参考《Lucene打分公式的数学推导》
http://topic.csdn.net/u/20100308/21/3386acef-d853-4738-9941-2a8b0ee157ca.html
其中各个因子的作用为:
tf(t in d):Term t在文档d中出现的词频
idf(t):Term t在几篇文档中出现过
norm(t, d):标准化因子,它包括三个参数:
Document boost:此值越大,说明此文档越重要。
Field boost:此域越大,说明此域越重要。
lengthNorm(field) = (1.0 / Math.sqrt(numTerms)):一个域中包含的Term总数越多,也即文档越长,此值越小,文档越短,此值越大。
boost(t.field in d):额外的提升
coord(q, d):主要用于AND查询时,符合多个的Term比其他的有更高的得分
queryNorm(q):计算每个查询条目的方差和,此值并不影响排序,而仅仅使得不同的query之间的分数可以比较。
通过Boost可以提升某文档的位置,相似性可以通过拓展Similarity来实现。
使用explain来理解得分
尽管公式非常复杂,但是可以使用内置的expalin()函数来理解得分。
Explanation explanation = searcher.explain(Quert, Document);
explanation可以获取详细的每一步的评分。
3.4 Lucene提供的多种Query
TermQuery
某个字段的检索
IndexSearcher searcher = new IndexSearcher(TestUtil.getBookIndexDirectory());
Term t = new Term("isbn", "1930110995");
Query query = new TermQuery(t);
TopDocs docs = searcher.search(query, 10);
assertEquals("JUnit in Action", 1, docs.totalHits);
searcher.close();
TermRangeQuery
因为是按照字典序排列的,所以Lucene中很容易通过"Range"即范围来检索。
Directory dir = TestUtil.getBookIndexDirectory();
IndexSearcher searcher = new IndexSearcher(dir);
TermRangeQuery query = new TermRangeQuery("title2", "d", "j", true, true);
TopDocs matches = searcher.search(query, 100);
assertEquals(3, matches.totalHits);
searcher.close();
dir.close();
两个true、true分别代表了是否包含d j两点。
也可以对不连续的进行选择,使用Collator,但性能很差。
NumericRangeQuery
与RangeQuery类似,只不过是对数值进行范围检索
Directory dir = TestUtil.getBookIndexDirectory();
IndexSearcher searcher = new IndexSearcher(dir);
// pub date of TTC was October 1988
NumericRangeQuery query = NumericRangeQuery.newIntRange("pubmonth",
198805,
198810,
true,
true);
TopDocs matches = searcher.search(query, 100);
assertEquals(1, matches.totalHits);
searcher.close();
dir.close();
PrefixQuery
前缀搜索,只检索前缀为xxx字符串的匹配结果。
IndexSearcher searcher = new IndexSearcher(TestUtil.getBookIndexDirectory());
// search for programming books, including subcategories
Term term = new Term("category", //#A
"/technology/computers/programming"); //#A
PrefixQuery query = new PrefixQuery(term); //#A
TopDocs matches = searcher.search(query, 10); //#A
int programmingAndBelow = matches.totalHits;
// only programming books, not subcategories
matches = searcher.search(new TermQuery(term), 10); //#B
int justProgramming = matches.totalHits;
assertTrue(programmingAndBelow > justProgramming);
searcher.close();
BooleanQuery
与、或、非的将其他Query组合起来。
public void add(Query query, BooleanClause.Occur occur)
通过occour设置AND、OR或NOT
AND:occour设置为Occur.MUST
OR:occour设置为Occur.SHOULD
NOT:occour设置为Occur.MUST_NOT
PhraseQuery
PhraseQuery支持多个关键字的搜索。
slop用于表示“距离”,设定PhraseQuery的slop可控制多关键词的检索。
例如对于Field:
doc.add(new Field("field", "the quick brown fox jumped over the lazy dog", Field.Store.YES, Field.Index.ANALYZED));
相连的两词,将总被检索出来,无论slop为多少:
PhraseQuery query = new PhraseQuery();
query.add(new Term("field", "quick"));
query.add(new Term("field", "brown"));
可以被检索出来
再例如,brown,quick与原Doc的距离为3(注意顺序也有影响),则当slop大于等于3的时候才能被检索出来。
再例如下述PhraseQuery的检索结果。
assertFalse("not close enough",
matched(new String[] {"quick", "jumped", "lazy"}, 3));
assertTrue("just enough",
matched(new String[] {"quick", "jumped", "lazy"}, 4));
assertFalse("almost but not quite",
matched(new String[] {"lazy", "jumped", "quick"}, 7));
assertTrue("bingo",
matched(new String[] {"lazy", "jumped", "quick"}, 8));
slop实际是移动距离:将一个Query经过移动多少步可以符合另一个。
WildcardQuery:通配符查询
Query query = new WildcardQuery(new Term("contents", "?ild*"));
WildcardQuery面临着较为严重的性能问题:当前缀(*?之前)较长时,需要遍历的term将减少,反之极端,在开头使用通配符将导致遍历所有term。
FuzzyQuery:模糊查询
使用了“编辑距离”:number of character deletions, insertions, or substitutions required to transform one string to the other string.
如下所示:
indexSingleFieldDocs(new Field[] { new Field("contents",
"fuzzy",
Field.Store.YES,
Field.Index.ANALYZED),
new Field("contents",
"wuzzy",
Field.Store.YES,
Field.Index.ANALYZED)
});
IndexSearcher searcher = new IndexSearcher(directory);
Query query = new FuzzyQuery(new Term("contents", "wuzza"));
TopDocs matches = searcher.search(query, 10);
assertEquals("both close enough", 2, matches.totalHits);
assertTrue("wuzzy closer than fuzzy",
matches.scoreDocs[0].score != matches.scoreDocs[1].score);
Document doc = searcher.doc(matches.scoreDocs[0].doc);
使用FuzzyQuery,则wuzzy可以匹配wuzzy,也可以匹配fuzzy。
FuzzyQuery不接受“距离”,而是接受0~1之间的一个“阈值”。
例如构造函数:
FuzzyQuery(Term term, float minimumSimilarity, int prefixLength)
当编辑距离小于minimumSimilarity*(Length(term)-prefixLength)的时候,则认为匹配FuzzyQuery。
FuzzyQuery将枚举索引中全部的Term,比较耗费资源!!
MatchAllDocsQuery
MatchAllDocsQuery将匹配索引中所有的Doc,Boost值默认都是1.0,并支持按照某field计算Boost数值。
3.5 QueryParser
尽管通过QueryAPI可以创建强大的查询,但是不需要完全从API创建起来Query,也可以通过
String -> QueryParser解析->Query的方法。
例如:
+pubdate:[20040101 TO 20041231] Java AND (Jakarta OR Apache)
在String的Query字符串中,下述字符需要转移,在字符前面加上‘\’:
\ + - ! ( ) : ^ ] { } ~ * ?
对于一个Query对象,
Query.toString()可以显示其String类型的Query表示。
例如:
query.add(new FuzzyQuery(new Term("field", "kountry")),
BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term("title", "western")),
BooleanClause.Occur.SHOULD);
的toString为:
+field:kountry~0.5 title:western
注意:FuzzyQuery的默认相似编辑距离为0.5。
TermQuery
QueryParser parser = new QueryParser(Version.LUCENE_CURRENT,
"subject", analyzer);
将被解析为:
term: subject:computers
即field:term的形式
TermRangeQuery
范围搜索的String
title2:[K TO N] //两边都包含
title2:{K TO Mindstorms} //两边都不包含
NumericQuery和DateQuery
QueryParser不提供将String解析成NumericQuery或者DateQuery,需要通过继承QueryParser,在子类中实现。(见6.3.3和6.3.4)
前缀查询和通配符查询
Query q = new QueryParser(Version.LUCENE_CURRENT,
"field", analyzer).parse("PrefixQuery*");
默认情况下,其String为
prefixquery*
即默认全部小写化。
可以通过这样控制不小写
qp.setLowercaseExpandedTerms(false);
布尔查询
AND OR NOT必须大写。
默认情况下,空格表示OR。
abc xyz => abc OR xyz
可以更改默认操作符:
parser.setDefaultOperator(QueryParser.AND_OPERATOR);
则之后的
abc xyz => abc AND xyz
也可以使用缩写形式即+-表示。
a AND b == +a +b
a OR b == a b
a AND NOT b == +a –b
注意NOT之前必须至少有一个非NOT的操作符,即不能单独使用NOT word来找不含word的所有Doc。
PhraseQuery
将String的Query放在双引号“”内可创建一个QueryParser,用于将上述各种的Query的组合进行解析。
注意一定要用引号“”包围!
例如下述
This is Some Phrase*
将被解析为TermQuery,并非WildQuery,
而下述才可以:
\"This is Some Phrase*\"
但在此例子中,This、is将作为stop words被过滤。
双引号外面的~N可以设置slop数值,例如:
\"sloppy phrase\"~5 表示slop的数值时5(用于PhraseQuery)
FuzzyQuery
在Term后置~表示模糊查询,即FuzzyQuery。
例如:
Query query = parser.parse("kountry~");
或者
query = parser.parse("kountry~0.7");
MatchAllDocsQuery
*:*表示MatchAllDocsQuery,即匹配所有Document。
Grouping
Query query = new QueryParser(
Version.LUCENE_CURRENT,
"subject",
analyzer).parse("(agile OR extreme) AND methodology");
字段选择
boost单个Term
^Float可以提升Term的Boost数值例如:
junit^2.0 testing
将junit的Boost提高一倍而testing不变