Chapter 5. Querying
The second most important capability of Hibernate Search is the ability to execute a Lucene query and retrieve entities managed by an Hibernate session, providing the power of Lucene without leaving the Hibernate paradigm, and giving another dimension to the Hibernate classic search mechanisms (HQL, Criteria query, native SQL query). Preparing and executing a query consists of four simple steps:
- Creating a
FullTextSession
- Creating a Lucene query
- Wrapping the Lucene query using a
org.hibernate.Query
- Executing the search by calling for example
list()
orscroll()
To access the querying facilities, you have to use an
FullTextSession
. This Search specific session wraps a regular org.hibernate.Session
to provide query and indexing capabilities.
Example 5.1. Creating a FullTextSession
Session session = sessionFactory.openSession(); ... FullTextSession fullTextSession = Search.getFullTextSession(session);
The actual search facility is built on native Lucene queries which the following example illustrates.
Example 5.2. Creating a Lucene query
org.apache.lucene.queryParser.QueryParser parser =
new QueryParser("title", new StopAnalyzer() );
org.apache.lucene.search.Query luceneQuery = parser.parse( "summary:Festina Or brand:Seiko" );
org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery );
List result = fullTextQuery.list(); //return a list of managed objects
The Hibernate query built on top of the Lucene query is a regular
org.hibernate.Query
, which means you are in the same paradigm as the other Hibernate query facilities (HQL, Native or Criteria). The regular list()
, uniqueResult()
, iterate()
and scroll()
methods can be used.
In case you are using the Java Persistence APIs of Hibernate (aka EJB 3.0 Persistence), the same extensions exist:
Example 5.3. Creating a Search query using the JPA API
EntityManager em = entityManagerFactory.createEntityManager();
FullTextEntityManager fullTextEntityManager =
org.hibernate.hibernate.search.jpa.Search.getFullTextEntityManager(em);
...
org.apache.lucene.queryParser.QueryParser parser =
new QueryParser("title", new StopAnalyzer() );
org.apache.lucene.search.Query luceneQuery = parser.parse( "summary:Festina Or brand:Seiko" );
javax.persistence.Query fullTextQuery = fullTextEntityManager.createFullTextQuery( luceneQuery );
List result = fullTextQuery.getResultList(); //return a list of managed objects
The following examples we will use the Hibernate APIs but the same example can be easily rewritten with the Java Persistence API by just adjusting the way the
FullTextQuery
is retrieved.
5.1. Building queries
Hibernate Search queries are built on top of Lucene queries which gives you total freedom on the type of Lucene query you want to execute. However, once built, Hibernate Search wraps further query processing using
org.hibernate.Query
as your primary query manipulation API.
5.1.1. Building a Lucene query
It is out of the scope of this documentation on how to exactly build a Lucene query. Please refer to the online Lucene documentation or get hold of a copy of either Lucene In Action or Hibernate Search in Action.
5.1.2. Building a Hibernate Search query
5.1.2.1. Generality
Once the Lucene query is built, it needs to be wrapped into an Hibernate Query.
Example 5.4. Wrapping a Lucene query into a Hibernate Query
FullTextSession fullTextSession = Search.getFullTextSession( session ); org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery );
If not specified otherwise, the query will be executed against all indexed entities, potentially returning all types of indexed classes. It is advised, from a performance point of view, to restrict the returned types:
Example 5.5. Filtering the search result by entity type
org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery, Customer.class ); // or fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery, Item.class, Actor.class );
The first example returns only matching
Customer
s, the second returns matching Actor
s and Item
s. The type restriction is fully polymorphic which means that if there are two indexed subclasses Salesman
and Customer
of the baseclass Person
, it is possible to just specify Person.class
in order to filter on result types.
5.1.2.2. Pagination
Out of performance reasons it is recommended to restrict the number of returned objects per query. In fact is a very common use case anyway that the user navigates from one page to an other. The way to define pagination is exactly the way you would define pagination in a plain HQL or 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
Note
It is still possible to get the total number of matching elements regardless of the pagination via
fulltextQuery.
getResultSize()
5.1.2.3. Sorting
Apache Lucene provides a very flexible and powerful way to sort results. While the default sorting (by relevance) is appropriate most of the time, it can be interesting to sort by one or several other properties. In order to do so set the Lucene Sort object to apply a 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"));
query.setSort(sort);
List results = query.list();
One can notice the
FullTextQuery
interface which is a sub interface of org.hibernate.Query
. Be aware that fields used for sorting must not be tokenized.
5.1.2.4. Fetching strategy
When you restrict the return types to one class, Hibernate Search loads the objects using a single query. It also respects the static fetching strategy defined in your domain model.
It is often useful, however, to refine the fetching strategy for a specific use case.
Example 5.8. Specifying FetchMode
on a query
Criteria criteria = s.createCriteria( Book.class ).setFetchMode( "authors", FetchMode.JOIN ); s.createFullTextQuery( luceneQuery ).setCriteriaQuery( criteria );
In this example, the query will return all Books matching the luceneQuery. The authors collection will be loaded from the same query using an SQL outer join.
When defining a criteria query, it is not needed to restrict the entity types returned while creating the Hibernate Search query from the full text session: the type is guessed from the criteria query itself. Only fetch mode can be adjusted, refrain from applying any other restriction.
One cannot use
setCriteriaQuery
if more than one entity type is expected to be returned.
5.1.2.5. Projection
For some use cases, returning the domain object (graph) is overkill. Only a small subset of the properties is necessary. Hibernate Search allows you to return a subset of properties:
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 = (Integer) firstResult[0];
String summary = (String) firstResult[1];
String body = (String) firstResult[2];
String authorName = (String) firstResult[3];
Hibernate Search extracts the properties from the Lucene index and convert them back to their object representation, returning a list of
Object[]
. Projections avoid a potential database round trip (useful if the query response time is critical), but has some constraints:
- the properties projected must be stored in the index (
@Field(store=Store.YES)
), which increase the index size - the properties projected must use a
FieldBridge
implementingorg.hibernate.search.bridge.TwoWayFieldBridge
ororg.hibernate.search.bridge.TwoWayStringBridge
, the latter being the simpler version. All Hibernate Search built-in types are two-way. - you can only project simple properties of the indexed entity or its embedded associations. This means you cannot project a whole embedded entity.
- projection does not work on collections or maps which are indexed via
@IndexedEmbedded
Projection is useful for another kind of usecases. Lucene provides some metadata information to the user about the results. By using some special placeholders, the projection mechanism can retrieve them:
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 = (Float) firstResult[0];
Book book = (Book) firstResult[1];
String authorName = (String) firstResult[2];
You can mix and match regular fields and special placeholders. Here is the list of available placeholders:
- FullTextQuery.THIS: returns the initialized and managed entity (as a non projected query would have done).
- FullTextQuery.DOCUMENT: returns the Lucene Document related to the object projected.
- FullTextQuery.OBJECT_CLASS: returns the class of the indexed entity.
- FullTextQuery.SCORE: returns the document score in the query. Scores are handy to compare one result against an other for a given query but are useless when comparing the result of different queries.
- FullTextQuery.ID: the id property value of the projected object.
- FullTextQuery.DOCUMENT_ID: the Lucene document id. Careful, Lucene document id can change overtime between two different IndexReader opening (this feature is experimental).
- FullTextQuery.EXPLANATION: returns the Lucene Explanation object for the matching object/document in the given query. Do not use if you retrieve a lot of data. Running explanation typically is as costly as running the whole Lucene query per matching element. Make sure you use projection!