Faceted search

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.

Discrete facets

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:

Discrete facets

Discrete faceting request flow (dotted gray arrows represent optional paths)

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.

Range facets

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:

Range facets

Range faceting request flow (dotted gray arrows represent optional paths)

You can define a range as being above, below, or between two values (that is, fromto). 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.

Range facets
..................Content has been hidden....................

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