Chapter 5. Advanced Querying

In this chapter, we will elaborate on the basic search query concepts that we covered earlier, in light of the new mapping knowledge that we just picked up. We will now look at a number of techniques for making search queries more flexible and powerful.

We will see how to dynamically filter results at the Lucene level, before the database is even touched. We will also avoid database calls by using projection-based queries, to retrieve properties directly from Lucene. We will use faceted search, to recognize and isolate subsets of data within search results. Finally, we will cover some miscellaneous query tools, such as query-time boosting and placing time limits on a query.

Filtering

The process of building a query revolves around finding matches. However, sometimes you want to narrow the search results on the basis of a criteria that explicitly did not match. For example, let's say we want to limit our VAPORware Marketplace search to only those apps that are supported on a particular device:

  • Adding a keyword or phrase to an existing query doesn't help, because that would just make the query more inclusive.
  • We could turn the existing query into a boolean query, with an extra must clause, but then the DSL starts to become harder to maintain. Also, if you need to use complex logic to narrow your results, then the DSL may not offer enough flexibility.
  • A Hibernate Search FullTextQuery object inherits from the Hibernate ORM Query (or its JPA counterpart) class. So, we can narrow results using core Hibernate tools like ResultTransformer. However, this requires making additional database calls, which can impact performance.

Hibernate Search offers a more elegant and efficient filter approach. Through this mechanism, filter logic for various scenarios is encapsulated in separate classes. Those filter classes may be dynamically enabled or disabled at runtime, alone or in any combination. When a query is filtered, unwanted results are never fetched from Lucene in the first place. This reduces the weight of any follow-up database access.

Creating a filter factory

To filter our search results by supported devices, the first step is creating a class to store the filtering logic. This should be an instance of org.apache.lucene.search.Filter. For simple hardcoded logic, you might just create your own subclass.

However, if we instead generate filters dynamically with a filter factory, then we can accept parameters (for example, device name) and customize the filter at runtime:

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();
   }

}

The @Factory annotation is applied to the method responsible for producing the Lucene filter object. In this case, we annotate the aptly named getFilter method.

Note

Unfortunately, building a Lucene Filter object requires us to work more closely with the raw Lucene API, rather the convenient DSL wrapper provided by Hibernate Search. The full Lucene API is very involved, and covering it completely would require an entirely separate book. However, even this shallow dive is deep enough to give us the tools for writing really useful filters.

This example builds a filter by wrapping a Lucene query, and then applying a second wrapper to facilitate filter caching. A specific type of query used is org.apache.lucene.search.PhraseQuery, which is equivalent to the DSL phrase query that we explored in Chapter 3, Performing Queries.

Tip

We are examining the phrase query in this example, because it is one of the most useful types for a building a filter. However, there are 15 Lucene query types in total. You can explore the JavaDocs at http://lucene.apache.org/core/old_versioned_docs/versions/3_0_3/api/all/org/apache/lucene/search/Query.html.

Let's review some of the things we know about how data is stored in a Lucene index. By default, an analyzer tokenizes strings, and indexes them as individual terms. The default analyzer also converts the string data into lowercase. The Hibernate Search DSL normally hides all of this detail, so developers don't have to think about it.

However, you do need to account for these things when using the Lucene API directly. Therefore, our setDeviceName setter method manually converts the deviceName property to lower case, to avoid a mismatch with Lucene. The getFilter method then manually tokenizes this property into separate terms, likewise to match what Lucene has indexed.

Each tokenized term is used to construct a Lucene Term object, which consists of the data and the relevant field name (that is, supportedDevices.name in this case). These terms are added to the PhraseQuery object one by one, in the exact order that they appear in the phrase. The query object is then wrapped up as a filter and returned.

Adding a filter key

By default, Hibernate Search caches filter instances for better performance. Therefore, each instance requires that a unique key be referenced by in the cache. In this example, the most logical key would be the device name for which each instance is filtering.

First, we add a new method to our filter factory, annotated with @Key to indicate that it is responsible for generating the unique key. This method returns a subclass of FilterKey:

...
@Key
Public FilterKey getKey() {
   DeviceFilterKey key = new DeviceFilterKey();
   key.setDeviceName(this.deviceName);
   return key;
}
...

Custom FilterKey subclasses must implement the methods equals and hashCode. Typically, when the actual wrapped data may be expressed as a string, you can delegate to the corresponding methods on the String class:

public class DeviceFilterKey extends FilterKey {

   private String deviceName;

   @Override
   public boolean equals(Object otherKey) {
      if(this.deviceName == null
           || !(otherKey instanceof DeviceFilterKey)) {
         return false;
      }
      DeviceFilterKeyotherDeviceFilterKey =
           (DeviceFilterKey) otherKey;
      return otherDeviceFilterKey.deviceName != null
              && this.deviceName.equals(otherDeviceFilterKey.deviceName);
   }

   @Override
   public int hashCode() {
      if(this.deviceName == null) {
         return 0;
      }
      return this.deviceName.hashCode();
   }

   // GETTER AND SETTER FOR deviceName...
}

Establishing a filter definition

To make this filter available for our app searches, we will create a filter definition in the App entity class:

...
@FullTextFilterDefs({
   @FullTextFilterDef(
      name="deviceName", impl=DeviceFilterFactory.class
   )
})
public class App {
...

The @FullTextFilterDef annotation links the entity class with a given filter or filter-factory class, specified by the impl element. The name element is a string by which Hibernate Search queries can reference the filter, as we'll see in the next subsection.

An entity class may have any number of defined filters. The plural @FullTextFilterDefs annotation supports this, by wrapping a comma-separated list of one or more singular @FullTextFilterDef annotations.

Enabling the filter for a query

Last but not least, we enable the filter definition for a Hibernate Search query, using the FullTextQuery object's enableFullTextFilter method:

...
if(selectedDevice != null && !selectedDevice.equals("all")) {
   hibernateQuery.enableFullTextFilter("deviceName")
      .setParameter("deviceName", selectedDevice);
}
...

This method's string parameter is matched to a filter definition on one of the entity classes involved in the query. In this case, it's the deviceName filter defined on App. When Hibernate Search finds this match, it will automatically invoke the corresponding filter factory to get a Filter object.

Our filter factory uses a parameter, also called deviceName for consistency (although it's a different variable). Before Hibernate Search can invoke the factory method, this parameter must be set, by passing the parameter name and value to setParameter.

The filter is enabled within an if block, so that we can skip this when no device was selected (that is, the All Devices option). If you examine the downloadable code bundle for this chapter's version of the VAPORware Marketplace application, you will see that the HTML file has been modified to add a drop-down menu for device selection:

Enabling the filter for a query
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset