六、高级搜索技巧:
大纲:1. 对搜索结果的排序
2. 多字段搜索和多索引搜索
3. 对搜索结果的过滤
1. 对搜索的结果排序:Lucene 1.4以前的版本,搜索结果只能够以Lucene内部的评分标准,采用降序排列来返回搜索结果集,
通过这种方式实现将最相关的结果排在返回结果的较前面显示。如果想实现通过自己的方法排序也行,
不过就要使用一些比较麻烦的手段了,就是先得到返回的搜索结果,然后再对结果集进行排序等操作,以便实现自己的排序。
** 解决方案:但是Lucene 1.4版本之后提供了更加好的方法能够解决上面的问题。下边就来介绍各种各样的排序方法,
包括通过一个或多个字段进行排序,还有降序排列和升序排列等。
1.1 使用Sort类排序:之前我们使用IndexSearcher的时候只是使用了search(Query)这种声明的方法,
这种方法的返回结果是按照与搜索关键字的相关度(即得分)降序排列的。
但是IndexSearcher中有很多重载方法其中就有一个search(Query, Sort)这种声明的方法(是IndexSearcher的基类Searcher中的方法)。
该方法的返回结果是按照传入的Sort对象具有的规则进行排序的。
例子:和以前写得IndexSearcher搜索的例子一样,不一样的是调用search()方法的时候,如下
//创建排序对象:这里调用缺省构造方法,即按照Sort的缺省规则进行排序
Sort sort = new Sort();
Hits hits = searcher.search(query, sort);
......
** 注意:这里我们采用的是缺省的构造方法对Sort对象进行创建。
还可以在构造Sort对象的时候传递一个字符串类型的字段名作为参数,即采用构造方法Sort(String field)进行创建,
这样返回的搜索结果可以按照指定的字段进行排序。
还可以在构造Sort对象的时候传递一个字符串数组,代表一组字段名,即采用构造方法Sort(String[] fields)进行创建,
这样返回的搜索结果可以依次按照字段数组中指定的字段进行分组和排序。
1.2 最简单的排序----相关度:其实使用IndexSearcher的search(Query)方法就是使用的相关度(评分)查询,
向上边的使用缺省的Sort构造方法创建了一个Sort对象最为排序规则,因为它使用的是缺省构造方法,所以它也将会按照相关度排序。
还有就是如果不使用缺省Sort构造Sort对象,也不调用IndexSearcher的search(Query)方法,那么我们可以通过下边的形式实现相关度查询。
例如:其他代码和上边一样
Hits hits = searcher.search(query, Sort.RELEVANCE);这和传入Sort sort = new Sort();对象,可通过源代码分析。
** 说明:Sort类中有两个静态变量都是Sort类型的对象
1.2.1 Sort.RELEVANCE :该变量指对搜索结果按搜索的相关度进行排序,这也是系统默认采用的一种处理方式。
1.2.2 Sort.INDEXORDER :该变量指对搜索结果按照建立索引时的顺序进行排序。
** 一般性建议:在Lucene中"相关度(评分,即Sort.RELEVANCE表示的)"
和"文档ID(就是建立索引时保存文档的顺序,即Sort.INDEXORDER表示的)"往往会同时使用,
就是将搜索结果,先以相关度为标准降序排列;当遇到文档的相关度相同的时候,那么以文档ID为标准进行升序排列。
然后最后返回结果。
1.3 按Field字段对Document进行排序:在实际使用Lucene进行搜索时,经常会遇到需要按照某一Field字段进行排序的情况,
为此Lucene为我们提供了这种需求,但是有一个条件:就是该字段是"可排序"的。
所谓可排序我个人认为就是可以比较大小,像整型、字符串等都可以进行排序。
** 使用方法:要使用某一字段进行排序,必须创建一个Sort对象,并且提供作为排序条件的Field字段名最为构造方法的参数。
例如:Sort sort = new Sort("title");//其中title为Field字段名
例如:
//创建保存索引目录,这里用内存保存
Directory directory = new RAMDirectory();
//创建分析器
Analyzer analyzer = new SimpleAnalyzer();//这里创建的是SimpleAnalyzer类的分析器对象
//创建索引写入器
IndexWriter writer = new IndexWriter(directory, analyzer, true);
//创建将作为索引的文本内容
String[] contents = {
"abcde",
"abcdeabcde",
"abcdefghij",
"ace",
"aceace",
"aceabc"
};
//循环创建Document对象
for(int i = 0; i < contents.length; i++){
Document doc = new Document();
doc.add(Field.Text("content", contents[i]));
writer.addDocument(doc);
}
//创建完索引要关闭写入器
writer.close();
//搜索
IndexSearcher searcher = new IndexSearcher(directory);
//搜索关键字
String key = "ab";
//创建QueryParser对象:这里只指定了字段名称和分析器,并没有传入搜索关键字
QueryParser parser = new QueryParser("content", analyzer);
//创建Query对象
Query query = parser.parse(key);
//创建搜索结果排序规则:按照content字段对搜索结果进行排序
Sort sort = new Sort("content");
//搜索
Hits hits = searcher.search(query, sort);
//打印
......
** 注意:在这里因为content字段中保存的是字符串,所以是按照指定的字段以字母表的排列顺序为标准,进行升序排列。
** 注意:这里要特别注意的是,如果只指定了单独的一个字段作为排序规则的话(就想上边),
那么Sort类会自动的将"文档ID"添加为第二个排序规则。
换句话说就是再按照指定的字段排序后有次序相同的文档(即按照字母表顺序排序相同的文档),
此时会将相同次序的文档按照他们各自的文档ID(建立索引时保存文档的顺序编号)顺序进行排序。
** 说明:按照字段排序中,如果只指定一个排序规则的话,那么文档ID将充当隐藏排序规则。
也可认为是两个字段的排序(也即多字段排序,下一小节)。只不过这里是隐式的多字段排序。
1.4 对多个Field字段排序:对于多字段排序的使用可以通过SortField数组显示的指定排序的字段。
按照多个字段排序就是首先按照第一个字段分组排序,然后再在这个组内按照第二个字段进行排序,
如此这样实现多个字段的排序。
例如:前边代码同上,修改出如下
Sort sort = new Sort(
new SortField[]{
new SortField("title"),
new SortField("content")
}
);
//搜索
Hits hits = searcher.search(query, sort);
//打印
......
** 说明:代码中使用了两个字段title和content进行排序,而且title字段作为了第一个参数,
所以排序的时候就先按照title字段进行排序(分组排序),然后在该组内再按照content字段进行排序。
1.5 自定义的排序:如果上边的评分相关度排序、文档ID排序和通过多字段排序都不能满足我们的需求,
Lucene还实现了一种自定义的排序机制,就是我们可以定制我们自己的排序规则。
** 使用方法:自定义排序方法很简单,只要实现SortComparatorSource接口就可以。
** 注意:在建立索引时还不能确定排序标准的情况下,使用自定义的排序方法是最有效的。
2. 多字段搜索和多索引搜索:
2.1 使用MultiFieldQueryParser进行多字段搜索:在使用Lucene时,如果查询的只是某些分词Term,
而不关系这些分词Term到底是在哪个字段中。这是就可以使用MultiFieldQueryParser这个类来生成搜索条件Query对象。
** 说明:这个类是构建在QueryParser基础之上了,可以查看它源码。
默认情况下它会在每一个指定的字段上使用QueryParser的parse方法,生成一个Query对象,然后将该Query对象加入到BooleanQuery对象中,
构成复合查询条件,默认加入到BooleanQuery对象中后这些Query对象之间的关系是"或"的连接关系。
例如:
//创建搜索器
IndexSearcher searcher = new IndexSearcher("C://lucene_index");
//创建分析器
Analyzer analyzer = new StandardAnalyzer();
/*
解析生成Query对象:使用MultiFieldQueryParser.parse方法,传入搜索关键字,和指定要搜索的字段
返回的其实是BooleanQuery对象,默认的意思是对搜索关键字和title字段创建一个Query放入BooleanQuery中
然后再对搜索关键字和content字段创建一个Query对象方法BooleanQuery中,这两个个Query默认是"或"的关系
即:不管title还是content字段中那个里边有lucene都将会作为返回结果返回
*/
Query query = MultiFieldQueryParser.parse("lucene", new String[]{"title", "content"}, analyzer);
//搜索
Hits hits = searcher.search(query);
//打印
......
** 注意:上边我们使用的是默认的关系"或",我们还可以通过使用MultiFieldQueryParser中的两个静态变量来改变关系:
2.1.1 MultiFieldQueryParser.REQUIRED_FIELD :该字段中必须包含有输入的搜索关键字
2.1.2 MultiFieldQueryParser.PROHIBITED_FIELD :该字段中必须不包含有输入的搜索关键字
2.1.3 MultiFieldQueryParser.NORMAL_FIELD :该字段采用默认的情况,即"或",和不指定是一回事。
例如:
//构建Query对象
Query query = MultiFieldQueryParser.parse(
"lucene",
new String[]{"title","content"},
new int[]{MultiFieldQueryParser.REQUIRED_FIELD,MultiFieldQueryParser.PROHIBITED_FIELD},
analyzer
);
** 说明:像上面的Query查询对象的效果就是搜索在字段title中包含"lucene"关键字
同时在content字段中不包含"lucene"关键字的Document文档
2.2 使用MultiSearcher来同时搜索多个索引目录:如果系统中有多个索引目录,但是又要使用同一个单独的Query搜索条件对象,
那么可以使用MultiSearcher对这两个目录进行同时搜索并且将结果按一定的顺序(顺序或逆序)合并到一起。
当然了它也可以在一个索引目录进行搜索,这和IndexSearcher使用方法一样。这里我们只看多目录的搜索。
** 大概过程:
首先生成存放索引的两个目录;
然后往索引目录中添加一些索引文件;
再分别用两个索引目录构造两个读取器IndexSearcher用来读取信息;
再将两个读取器放到一个Searcher数组中,作为构造MultiSearcher对象的参数;
最后调用MultiSearcher的search(query)方法就可以对两个索引目录都使用query进行搜索了
例如:
//创建量内存索引目录
Directory directory1 = new RAMDirectory();
Directory directory2 = new RAMDirectory();
//创建三个文档,注意三个文档中的handle字段的值都一样
Document doc1 = new Document();
doc1.add(Field.Text("content", "Once upon a time......"));
doc1.add(Field.Keyword("handle","1"));
Document doc2 = new Document();
doc1.add(Field.Text("content", "A bizarre bug......"));
doc1.add(Field.Keyword("handle","1"));
Document doc3 = new Document();
doc1.add(Field.Text("content", "Once upon a time......"));
doc1.add(Field.Keyword("handle","1"));
//创建两个索引写入器对应两个目录
IndexWriter writer1 = new IndexWriter(directory1, new StandardAnalyzer(), true);
IndexWriter writer2 = new IndexWriter(directory2, new StandardAnalyzer(), true);
//创建索引:第一个目录中保存两个,第二个目录中保存一个
writer1.addDocument(doc1);
writer1.addDocument(doc2);
writer2.addDocument(doc3);
writer1.optimize();
writer2.optimize();
writer1.close();
writer2.close();
//创建Query对象:搜索handle字段中关键字1,所以连个目录结果加一块应该是三个文档
Query query = QueryParser.parse("1", "handle", new StandardAnalyzer());
//创建搜索器数组
Searcher[] searchers = {new IndexSearcher(directory1), new IndexSearcher(directory2)};
//创建多索引目录搜索器:传入搜索器数组
Searcher searcher = new MultiSearcher(searchers);
//搜索
Hits hits = searcher.search(query);
//打印
System.out.println("搜索到的文档数量是: " + hits.length());
** 结果说明:搜索到结果是三个文档。
2.3 使用ParallelMultiSearcher来构建多线程搜索:它是一个可以进行多线程搜索的MultiSearcher类,
也可以说成ParallelMultiSearcher是MultiSearcher的一个可以进行多线程搜索的版本。
** 功能:基本的搜索过程和带有过滤器的搜索过程都可以被并行化处理,但是使用HitCollector的搜索还不能够被并行处理。
** 使用场合:在应用程序中是否使用ParallelMultiSearcher取决于应用程序和系统的架构,
假设建立好的索引文件被存放在不同的屋里磁盘上,并且还可以同时使用多个CPU,
那么使用ParallelMultiSearcher或许会提高一些系统的效率。
** 使用方法:同MultiSearcher的使用完全一样。
3. 对搜索结果的过滤:
3.1 日期过滤器----DateFilter类:在进行搜索的时候,可能还对搜索结果有时间上的约束。
例如:只想得到2000年之后的相关搜索结果;或者只想得到2000~2002年之间的相关结果,
这是应该使用DateFilter对搜索结果进行过滤。
例子:
//创建Document
Document doc1 = new Document();
doc1.add(Field.Keyword("date", DateField.timeToString(System.currentTimeMillis() - 1000)));
doc1.add(Field.Text("content", "hao xing ......"));
//创建索引
......
//创建日期过滤器:对查询结果的文档的date字段的值进行过滤,选出当天时间之前的结果再返回
DateFilter filter1 = DateFilter.Before("date", System.currentTimeMillis());
//创建过滤器:对查询结果的文档的date字段进行过滤,选出当前时间前2000毫秒的结果返回
DateFilter filter2 = DateFilter.Before("date", (System.currentTimeMillis() - 2000));
/*
构建Query对象:通过这个Query对象是可以找到Document的,但是如果分别用两个不同的过滤器查询过滤,那么结果会不同
即第一个过滤器会找到结果,而第二个过滤器不会找到结果
*/
Query query = new TermQuery(new Term("content", "hao"));
//使用第一个过滤器搜索结果
Hits hits = searcher.search(query, filter1);//能搜索到结果
//使用第二个过滤器搜索结果
Hits hits = searcher.search(query, filter1);//不能搜索到结果,因为被过滤器过滤掉了
** 说明:创建过滤器的时候除了使用上边的方法还可以使用构造方法(具体看Javadoc文档)
** 注意:一定要告诉过滤器对那个字段进行过滤;对于上边的例子,我指定的是date字段。
3.2 查询过滤器----QueryFilter类:比DateFilter更为常用。
QueryFilter使用一个搜索结果来限制后续的搜索中可以可以得到的搜索结果,
并且在QueryFilter中使用了缓存,以优化搜索性能。
** 说明:QueryFilter甚至可以替代DateFilter的使用,尽管使用QueryFilter需要书写较多代码,并且看起来不是非常优雅。
例子:可以自己写。
3.3 带缓存的过滤器----CachingWrapperFilter类:使用过滤器最大的好处是他们可以被缓存继而被重用。
在DateFilter中没有使用缓存,但是在QueryFilter中就使用了缓存。
** 说明:Lucene提供了一个CachingWrapperFilter类,
可以使用该类对没有使用缓存的过滤器进行包装,这样就使被包装的过滤器自动具有了缓存的功能。
例如:具体的例子请到网上查看。
CachingWrapperFilter filter = new CachingWrapperFilter(传入要加入缓存的对象);