Lucene filters are a powerful tool for narrowing the scope of a query to some particular subset. However, filters work on predefined subsets. You must already know what it is that you are seeking.
Sometimes you need to dynamically identify subsets. For example, let's give our App
entity a category
property representing its genre:
... @Column @Field private String category; ...
When we perform a keyword search for apps, we might want to know which categories are represented in the results and how many results fall under each category. We might also want to know which price ranges were found. All of this information can help guide users in narrowing their queries more effectively.
The process of dynamically identifying dimensions and then filtering by them is called faceted search. The Hibernate Search query DSL has a flow for this, starting with a QueryBuilder
object's facet
method:
The name
method takes some descriptive identifier for this facet (for example, categoryFacet
), so that it can be referenced by queries later. The familiar onField
clause declares the field by which to group results (for example, category
).
The discrete
clause indicates that we are grouping by single values, as opposed to ranges of values. We'll explore range facets in the next section.
The createFacetingRequest
method completes this process and returns a FacetingRequest
object. However, there are three optional methods that you can call first, in any combination:
includeZeroCounts
: It causes Hibernate Search to return all possible facets, even those which do not have any hits in the current search results. By default, facets with no hits are quietly ignored.maxFacetCount
: It limits the number of facets to be returned.orderedBy
: It specifies the sort order of the facets found. The three options relevant to discrete facets are:COUNT_ASC
: Facets are sorted in an ascending order by the number of associated search results. The facets with the lowest number of hits are listed first.COUNT_DESC
: This is the exact opposite of COUNT_ASC
. Facets are listed from the highest hit count to the lowest.FIELD_VALUE
: Facets are sorted in an alphabetical order by the value of the relevant field. For example, the "business" category would come before the "games" category.This chapter's version of the VAPORware Marketplace now includes the following code for setting up a faceted search on the app
category:
... // Create a faceting request FacetingRequestcategoryFacetingRequest = queryBuilder .facet() .name("categoryFacet") .onField("category") .discrete() .orderedBy(FacetSortOrder.FIELD_VALUE) .includeZeroCounts(false) .createFacetingRequest(); // Enable it for the FullTextQuery object hibernateQuery.getFacetManager().enableFaceting( categoryFacetingRequest); ...
Now that the faceting request is enabled, we can run the search query and retrieve the facet information using the categoryFacet
name that we just declared:
...
List<App> apps = hibernateQuery.list();
List<Facet> categoryFacets =
hibernateQuery.getFacetManager().getFacets("categoryFacet");
...
The Facet
class includes a
getValue
method, which returns the value of the field for a particular group. For example, if some of the matching apps are in the "business" category, then one of the facets will have the string "business" as its value. The getCount
method reports how many search results are associated with that facet.
Using these two methods, our search servlet can iterate through all of the category facets, and build a collection to be used for display in the search results JSP:
... Map<String, Integer> categories = new TreeMap<String, Integer>(); for(Facet categoryFacet : categoryFacets) { // Build a collection of categories, and the hit count for each categories.put( categoryFacet.getValue(),categoryFacet.getCount()); // If this one is the *selected* category, then re-run the query // with this facet to narrow the results if(categoryFacet.getValue().equalsIgnoreCase(selectedCategory)) { hibernateQuery.getFacetManager() .getFacetGroup("categoryFacet").selectFacets(categoryFacet); apps = hibernateQuery.list(); } } ...
If the search servlet receives a request with a selectedCategory
CGI parameter, then the user chooses to narrow results to a specific category. So if this string matches the value of a facet being iterated, then that facet is "selected" for the FullTextQuery
object. The query can then be re-run, and it will then return only apps belonging to that category.
Facets are not limited to single discrete values. A facet may also be created from a range of values. For example, we might want to group apps by a price range—search results priced below one dollar, between one and five dollars, or above five dollars.
The Hibernate Search DSL for range faceting takes the elements of the discrete faceting flow and combines them with elements from the range query that we saw in Chapter 3, Performing Queries:
You can define a range as being above, below, or between two values (that is, from
– to
). These options may be used in combination to define as many range subsets as you wish.
As with regular range queries, the optional excludeLimit
method exclude its boundary value from the range. In other words, above(5)
means "greater than or equal to 5", whereas above(5).excludeLimit()
means "greater than 5, period".
The optional includeZeroCounts
, maxFacetCount
, and
orderBy
methods operate in the same manner as with discrete faceting. However, range faceting offers an extra choice for sorting order. FacetSortOrder.RANGE_DEFINITION_ODER
causes facets to be returned in the order they were defined (note that the "r
" is missing in "oder
").
Along the discrete faceting request for category
, the example code for this chapter also includes the following code snippet to enable range faceting for price
:
... FacetingRequestpriceRangeFacetingRequest = queryBuilder .facet() .name("priceRangeFacet") .onField("price") .range() .below(1f).excludeLimit() .from(1f).to(5f) .above(5f).excludeLimit() .createFacetingRequest(); hibernateQuery.getFacetManager().enableFaceting( priceRangeFacetingRequest); ...
If you take a look at the source code for search.jsp
, it now includes both the category and price range facets found during each search. These two faceting types may be used in combination to narrow the search results, with the currently-selected facets highlighted in bold. When all is selected for either type, that particular facet is removed and the search results widen again.