Chapter 5. Querying


5.1. Building queries 5.1.1. Building a Lucene query using the Lucene API 5.1.2. Building a Lucene query with the Hibernate Search query DSL 5.1.3. Building a Hibernate Search query 5.2. Retrieving the results 5.2.1. Performance considerations 5.2.2. Result size 5.2.3. ResultTransformer 5.2.4. Understanding results 5.3. Filters 5.3.1. Using filters in a sharded environment 5.4. Faceting 5.4.1. Creating a faceting request 5.4.2. Applying a faceting request 5.4.3. Restricting query results 5.5. Optimizing the query process 5.5.1. Caching index values: FieldCache




Hibernate Search第二个最重要的能力就是执行lucene查询和检索Hibernate session中的实体.



准备和执行查询包括以下步骤:



  • 创建FullTextSession
  • 创建Lucene query,通过Hibernate Search query DSL (recommended)或者使用Lucene query API
  • Wrapping the Lucene query using an org.hibernate.Query

list()

  •  or 

scroll()

我们使用FullTextSession进行查询,通过传递一个Hibernate的session



Example 5.1. Creating a FullTextSession



Session session = sessionFactory.openSession();
...
FullTextSession fullTextSession =Search.getFullTextSession(session);



一旦你拥有了FullTextSession,你可以使用2种查询方法:



DSL查询方法:



final QueryBuilder b = fullTextSession.getSearchFactory()
    .buildQueryBuilder().forEntity( Myth.class ).get();

org.apache.lucene.search.Query luceneQuery =
    b.keyword()
        .onField("history").boostedTo(3)
        .matching("storm")
        .createQuery();

org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery );List result = fullTextQuery.list();//return a list of managed objects



二选一,你可以选择一种方法进行查询操作。下面的例子是lucene api查询.



Example 5.2. Creating a Lucene query via the QueryParser



SearchFactory searchFactory = fullTextSession.getSearchFactory();
org.apache.lucene.queryParser.QueryParser parser = 
    new QueryParser("title", searchFactory.getAnalyzer(Myth.class) );
try {
    org.apache.lucene.search.Query luceneQuery = parser.parse( "history:storm^3" );
}
catch (ParseException e) {
    //handle parsing failure
}

org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery(luceneQuery);List result = fullTextQuery.list();//return a list of managed objects



Note

Hibernate query方法是基于lucene query的:org.hibernate.Query, 这意味着Hibernate query也支持HQL, Native or Criteria). The regular list() , uniqueResult()iterate() and scroll()等平常我们使用的方法


你也可以使用JPA查询:



Example 5.3. Creating a Search query using the JPA API



EntityManager em = entityManagerFactory.createEntityManager();

FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

...finalQueryBuilder b = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(Myth.class).get();

org.apache.lucene.search.Query luceneQuery =  b.keyword().onField("history").boostedTo(3).matching("storm").createQuery();
javax.persistence.Query fullTextQuery = fullTextEntityManager.createFullTextQuery( luceneQuery );

List result = fullTextQuery.getResultList();//return a list of managed objects



Note

接下来的例子都是介绍hibernate apis,但是可以很方便的转换到jpa方式、



5.1. Building queries



5.1.1. Building a Lucene query using the Lucene API



使用lucene api,你可以有几个选项,使用query parser(简单查询),或者lucene programmatic api(复杂查询)



这个超出我们文档范围。请出门右拐找lucene文档。



 



5.1.2. Building a Lucene query with the Hibernate Search query DSL



使用lucene programmatic api进行全文检索挺麻烦的...balabala....



 



Hibernate Search 的DSL查询方法的api可以称作流畅的api(无耻 - -),有几个特性:



  • 方法名言简意赅
  • 省略不必要的配置
  • It often uses the chaining method pattern(没懂 - -)
  • 方便使用和阅读



现在我们来看如何使用API,首先需要一个QueryBuilder,绑定一个要查询的类。QueryBuilder知道用什么分析器,



使用什么桥。



你也可以重写域要使用的分析器,但是很少这么做。除非你知道你在做什么、



QueryBuilder mythQB = searchFactory.buildQueryBuilder().forEntity(Myth.class).overridesForField("history","stem_analyzer_definition").get();



使用query builder,要注意的是最终结果都是来自lucene query.因为这个原因,我们可以很容易的将lucene's query 



