高级查询
在介绍了更多的高级映射功能之后,是时候回顾一下之前介绍过的查询功能了,看看如何借助这些高级的映射功能来使用一些高级的查询功能。本文会通过以下几个方面进行介绍:
- 如何在不和数据库进行任何交互的前提下,借助Lucene的力量来动态的筛选结果
- 如何通过使用基于投影(Projection)的查询来获取需要的属性,从而避免与数据库的交互
- 如何使用分面搜索(Faceted Search)对搜索结果进行划分
- 如何使用查询时提升(Boosting)
- 如何给查询设置时间限制
过滤(Filtering)
虽然是全文搜索,但是我们有时候需要将搜索的结果限定到某个范围内。比如,当我们只需要搜索特定设备上的支持的App,有以下几个思路:
-
将限定范围作为搜索关键字传入到查询对象中。但是稍微想想就会发现问题:这样做只会增大搜索的范围而导致更多的结果被返回,因为搜索关键字变多了。
-
使用布尔查询,向其中添加must子查询。这样做是可行的,只不过这样做会让DSL难以维护,失去其简洁的特点。同时,如果需要过滤逻辑相对比较复杂的话,使用DSL会让代码变的臃肿。
-
由于Hibernate Search中的FullTextQuery是继承自Hibernate ORM Query(或者相应的JPA Query)对象。所以我们可以考虑使用类似ResultTransformer这种对象进行过滤。但是这样做的问题是会让代码和数据库之间的交互变的更多,导致性能的下滑。
实际上,针对这一类问题Hibernate Search提供了一套更优雅和高效的解决方案:过滤器(Filter)。
过滤器会将过滤的逻辑封装到其中,然后在运行时通过动态地使用这些过滤器来完成需要的过滤操作。过滤行为是针对Lucene索引的,被过滤的内容绝对不会出现在最终的搜索结果中。因此从某种意义上而言,它也减小了最终需要从数据库中获取的数据量。
创建一个过滤器工厂
过滤器对应着Lucene中的org.apache.lucene.search.Filter类型。因此,对于简单的过滤器直接创建Filter类型的一个子类就够了。但是,如果想在运行时根据条件动态地生成Filter实例,就需要使用过滤器工厂:
public class DeviceFilterFactory {
private String deviceName;
@Factory
public Filter getFilter() {
PhraseQuery query = new PhraseQuery();
StringTokenizertokenzier = new StringTokenizer(deviceName);
while(tokenzier.hasMoreTokens()) {
Term term = new Term("supportedDevices.name", tokenzier.nextToken());
query.add(term);
}
Filter filter = new QueryWrapperFilter(query);
return new CachingWrapperFilter(filter);
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName.toLowerCase();
}
}
上述代码中最关键的就是@Factory注解的使用。它表明了getFilter方法能够返回一个过滤器实例。在getFilter的实现中,必须要使用一些Lucene的原生API,它虽然没有Hibernate Search DSL方便,但是也并不难理解。
最终返回的过滤器的类型时CachingWrapperFilter,使用它是为了将过滤器进行缓存来避免创建不必要的重复Filter,它封装了QueryWrapperFilter实例,而后者则建立在一个Query对象上。这个Query对象表示的就是进行筛选操作所必要的查询。这里我们想精确的匹配设备名称,因此使用的查询类型时短语查询(PhraseQuery)。
让我们回顾一下数据在被Lucene索引时所经历的过程:
- 解析器会进行字符过滤,分词和词条过滤,然后将每个词条都抽象为Lucene中的一个数据单元(即Term类型,上面的代码中有用到)。
- 在将数据写入索引前,默认的解析器会将字符串数据转换为小写的形式。
但是在使用Hibernate Search时,这些Lucene细节都不需要开发人员费心。可是,当像上述代码那样使用底层Lucene API时,就需要注意这些细节了。因此,在setDeviceName方法中,我们会将传入的deviceName转换为小写的。然后在创建Query类型时,会将分词得到的每个词条都先转换为Term类型,再添加到Query中。
添加过滤器键(Filter Key)
正因为在创建过滤器时,我们使用了CachingWrapperFilter完成了一次封装用来缓存该过滤器。所以当需要从缓存中取回某个过滤器时,我们还需要使用一个Key,这个Key就是所谓的过滤器键(Filter Key)。这里我们使用需要过滤的设备名称作为键,配合@Key实现如下:
@Key
Public FilterKey getKey() {
DeviceFilterKey key = new DeviceFilterKey();
key.setDeviceName(this.deviceName);
return key;
}
该方法也实现