转自:http://sin90lzc.iteye.com/blog/1106714
Querying
Hibernate Search的第二个很重要的能力是运行Lucene queries并通过Hibernate session获得受管理的实体。search在提供了Lucene强大的功能之外还保持着Hibernate的编程模式(给Hibernate典型的search机制提供另外的dimension:HQL,Criteria query,native SQL query)
预备和运行一个query由4个步骤组成:
- 创建一个FullTextSession
- 通过Hibernate Search query DSL(推荐的)或Lucene query API创建一个Lucene query。
- 使用org.hibernate.Query包装Lucene query
- 通过调用list() 或scroll()执行search
你必须使用FullTextSession来访问query功能。这个具体的search session包装了一个一般的org.hibernate.Session来提供query和indexing能力。
Example 5.1. Creating a FullTextSession
- Session session = sessionFactory.openSession();
- ...
- FullTextSession fullTextSession = Search.getFullTextSession(session);
当你有了FullTextSession之后,你有两种方式来生成full-text query:the Hibernate Search query DSL 或 the native Lucene query.
如果你使用的是Hibernate Search query 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 query parser或Lucene programmatic API来得到Lucene query。
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:建立在Lucene query之上的Hibernate query是一般的org.hibernate.Query实现,这意味着你可以使用与其他Hibernate query功能(HQL,Native or Criteria)相同的模式来编程。org.hibernate.Query中的list(),uniqueResult(),iterate()和scroll()方法都可以使用。
你也可以使用Hibernate的Java Persistence API:
Example 5.3. Creating a Search query using the JPA API
- EntityManager em = entityManagerFactory.createEntityManager();
- FullTextEntityManager fullTextEntityManager =
- org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
- ...
- final QueryBuilder 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 API,不过也可能很容易地使用JPA来重写。
5.1. Building queries
Hibernate Search的query是建立在Lucene query之上,Lucene query给你最大的自由度来提供具体的Lucene query类型来运行查询。这样的话,org.hibernate.Query包装了lucene query作为你主要的操作API。
5.1.1.通过Lucene API来创建Lucene query(Building a Lucene query using the Lucene API)
你有多种方式来使用Lucene API。你可以使用query parser(对于简单查询来说已经足够了)或Lucene编程API(对于复杂的用例)。如何创建一个Lucene query超过了本文档的范围。具体请查阅Lucene在线文档《Lucene In Action》或《Hibernate Search in Action》
5.1.2. 使用Hibernate Search query DSL创建一个Lucene query(Building a Lucene query with the Hibernate Search query DSL)
使用Lucene编程API来生成query是相当复杂的。因为与生俱来的API复杂性,你必须记得转换参数到相等的字符串并确保fields应用正确的analyzer。
Hibernate Search query DSL使用了一种称为流畅的(fluent)API.这类API有一些关键的特征:
- 带有清晰语义的方法名来完成一系列的操作
- 它限制一些有用的选项提交给上下文
- 它经常使用方法链模式。
- 它很容易使用并可读性高。
- QueryBuilder mythQB = searchFactory.buildQueryBuilder().forEntity( Myth.class ).get();
你也可以覆盖属性域上的analyzer。这一般都很少使用和应该避开这样的做法,除非你知道自己在做什么。
- QueryBuilder mythQB = searchFactory.buildQueryBuilder()
- .forEntity( Myth.class )
- .overridesForField("history","stem_analyzer_definition")
- .get();
你可以使用query builder来生成query。很重要的一点是,你要认识到QueryBuilder的最终结果是一个Lucene query。基于这个原因,你可以很容易地通过Lucene query parser或Lucene编程API来生成Lucene query,并与Hibernate Search DSL一起使用。只有在DSL缺少某些功能的时候才应该去使用Lucene编程API。
5.1.2.1. 关键字查询(Keyword queries)
让我们从最常用的用例说起-搜索某个词:
- Query luceneQuery = mythQB.keyword().onField("history").matching("storm").createQuery();
keyword()方法意味着你正在尝试着去查询一个指定的词语。onField()方法指定查询哪个Lucene field。matching()方法告诉查询哪个词语。最后createQuery()方法创建Lucene query对象。除了这些方法之外,还能很多其他的方法组合来生成query。
- 值'store'传递给history属性的FieldBridge:由于这里'store'是字符串,FieldBridge并不做任何的转换,但如果是数字或日期值,你就会看到转换效果。
- 接着field bridge的值会传递给与索引history属性相同的analyzer。这可以确保在查询时与索引时使用相同的term转换。如果解析过程中生成了多个term,那么将会使用逻辑查询(boolean query)中的SHOULD逻辑(即OR逻辑)
下面我们看一下怎么搜索一个属性域并不是一个字符串类型的情形。
- @Entity
- @Indexed
- public class Myth {
- @Field(index = Index.UN_TOKENIZED)
- @DateBridge(resolution = Resolution.YEAR)
- public Date getCreationDate() { return creationDate; }
- public Date setCreationDate(Date creationDate) { this.creationDate = creationDate; }
- private Date creationDate;
- ...
- }
- Date birthdate = ...;
- Query luceneQuery = mythQb.keyword().onField("creationDate").matching(birthdate).createQuery();
Note:在一般的Lucene中,你需要转换Date对象为它的字符串形式。
这个自动转换功能适用于所有对象,并不局限于Date,只要属性域上有对应的FieldBridge就可以了。
我们看一下一个高级点的例子,怎么样去搜索使用了ngram analyzer的field。ngram analyzer索引一连串的词语的ngram,ngram可以还原用户错字。例如把hibernate拆分成3-grams:hib, ibe, ber, ern,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
- @Indexed
- public class Myth {
- @Field(analyzer=@Analyzer(definition="ngram")
- public String getName() { return name; }
- public void setName(String name) { this.name = name; }
- private String name;
- ...
- }
- Date birthdate = ...;
- Query luceneQuery = mythQb.keyword().onField("name").matching("Sisiphus")
- .createQuery();
搜索词"Sisiphus"会小写,并分割成3-grams:sis,isi,sip,iph,phu,hus。每个n-gram都会是query的一部分。那么我们就可以通过查询找到Sysiphus myth。所有的这些都可以自动地完成。
Note:对于某些原因,你可能不想某个field使用field bridge或analyzer,你可以调用 ignoreAnalyzer()方法或ignoreFieldBridge()方法。
如果想要在同一个field中搜索多个词,只需要简单地在matching语句中添加他们。
- //search document with storm or lightning in their history
- Query luceneQuery =
- mythQB.keyword().onField("history").matching("storm lightning").createQuery();
如果想搜索多个field,可以使用onFields方法。
- Query luceneQuery = mythQB
- .keyword()
- .onFields("history","description","name")
- .matching("storm")
- .createQuery();
有时候,一个field与其他field可能会被不同地对待,即使是搜索同一个term,你可以使用andField()方法来达到这种需求。
- Query luceneQuery = mythQB.keyword()
- .onField("history")
- .andField("name")
- .boostedTo(5)
- .andField("description")
- .matching("storm")
- .createQuery();
在上一个例子中,只有field 'name'优先级定为5。
5.1.2.2. 模糊查询(Fuzzy queries)
运行一个模糊查询(基于Levenshtein距离算法),像keyword查询一样,添加一个fuzzy()方法标志。
- Query luceneQuery = mythQB
- .keyword()
- .fuzzy()
- .withThreshold( .8f )
- .withPrefixLength( 1 )
- .onField("history")
- .matching("starm")
- .createQuery();
threshold是一种限制,它定义了如何认为两个term是匹配的。它是一个0-1之间的数字,默认是0.5。prefixLength定义了模糊查询忽略前缀的长度:默认是0,如果能明确地知道前缀的话,推荐赋予一个非零值。
5.1.2.3. 通配符查询(Wildcard queries)
你也可以执行通配符查询(查询词中有某些部分是不确定的)。'?'代表了一个字符,*代表任何的字符串。为了性能的表现目的,推荐query不要以?或*开始。
- Query luceneQuery = mythQB
- .keyword()
- .wildcard()
- .onField("history")
- .matching("sto*")
- .createQuery();
Note:通配符查询是不会应用analyzer的,否则的话?和*将会很大可能被删掉。
5.1.2.4. Phrase queries
到目前为止,我们已经能查询单个或多个词语了。你也同样可以搜索精确的、接近的短句。使用phrase()方法完成这个需求。
- Query luceneQuery = mythQB
- .phrase()
- .onField("history")
- .matching("Thou shalt not kill")
- .createQuery();
你可以通过添加一个slop因子来搜索接近的短句。slop因子表示短句中词语间允许的间隔。
- Query luceneQuery = mythQB
- .phrase()
- .withSlop(3)
- .onField("history")
- .matching("Thou kill") .createQuery();
5.1.2.5. 范围查询(Range queries)
一个范围查询搜索某个范围内的值或高于(above)或是低于(below)某个界限的值。
- //look for 0 <= starred < 3
- Query luceneQuery = mythQB
- .range()
- .onField("starred")
- .from(0).to(3).excludeLimit()
- .createQuery();
- //look for myths strictly BC
- Date beforeChrist = ...;
- Query luceneQuery = mythQB
- .range()
- .onField("creationDate")
- .below(beforeChrist).excludeLimit()
- .createQuery();
5.1.2.6. 组合查询(Combining queries)
最后你还可以组合查询来形成更复杂的查询。下列的逻辑操作是允许的:
- SHOULD: query之间是或关系。
- MUST:query之间是并关系。
- MUST NOT:必须不包含该query。
- //look for popular modern myths that are not urban
- Date twentiethCentury = ...;
- 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();
- boostedTo (on query type and on field): 使用给定的factor来boost整个query或指定的field
- withConstantScore (on query):所有匹配query的结果都有一个常量的分数等同于boost。
- filteredBy(Filter) (on query):使用Filter实例来过滤查询结果
- ignoreAnalyzer (on field):不用analyzer来处理这个field。
- ignoreFieldBridge (on field):不用field bridge来处理这个field。
让我们看看应用了这些选项的例子:
- 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. 创建Hibernate Search查询(Building a Hibernate Search query)
到目前为止,我们只讲述了怎么样创建Lucene query的过程(see Section 5.1, “Building queries”)。然而,这只是搜索链中的第一步。让我们看看怎么样由Lucene query建立Hibernate Search query
5.1.3.1. 概要(Generality)
创建了Lucene query之后,它需要包装进hibernate Query中去。如果没有指定Lucene query,query将会查询所有的indexed实体,潜在地返回所有indexed的类。
Example 5.4. Wrapping a Lucene query into a Hibernate Query
- FullTextSession fullTextSession = Search.getFullTextSession( session );
- org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery );
从性能的角度来看,推荐限制返回类型:
- fullTextQuery = fullTextSession
- .createFullTextQuery( luceneQuery, Customer.class );
- // or
- fullTextQuery = fullTextSession
- .createFullTextQuery( luceneQuery, Item.class, Actor.class );
In Example 5.5, “Filtering the search result by entity type” 第一个例子只返回Customer类型的结果,第二个例子只返回Actor和Item的类型。类型约束是支持多态(例如:Customer和Salesman继承于Person,如果要求结果集能返回Customer和Salesman,只需要指定Person.class即可)。
5.1.3.2. 分页(Pagination)
出于性能的考虑,推荐约束每次查询返回的对象数量。事实上,这是一个非常普通的用例可以让用户浏览一页的数据。定义pagination与在plain HQL或Criteria query中定义分页是完全一样的。
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 element
- fullTextQuery.setMaxResults(10); //return 10 elements
Tip:fulltextQuery.getResultSize()方法可以返回所有匹配对象的总数,不管你是否应用了pagination。
5.1.3.3. Sorting
Apache Lucene已经提供了一个非常灵活和强大的方式去排序结果。默认的排序是relevance(按分数排序)适用于大多数的时候,不过也可以按一个或多个属性域来排序。为了达到这个目的,可以设置Lucene Sort对象来应用Lucene的sorting strategy。
Example 5.7. Specifying a Lucene Sort in order to sort the results
- 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:用作排序的field不能被tokenized。
5.1.3.4. 抓取策略(Fetching strategy)
当你限制返回类型只有一个类的时候,Hibernate Search使用一个查询语句来加载对象。该查询同样会使用域模型中定义的抓取策略。然而,也可以针对某个具体的用例来调整抓取策略。
Example 5.8. Specifying FetchMode on a query
- Criteria criteria = s.createCriteria( Book.class ).setFetchMode( "authors", FetchMode.JOIN );
- s.createFullTextQuery( luceneQuery ).setCriteriaQuery( criteria );
在这个例子中,查询会返回所有的匹配luceneQuery的Books。authors集合也会使用SQL外连接在同一个查询语句中加载进来。
当定义一个criteria查询的时候,Hibernate Search query不需要约束返回的实体类型,该返回类型由criteria查询定义。
Important:如果期望的返回类型不止1个的话,不能使用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];
Hibernate Search从Lucene index中抽离出实体对象的属性并把它们向上转换成Object,最终结果返回Object[]列表。Projection避开了潜在数据库的查询(如果响应时间很重要的话,这就会很有用了)。然而,它还有一些约束条件:
- 投影所对应的属性必须保存在index中,即@Field(store=Store.YES)
- 属性投影必须使用org.hibernate.search.bridge.TwoWayFieldBridge或org.hibernate.search.bridge.TwoWayStringBridge的FieldBridge实现,后者是一个简便版本。
Note:所有Hibernate Search内建类型都是two-way的。
- 你只可以投影实体对象或其关联对象中的简单属性。意思是说不能投影内嵌的整个实体。
- 投影不能应用于使用了@IndexedEmbedded的集合或map。
Example 5.10. Using projection in order to retrieve meta data
- org.hibernate.search.FullTextQuery query =
- s.createFullTextQuery( luceneQuery, Book.class );
- query.setProjection(
- FullTextQuery.SCORE,
- FullTextQuery.THIS,
- "mainAuthor.name" );
- List results = query.list();
- Object[] firstResult = (Object[]) results.get(0);
- float score = firstResult[0];
- Book book = firstResult[1];
- String authorName = firstResult[2];
你可以混合投影field和projection常量。下面列举了可用的projection常量:
- FullTextQuery.THIS:返回整个受管对象(这不再是使用projected query)
- FullTextQuery.DOCUMENT:返回实体对象对应的Lucene Document。
- FullTextQuery.OBJECT_CLASS:返回实体对象的class。
- FullTextQuery.SCORE:返回对应的document score
- FullTextQuery.ID:投影对象的id值。
- FullTextQuery.DOCUMENT_ID:投影Lucene document id。小心,Lucene document id会在打开新的IndexReader时变得不一样。(这个功能还在测试中)
- FullTextQuery.EXPLANATION:返回匹配的Lucene Explanation对象。
5.1.3.6.自定义对象初始化策略(Customizing object initialization strategies)
默认地,Hibernate Search使用最合适的策略来初始化匹配的实体对象。它运行一个或多个查询来获取请求的实体对象。当实体对象存储在持久化上下文或二级缓存中时,默认的方法会最小程度地访问数据库,因此也是最好的方法。
如果大多数的实体对象缓存在二级缓存中,你可以强迫Hibernate Search先从缓存中获取对象,如果没有再访问数据库。
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定义了一些策略去检查是否请求的对象是容易访问的(accessible),即不需要访问数据库。下面列举了其他一些选项:
- ObjectLookupMethod.PERSISTENCE_CONTEXT:在大多数匹配对象都保存在持久化上下文时很有用(如从Session或 EntityManager中加载)
- ObjectLookupMethod.SECOND_LEVEL_CACHE:先从持久化上下文中查找对象,如果没有再从二级缓存中查找。
Note:要应用二级缓存,首先要配置好以下几点:
- 配置好并激活二级缓存的相关属性
- 对应实体必须开启二级缓存功能(如@Cacheable)
- 必须允许Session,EntityManager,Query访问二级缓存(如本地Hibernate API中的CacheMode.NORMAL或JPA2 API中的CacheRetrieveMode.USE)
Warning:如果不是使用EHCache或Infinispan作为二级缓存,应避免使用ObjectLookupMethod.SECOND_LEVEL_CACHE。因为其他的缓存provider实现的效果并不好。
你同样可以通过DatabaseRetrievalMethod定义如何从数据库中加载对象:
- QUERY(default):使用一个或多个查询从数据库中批量加载对象。一般来说这是最好的方法。
- FIND_BY_ID:一个一个地通过Session.get或Entitymanager来加载对象。如果在entity上设置了batch-size,这就会很有用了。
5.1.3.7. 限制查询时间(Limiting the time of a query)
在Hibernate Search有两种方式可以限制一个查询任务时间:
- 当限制时间到达时,抛出一个异常。
- 返回限制时间到达前获取的结果。
5.1.3.7.1. 时间到达时抛出异常(Raise an exception on time limit)
当查询的时间超过限制,就会抛出QueryTimeoutException异常(org.hibernate.QueryTimeoutException或javax.persistence.QueryTimeoutException,这依赖于你的编程API)
使用下面其中一种方法来定义时间限制:
Example 5.12. Defining a timeout in query execution
- Query luceneQuery = ...;
- FullTextQuery query = fullTextSession.createFullTextQuery(luceneQuery, User.class);
- //define the timeout in seconds
- query.setTimeout(5);
- //alternatively, define the timeout in any given time unit
- query.setTimeout(450, TimeUnit.MILLISECONDS);
- try {
- query.list();
- }
- catch (org.hibernate.QueryTimeoutException e) {
- //do something, too slow
- }
同样的,时间限制对于getResultSize(), iterate() 和scroll()来说是直到这些方法调用结束为止。这就是说Iterable方法或ScrollableResultes方法将忽略时间限制。
Note:时间限制不能约束explain()方法,该方法只用于调试目的,特别是用于查出一些查询会很慢的原因。
当使用的是JPA,简单地使用标准的方式来限制查询运行时间。
Example 5.13. Defining a timeout in query execution
- Query luceneQuery = ...;
- FullTextQuery query = fullTextEM.createFullTextQuery(luceneQuery, User.class);
- //define the timeout in milliseconds
- query.setHint( "javax.persistence.query.timeout", 450 );
- try {
- query.getResultList();
- }
- catch (javax.persistence.QueryTimeoutException e) {
- //do something, too slow
- }
Important:记住,这是一个最有效的方法,但并不能保证在指定的时间内精确地停止。
5.1.3.7.2.返回返回限制时间到达前获取的结果(测试中) (Limit the number of results when the time limit is reached(EXPERIMENTAL))
另外,你可以获取在时间到达时已经读取的对象。注意,只有在查询Luene index时才受此限制。意思是说,还可以花更长的时间来获取受管状态的对象。(大概是指限制时间是查询index的时间,不包括数据库访问的时间)
Warning:该方法与 setTimeout方法是不兼容的。
使用下面的方法来定义这个限制。
Example 5.14. Defining a time limit in query execution
- Query luceneQuery = ...;
- FullTextQuery query = fullTextSession.createFullTextQuery(luceneQuery, User.class);
- //define the timeout in seconds
- query.limitExecutionTimeTo(500, TimeUnit.MILLISECONDS);
- List results = query.list();
同样的,时间限制对于getResultSize(), iterate() 和scroll()来说是直到这些方法调用结束为止。这就是说Iterable方法或ScrollableResultes方法将忽略时间限制。
你可以通过调用hasPartialResults方法来确定是否加载了部分结果
Example 5.15. Determines when a query returns partial results
- Query luceneQuery = ...;
- FullTextQuery query = fullTextSession.createFullTextQuery(luceneQuery, User.class);
- //define the timeout in seconds
- query.limitExecutionTimeTo(500, TimeUnit.MILLISECONDS);
- List results = query.list();
- if ( query.hasPartialResults() ) {
- displayWarningToUser();
- }
limitExecutionTimeTo 和 hasPartialResults方法也同样适用于JPA API.
5.2. 获取结果集(Retrieving the results)
当建立了Hibernate Search query后,运行它与运行HQL,Criteria查询没什么不同,都使用相同的编程模式和对象语义。所有普通的操作都是可用的:list(),uniqueResult(),iterate(),scroll()。
5.2.1.性能考虑(Performance considerations)
如果你期望返回合理数量的结果(如分页)并work on它们,list()和uniqueResult()方法是推荐使用的。如果实体的batch-size设置合适的话,list()会工作得最好。在使用list(),uniqueResult(),iterate()方法,Hibernate Search必须处理所有Lucene匹配的元素。
如果你想最小化加载Lucene document,scroll()方法会更合适。在你完成工作后,不要忘记关闭ScrollableResult对象,因为它保持着Lucene资源。如果你想使用scroll又想批量加载对象,你可以使用query.setFetchSize()方法。如果一个对象被访问,而它又没被加载过,Hibernate Search将会加载下一批对象。
Important:Pagination优于scrolling。
5.2.2. 结果大小(Result size)
有时候很有必要知道匹配的文档总数:
- 像Google中的功能"1-10 of about 888,000,000"
- 实现一个快速的pagination浏览
- 实现一个多步骤的搜索引擎(当查询没有返回任何结果或结果数不足时,添加接近的结果)
当然,获取所有匹配的document是非常消耗资源的。不管pagination参数如何,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,结果总量是一个接近数,因为index并不完全跟数据库一致(如异步集群方式)
5.2.3. 结果转换器(ResultTransformer)
在Section 5.1.3.5, “Projection”中看到的projection返回的结果是Object数组。这种数据结构并不总是适合应用的需要。在这种情况下,就可能需要应用一个ResultTransformer,它可以运行完查询后建立需要的数据结构。
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() );
- }
ResultTransformer的实现例子可以在Hibernate Core codebase中找到。
5.2.4. Understanding results
有时候你可能会因为某些查询结果而感到迷惑。Luke是一个很好的工具帮助你了解查询的结果。然而,Hibernate Search也能让你从给定的query中访问Lucene Explanation对象。这个对象对于Lucene用户来说是相当高级的,不过能为理解一个结果的分数提供很好的帮助。你有两种方式来访问结果对应的Explanation对象:
- 使用fullTextQuery.explain(int)方法
- 使用projection
Warning:document id与实体的id是不同的。不要混淆这两个概念。
第二种方法让你使用FullTextQuery.EXPLANATION常量来投影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可以被缓存和重用。下面列举了一些有趣的用例:
- 安全性过滤
- 过滤时间数据(比如说只能查找到上个月的数据)
- population filter (比如说限制搜索某个给定的分类)
- 还有很多很多
Hibernate Search把这个概念再推进了一步,称为能自动缓存的参数化命名过滤器(parameterizable named filters)。这与大家所熟悉的Hibernate Core过滤器API是非常相似的:
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
在这个例子中,我们在query之上使用了两个filter。如果你有这个需要的话,你可以使用任意数量的filter。
声明filter是通过@FullTextFilterDef注解完成。该注解标注在带有@Indexed注解的实体上。这暗示了filter声明是全局的和它们的名字必须是唯一的。如果两个不同的@FullTextFilterDef声明两个相同名字的filter,就会抛出SearchException。每个命名的filter必须指定它自己的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;
- }
- }
BestDriversFilter是一个简单的Lucene filter实现,它把所有'score'不为5的结果过滤掉了。在这个例子中,具体的filter直接实现了org.apache.lucene.search.Filter并包含一个无参的构造器。
如果你的Filter创建需要额外的步骤或Filter需要使用有参数的构造器,那么你就要使用factory模式:
Example 5.21. Creating a filter using the factory pattern
- @Entity
- @Indexed
- @FullTextFilterDef(name = "bestDriver", impl = BestDriversFilterFactory.class)
- public class Driver { ... }
- public class BestDriversFilterFactory {
- @Factory
- public Filter getFilter() {
- //some additional steps to cache the filter results per IndexReader
- Filter bestDriversFilter = new BestDriversFilter();
- return new CachingWrapperFilter(bestDriversFilter);
- }
- }
Hibernate Search 将会查找@Factory标注的方法并使用它来生成一个filter实例。该工厂类必须有无参构造器。
有时候需要向命名的filter传递参数。例如:一个security filter可能想要知道要应用的security level。
Example 5.22. Passing parameters to a defined filter
- fullTextQuery = s.createFullTextQuery( query, Driver.class );
- fullTextQuery.enableFullTextFilter("security").setParameter( "level", 5 );
每个参数名字应该与filter或filter factory中的setter方法相关联。
Example 5.23. Using parameters in the actual filter implementation
- public class SecurityFilterFactory {
- private Integer level;
- /**
- * injected parameter
- */
- public void setLevel(Integer level) {
- this.level = level;
- }
- @Key
- public FilterKey getKey() {
- StandardFilterKey key = new StandardFilterKey();
- key.addParameter( level );
- return key;
- }
- @Factory
- public Filter getFilter() {
- Query query = new TermQuery( new Term("level", level.toString() ) );
- return new CachingWrapperFilter( new QueryWrapperFilter(query) );
- }
- }
注意带有注解@Key的方法返回的是FilterKey对象。该对象有一个特别的条约:该对象必须实现equals()/hashCode()方法来确定两个FilterKey对象是否相等。FilterKey对象作为一个键值应用在缓存机制中。
@Key注解的方法只有在下面情况下是有必要的:
- 使用了filter缓存系统(默认情况下是使用的)
- filter带有自己的参数
Value | Definition |
FilterCacheModeType.NONE | 不应用任何缓存。每次filter的调用都生成新的filter。只适用于数据经常变化的应用中 |
FilterCacheModeType.INSTANCE_ONLY | 缓存filter实例,并发地调用Filter.getDocIdSet()时重用该实例。DocIdSet不会被缓存。当filter使用自己具体的缓存机制或filter结果会动态地改变时,这就会很有用了。 |
FilterCacheModeType.INSTANCE_AND_DOCIDSETRESULTS | 缓存filter实例和DocIdSet。这是默认值。 |
- 系统并不经常更新index。(另一个意思即经常重用同一个IndexReader)
- Filter的DocIdSet的计算是非常昂贵的(相对于执行一个查询来说)
5.3.1. Using filters in a sharded environment
略
5.4. Faceting
Faceted search[http://en.wikipedia.org/wiki/Faceted_search]是一门能给搜索结果分类的技术。这个categorisation包括了每个分类匹配数的计算和进一步约束基于分类的搜索结果。Example 5.24, “Search for 'Hibernate Search' on Amazon”展示了一个faceting的例子。页面的主要部分显示了15个搜索结果。在左边显示浏览条目,并显示了Computers & Internet及其子分类的编排:Computer Science, Databases, Software, Web Development, Networking and Home Computing。对于每个子分类右边的数字代表了全部搜索结果中需要该子分类的结果的数量。这样的一个划分Computers & Internet就是一个具体的search facet。另一种例子是平均用户审查。
在Hibernate Search中,QueryBuilder与FullTextQuery是faceting API的入口。前者允许创建faceting请求,而后者提供了FacetManager的访问。有了FacetManager的帮助,faceting请求可以被应用在query上,selected facet可以被加入一个存在的query来调整搜索结果。下面的章节将更详细地描述faceting过程。下面会使用Example 5.25, “Entity Cd”中定义的实体Cd作为例子
Example 5.25. Entity Cd
- @Entity
- @Indexed
- public class Cd {
- @Id
- @GeneratedValue
- private int id;
- @Fields( {
- @Field,
- @Field(name = "name_un_analyzed", index = Index.UN_TOKENIZED)
- })
- private String name;
- @Field(index = Index.UN_TOKENIZED)
- @NumericField
- private int price;
- @Field(index = Index.UN_TOKENIZED)
- @DateBridge(resolution = Resolution.YEAR)
- private Date releaseYear;
- @Field(index = Index.UN_TOKENIZED)
- private String label;
- // setter/getter
- ...
5.4.1. Creating a faceting request
faceted search的第一步是创建一个FacetingRequest。现在支持两种类型的faceting request。第一种类型称为discrete faceting request(离散的faceting request),第二种类型是range faceting request。对于discrete faceting request来说,你需要指定哪个index field用于facet(categorize)并应用哪些faceting选项。Example 5.26, “Creating a discrete faceting request”就是一个discrete faceting request例子。
Example 5.26. Creating a discrete faceting request
- QueryBuilder builder = fullTextSession.getSearchFactory()
- .buildQueryBuilder()
- .forEntity( Cd.class )
- .get();
- FacetingRequest labelFacetingRequest = builder.facet()
- .name( "labelFaceting" )
- .onField( "label")
- .discrete()
- .orderedBy( FacetSortOrder.COUNT_DESC )
- .includeZeroCounts( false )
- .maxFacetCount( 1 )
- .createFacetingRequest();
当运行这个faceting request,将会为每个离散值(在这里是'label' field的值)创建一个Facet实例。Facet实例会记录下实际的field value,包括这个field value的值在原查询结果中出现的频率。orderedBy,includeZeroCounts和maxFacetCount是任何faceting request的可选参数。orderedBy允许指定返回的facet的顺序,默认是FacetSortOrder.COUNT_DESC,不过你也可能按field value或按指定的范围排序。includeZeroCount定义是否计数为0的facet也包含在结果中(默认是包括的)。maxFacetCount限制了最大的facet返回数。
Tip:应用faceting的indexed field需要满足一些先决条件。被索引的属性域必须是字符串,日期或数值类型。另外属性域必须以Index.UN_TOKENIZED方式索引,数值型的属性域必须标注为@NumericField
range faceting request的创建非常相似,除了我们必须为field指定一个范围值。Example 5.27,“Creating a range faceting request”是一个range faceting request的例子,它指定了三个不同的price范围。below和above只能指定一次,但你可以任意地指定from-to范围。通过excludeLimit方法定义是否包括每个范围的边界。
Example 5.27. Creating a range faceting request
- QueryBuilder builder = fullTextSession.getSearchFactory()
- .buildQueryBuilder()
- .forEntity( Cd.class )
- .get();
- FacetingRequest priceacetingRequest = queryBuilder( Cd.class ).facet()
- .name( "priceFaceting" )
- .onField( "price" )
- .range()
- .below( 1000 )
- .from( 1001 ).to( 1500 )
- .above( 1500 ).excludeLimit()
- .createFacetingRequest();
5.4.2. Applying a faceting request
在5.4.1节'creating a faceting request',我们已经看到怎么样去创建一个faceting request。现在是时候在查询时应用这个faceting request。关键在于从FulltextQuery中获取的FacetManager。(see Example 5.28, “Applying a faceting request”)
Example 5.28. Applying a faceting request
- // create a fulltext query
- QueryBuilder builder = queryBuilder( Cd.class );
- Query luceneQuery = builder.all().createQuery(); // match all query
- FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery, Cd.class );
- // retrieve facet manager and apply faceting request
- FacetManager facetManager = query.getFacetManager();
- facetManager.enableFaceting( priceFacetingRequest );
- // get the list of Cds
- List<Cd> cds = fullTextQuery.list();
- ...
- // retrieve the faceting results
- List<Facet> facets = facetManager.getFacets( "priceFaceting" );
- ...
只要你有这样的需要,你可以使用添加任何数量的faceting request,并通过getFacets()和faceting request name来获取Facet。同样地,有一个disableFaceting()方法,它可以通过request name来禁用一个faceting request。
5.4.3. 限制查询结果(Restricting query results)
最后但不重要的是,你可以应用任何返回的Facet作为你原本的query的额外的criteria,这样就可以实现一个"drill-down"功能。为了这个目的,就得利用FacetSelection这个类。可以通过FacetManager来应用FacetSelection,并允许你选择一个facet作为query criteria(selectFacet),移除一个facet restriction(deselectFacets),移除所有facet restrictions(clearSelectedFacets)并获取当前所有的selected facets(getSelectedFacets)。 Example 5.29, “Restricting query results via the application of a FacetSelection” shows an example.
Example 5.29. Restricting query results via the application of a FacetSelection
- // create a fulltext query
- QueryBuilder builder = queryBuilder( Cd.class );
- Query luceneQuery = builder.all().createQuery(); // match all query
- FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery, clazz );
- // retrieve facet manager and apply faceting request
- FacetManager facetManager = query.getFacetManager();
- facetManager.enableFaceting( priceFacetingRequest );
- // get the list of Cd
- List<Cd> cds = fullTextQuery.list();
- assertTrue(cds.size() == 10);
- // retrieve the faceting results
- List<Facet> facets = facetManager.getFacets( "priceFaceting" );
- assertTrue(facets.get(0).getCount() == 2);
- // apply first facet as additional search criteria
- facetManager.getFacetGroup( "priceFaceting" ).selectFacets( facets.get( 0 ) );
- // re-execute the query
- cds = fullTextQuery.list();assertTrue(cds.size() == 2);
5.5. Optimizing the query process
查询性能依赖于下面几个准则:
- Lucene query自身的问题。查看关于这方面更多的文献
- 加载对象的数量。使用pagination或index projection。
- Hibernate Search与Lucene reader的交互方式:定义合适的Reader strategy。
- 缓存频繁使用的从index中抽离的值。see Section 5.5.1, “Caching index values:FieldCache”
5.5.1. Caching index values: FieldCache
Lucene index的主要功能是鉴定与查询的匹配关系,然而查询完成后,必须分析结果并抽离有用的信息:典型地,Hibernate Search需要抽出Class type和primary key。
从index中抽离需要的值是一种性能消耗,这种消耗可能很低并不易让人知道,但在某些时候caching会是一种很好的实践。
缓存的精确需要依赖于使用Projection的类型(see Section 5.1.3.5,“Projection”),有些时候,Class type是不需要缓存的,因为它可以通过query上下文获知。
使用@CacheFromIndex注解,你可以试验缓存Hibernate Search所需要的不同的主元数据field。
- import static org.hibernate.search.annotations.FieldCacheType.CLASS;
- import static org.hibernate.search.annotations.FieldCacheType.ID;
- @Indexed
- @CacheFromIndex( { CLASS, ID } )
- public class Essay {
- ...
通过这个注解现在就可以缓存Class type和ID。
- CLASS:Hibernate Search将会使用Lucene的FieldCache来改善从index抽离Class type的性能。默认下这个值是可用的,Hibernate Search将应该这个值如果你没有指定@CacheFromIndex注解。
- ID:缓存主键标识符。这好像能提供最好的查询表现,但同时也会消耗更多的内存(有可能会降低性能)
Note:在warmup(运行一些query)后,测量性能和内存消耗之间的影响:使用Field Cache好像能改善性能,但并不总是这样的。
使用FieldCache有两个缺点:
- 内存使用:缓存会消耗大量的内存。典型的CLASS缓存比ID缓存要求更低。
- Index warmup:当使用了field cache,第一次查询会比不用缓存慢。
the id, and have ids of the same type (this is evaluated at each Query execution).