parser或者lucene programmatic api的查询通hibernate search DSL结合在一起,以防DSL不支持一些功能



 



5.1.2.1. Keyword queries 关键字查询



我们先来查询特定单词



Query luceneQuery = mythQB.keyword().onField("history").matching("storm").createQuery();



keyword()的意思是,查找一个特定的单词。OnField()指明查找哪个域。matching()为要查询的单词。



  • storm这个值通过history桥
  • 桥的值之后会传递给分析器,分析器对索引进行匹配。



我们来看看被搜索的属性不是String的时候:



@Entity@IndexedpublicclassMyth{  @Field(analyze =Analyze.NO)  @DateBridge(resolution =Resolution.YEAR)publicDate getCreationDate(){return creationDate;}publicDate setCreationDate(Date creationDate){this.creationDate = creationDate;}privateDate creationDate;...}

Date birthdate =...;Query luceneQuery = mythQb.keyword().onField("creationDate").matching(birthdate).createQuery();



Note

使用lucene必须将日期转化为String类型。而hibernate search不用



hibernate search支持各种变换,不单单是Date,也提供其他的桥,提供objectToString方法(太方便啦!lucene只支持



String,而hibernate帮我们封装好啦)



 



 



接下来我们来个有点难度的例子。使用连词分析器(ngram analyzers)。连词分析器可以弥补因为用户打错字,导致



搜索不到结果的情况。比如我们搜索(3-grams,应该是3个字母组合的意思)hibernate可以是: hib, ibe, ber, rna, nat, ate.



@AnalyzerDef(name ="ngram",  tokenizer = @TokenizerDef(factory =StandardTokenizerFactory.class),  filters ={    @TokenFilterDef(factory =StandardFilterFactory.class),    @TokenFilterDef(factory =LowerCaseFilterFactory.class),    @TokenFilterDef(factory =StopFilterFactory.class),    @TokenFilterDef(factory =NGramFilterFactory.class,      params ={        @Parameter(name ="minGramSize", value ="3"),        @Parameter(name ="maxGramSize", value ="3")})})@Entity@IndexedpublicclassMyth{  @Field(analyzer=@Analyzer(definition="ngram")  @DateBridge(resolution =Resolution.YEAR)publicString getName(){return name;}publicString setName(Date name){this.name = name;}privateString name;...}

Date birthdate =...;Query luceneQuery = mythQb.keyword().onField("name").matching("Sisiphus").createQuery();



在上面的例子中,我们搜索的关键字Sisiphus,会先转换成小写,然后分成3个字母组合(3-grams) sis, isi, sip, phu, hus. 每个



n-gram都将作为查询关键字。



 



Note

如果不想使用桥(field bridge)或者分析器,可以使用ignoreAnalyzer()和ignoreFieldBridge()



 



查询一个域里面可能包含的多个关键字使用:



//search document with storm or lightning in their historyQuery luceneQuery =    mythQB.keyword().onField("history").matching("storm lightning").createQuery();



查询几个域中可能包含关键字使用:



Query luceneQuery = mythQB.keyword().onFields("history","description","name").matching("storm").createQuery();



我们可以对域设置权重,name这个域权重为5:



Query luceneQuery = mythQB.keyword().onField("history").andField("name").boostedTo(5).andField("description").matching("storm").createQuery();



5.1.2.2. Fuzzy queries 模糊查询(应该只支持英文)



使用模糊字段查询。



Query luceneQuery = mythQB.keyword().fuzzy().withThreshold(.8f).withPrefixLength(1).onField("history").matching("starm").createQuery();


threshold(

临界值)规定了两个 terms 被认为相同(匹配)的上限,是 0 ~ 1 之间的数,默认是 0.5 。 prefixLength(前缀长度)说明了模糊性(被忽略的前缀长度):如果被设置为0,则任意一个非零的值被推荐(估计是匹配所有)

 


5.1.2.3. Wildcard queries 通配符查询



可以执行通配符搜索(查找只知道单词部分内容),“?”代表单个字符,“ * ”代表任意多个字符。 注意 :出于性能的考虑,查询时不要以通配符开头。



Query luceneQuery = mythQB.keyword().wildcard().onField("history").matching("sto*").createQuery();



5.1.2.4. Phrase queries 短语查询



可以使用它来搜索确切匹配或者相似的句子,可以使用 phrase ()来完成:



Query luceneQuery = mythQB.phrase().onField("history").sentence("Thou shalt not kill").createQuery();



也可以搜索相似的句子,可以通过添加一个 slop factor 来实现。它允许其它单词出现在这个句子中。



Query luceneQuery = mythQB.phrase().withSlop(3).onField("history").sentence("Thou kill").createQuery();



5.1.2.5. Range queries 边界查询



现在介绍边界搜索(可以作用在数字、日期、字符串等上)。边界搜索用来在某两个边界之间进行搜索,或者搜索给定值之上或之下的结果,示例代码如下



//look for0<= starred <3Query luceneQuery = mythQB.range().onField("starred").from(0).to(3).excludeLimit().createQuery();

//look for myths strictly BCDate beforeChrist =...;Query luceneQuery = mythQB.range().onField("creationDate").below(beforeChrist).excludeLimit().createQuery();



5.1.2.6. Combining queries 组合查询



最后介绍组合查询,可以创建更复杂的查询语句,有以下组合操作可以供使用:


SHOULD

  • : 查询应该包含子查询的结果。

MUST

  • : 必须包含匹配元素的子查询。

MUST NOT

  • : 一定不能包含。



//look for popular modern myths that are not urban



DatetwentiethCentury =...;



Query luceneQuery = mythQB.bool().must( mythQB.keyword().onField("description").matching("urban").createQuery()).not().must( mythQB.range().onField("starred").above(4).createQuery()).must( mythQB.range().onField("creationDate").above(twentiethCentury).createQuery()).createQuery();
//look for popular myths that are preferably urban



Query luceneQuery = mythQB.bool().should( mythQB.keyword().onField("description").matching("urban").createQuery()).must( mythQB.range().onField("starred").above(4).createQuery() ).createQuery();



//look for all myths except religious ones
Query luceneQuery = mythQB.all().except( monthQb.keyword().onField("description_stem").matching("religion").createQuery()).createQuery();



5.1.2.7. Query options



?     boostedTo:可以用在查询实体或字段中,使用给定的因子提升整个查询或特定字段。

?     withConstantScore (on query):和boost(作用)一样,所有匹配的查询结果有一个常量分数。

?     filteredBy(on query):使用过滤器过滤查询结果。

?     ignoreAnalyzer (on field):处理字段时忽略analyzer。

?     ignoreFieldBridge (on field):处理字段时忽略field bridge。



来看例子:


Query luceneQuery = mythQB 
   
     .bool() 
   
       .should( mythQB.keyword().onField("description").matching("urban").createQuery() ) 
   
       .should( mythQB 
   
         .keyword() 
   
         .onField("name") 
   
           .boostedTo(3) 
   
           .ignoreAnalyzer() 
   
         .matching("urban").createQuery() ) 
   
       .must( mythQB 
   
         .range() 
   
           .boostedTo(5).withConstantScore() 
   
         .onField("starred").above(4).createQuery() ) 
   
     .createQuery();




5.1.3. Building a Hibernate Search query 构建hibernate search查询



目前为止我们只讨论了如何创建 LuceneQuery ,这只是一系列动作中的第一步,现在看一看如果从 Lucene Query 创建 Hibernate Search Query 。



5.1.3.1. Generality



Lucene Query被创建,他需要被包装成一个Hibernate查询。如果没有特殊说明,它将会对所有的索引实体进行查询,可能返回所有的索引类的类型。



从性能的角度考虑,建议限制返回的实体类型。



Example 5.4. Wrapping a Lucene query into a Hibernate Query



FullTextSession fullTextSession = Search.getFullTextSession( session );
org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery );



Example 5.5. Filtering the search result by entity type



fullTextQuery = fullTextSession    .createFullTextQuery( luceneQuery, Customer.class );

// or

fullTextQuery = fullTextSession    .createFullTextQuery( luceneQuery, Item.class, Actor.class );



在例 5.5 中,第一个例子只返回匹配 Customer 的结果,第二个例子返回匹配 Actor 和 Item 类的机构。结果限制是多态实现的,也就是说如果有两个子类 Salesman 和 Custom 继承自父类 Person ,可以只指定 Person.class 来过滤返回结果。






5.1.3.2. Pagination 分页



由于性能的原因,推荐每次查询返回一定数量的查询结果。事实上用户浏览时从一页翻到另一页是非常常见的情况。你定义翻页的方法正是使用 HQL 或 Criteria 定义分页的方法。



Example 5.6. Defining pagination for a search query



org.hibernate.Query fullTextQuery =     fullTextSession.createFullTextQuery( luceneQuery, Customer.class );
fullTextQuery.setFirstResult(15); //start from the 15th
elementfullTextQuery.setMaxResults(10); //return 10 elements



Tip

可以使用fulltextQuery.getResultSize()获取全部匹配元素的个数。



5.1.3.3. Sorting 排序



apache lucene提供非常强大方便的排序功能,



Example 5.7. Specifying a Lucene Sort


org.hibernate.search.FullTextQuery query = s.createFullTextQuery( query, Book.class );
org.apache.lucene.search.Sort sort = new Sort(new SortField("title", SortField.STRING));
query.setSort(sort);
List results = query.list();




Tip







注意需要排序的域是不能被标注为分词的( tokenized )



5.1.3.4. Fetching strategy 抓取策略



Example 5.8. Specifying FetchMode


Criteria criteria =     s.createCriteria( Book.class ).setFetchMode( "authors", FetchMode.JOIN );
s.createFullTextQuery( luceneQuery ).setCriteriaQuery( criteria );



上面的例子将返回所有luceneQuery匹配的Books,authors将被作为外部链接加载。



Important

只有设置fetch mode才可以使用criteria的restriction



Important

如果返回多个不同类型实体,则不能使用setCriteriaQuery



5.1.3.5. Projection 投影



有些时候不需要返回整个实体模型,而仅仅是实体中的部分字段。 Hibernate Search 允许你这样做,即返回几个字段。



Example 5.9. Using projection instead of returning the full domain object



org.hibernate.search.FullTextQuery query =     s.createFullTextQuery( luceneQuery, Book.class );
query.setProjection( "id", "summary", "body", "mainAuthor.name" );
List results = query.list();
Object[] firstResult = (Object[]) results.get(0);
Integer id = firstResult[0];
String summary = firstResult[1];
String body = firstResult[2];
String authorName = firstResult[3];



5.1.3.6. Customizing object initialization strategies 自定义对象初始化策略



设置hibernate search先从二级缓存取实体还是先从database中取:



Example 5.11. Check the second-level cache before using a query



FullTextQuery query = session.createFullTextQuery(luceneQuery, User.class);query.initializeObjectWith(    ObjectLookupMethod.SECOND_LEVEL_CACHE,    DatabaseRetrievalMethod.QUERY);



ObjectLookupMethod.PERSISTENCE_CONTEXT

  • : useful if most of the matching entities are already in the persistence context (ie loaded in the 

Session

  •  or 

EntityManager

  • )

ObjectLookupMethod.SECOND_LEVEL_CACHE

  • : check first the persistence context and then the second-level cache.



5.1.3.7. Limiting the time of a query 限制时间查询



使用Hibernate Search进行全文检索时,你可以使用下面两种方式限制每次查询的时间:

?       当限定时间到时抛出异常

?       当限定时间到时限制查询结果的个数。(EXPERIMENTAL)

两种方式不兼容。



5.2. Retrieving the results 检索结果


一旦建立了Hibernate Search query.执行查询操作就像执行HQL,Criteria查询一样,

list()uniqueResult()iterate()scroll()


5.2.1. Performance considerations 考虑效率


如果需要返回特定结果,(比如利用分页),并且希望所有查询结果都运用该规则,推荐

list() or uniqueResult()。

list()可以设置batch-size。当使用

list() , uniqueResult() and iterate()时,

注意hibernate search会处理所有Lucene匹配的索引(包括分页)





如果你希望尽量少去加载lucene document,scroll非常适合。别忘了使用完关闭ScrollableResults对象



Important

分页比用scrolling好



5.2.2. Result size 返回结果数量



有时候我们需要知道搜到到的结果集数量



  • 像我们使用google搜索时,显示的结果数量 "1-10 of about 888,000,000"
  • 分页需要
  • to implement a multi step search engine (adding approximation if the restricted query return no or not enough results)



将所有匹配到的lucene document都取出来肯定会损耗很多资源。



hibernate search允许获取所有匹配到的索引document,即使你设置了分页参数,.更有趣的是,支持获取所有索引个数,而不需要加搜索条件



Example 5.16. Determining the result size of a query



org.hibernate.search.FullTextQuery query =     s.createFullTextQuery( luceneQuery, Book.class );
//return the number of matching books without loading a single one
assert 3245 == query.getResultSize(); 

org.hibernate.search.FullTextQuery query =     s.createFullTextQuery( luceneQuery, Book.class );
query.setMaxResult(10);List results = query.list();
//return the total number of matching books regardless of pagination
assert 3245 == query.getResultSize();



Note

就像Google,搜索结果数量只是个大概,如果有索引还没有被更新添加



5.2.3. ResultTransformer 结果转换



就像 Section 5.1.3.5, “Projection”章节看到的投影结果就是返回成一个Object数组。



但有时候这样的数据结构不是我们想要的,那么我们可以转换:



Example 5.17. Using ResultTransformer in conjunction with projections



org.hibernate.search.FullTextQuery query =     s.createFullTextQuery( luceneQuery, Book.class );
query.setProjection( "title", "mainAuthor.name" );
query.setResultTransformer( 
    new StaticAliasToBeanResultTransformer( 
        BookView.class, 
        "title", 
        "author" ) 
);
List<BookView> results = (List<BookView>) query.list();
for(BookView view : results) {
log.info( "Book: " + view.getTitle() + ", " + view.getAuthor() );
}



上面的例子,将投影的两个域title,mainAuthor.name,利用ResultTransformaer封装成BookView(tile,author)类.



5.2.4. Understanding results 理解、调试结果



有时候我们查询得到的结果不是我们想要的,比如返回空结果或者乱七八糟,我们可以利用



luke来调试。但是hibernate search也提供一个操作lucene解释类(  Explanation object )的方法。


fullTextQuery.explain(int)

  • 使用projection



第一个方式使用ducument id作为参数、获得Explanation对象。document id 可以通过projection或者



FullTextQuery.DOCUMENT_ID



Warning

Document ID 和 实体类的ID不是同一个东西



第二个方法:利用FullTextQuery.EXPLANATION常量



Example 5.18. Retrieving the Lucene Explanation object using projection



FullTextQuery ftQuery = s.createFullTextQuery( luceneQuery, Dvd.class )
.setProjection(
FullTextQuery.DOCUMENT_ID,
FullTextQuery.EXPLANATION,
FullTextQuery.THIS );
@SuppressWarnings("unchecked") List<Object[]> results = ftQuery.list();
for (Object[] result : results) {
Explanation e = (Explanation) result[1];
display( e.toString() );
}



注意,在使用explanation对象的时候,会粗略、损耗性大的再跑一遍与lucene query。所以必须的



时候再使用这个。






5.3. Filters 过滤器



apache lucene允许使用filter过滤器过滤查询结果,也支持自定义的过滤器。应用例子:



  • security
  • temporal data (eg. view only last month's data)
  • population filter (eg. search limited to a given category)
  • and many more



Hibernate Search过滤器类似Hibernate过滤器:



Example 5.19. Enabling fulltext filters for a given query



fullTextQuery = s.createFullTextQuery( query, Driver.class );
fullTextQuery.enableFullTextFilter("bestDriver");
fullTextQuery.enableFullTextFilter("security").setParameter( "login", "andre" );
fullTextQuery.list(); //returns only best drivers where andre has credentials



上面的例子中我们启用了两个过滤器。






通过@FullTextFilterDef标注声明过滤器。过滤器可以标注在任何被@Indexed的实体类。



过滤器必须实现filter的函数






Example 5.20. Defining and implementing a Filter



@Entity @Indexed @FullTextFilterDefs( { @FullTextFilterDef(name = "bestDriver", impl = BestDriversFilter.class), @FullTextFilterDef(name = "security", impl = SecurityFilterFactory.class) }) public class Driver { ... }



public class BestDriversFilter extends org.apache.lucene.search.Filter {

    public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
OpenBitSet bitSet = new OpenBitSet( reader.maxDoc() );
TermDocs termDocs = reader.termDocs( new Term( "score", "5" ) );
while ( termDocs.next() ) {
bitSet.set( termDocs.doc() );        }        return bitSet;    }}



下步意义。