In today’s tutorial we’re exploring the world of faceted searches like the one we’re used to see when we’re searching for an item on Amazon.com or other websites. We’re using Hibernate Search here that offers an API to perform discrete as well as range faceted searches on our persisted data.

Maven Dependencies Needed

For simplicity’s sake am I going to use an HSQL database for persistence, in addition the dependencies for hibernate-entitymanager and hibernate-search (of course) should be added to your pom.xml

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-search</artifactId>
        <version>4.0.0.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.0.1.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hsqldb</groupId>
        <artifactId>hsqldb</artifactId>
        <version>2.2.8</version>
    </dependency>
</dependencies>

Facet Search Examples

First we need something to search for so we’re creating a book bean to contain some searchable information and afterwards we’re creating and storing a bunch of books to perform a facet search on ..

The Book Bean

This is what our book bean looks like .. it is important to know that the current implementation of hibernate search has its limitations when it comes to facet searches.

The first one is that the field that is used for the facet search must not be analyzed .. so we’re using Analyze.NO here. The second limitation is that a range facet search only works with a field that holds an integer value. If this is going to be changed or has already changed with another version and you know it, please keep me updated.

package com.hascode.tutorial;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;

@Entity
@Indexed(index = "index/books")
public class Book {
	private Long id;
	private String title;
	private String author;
	private String category;
	private int price;

	@Id
	@GeneratedValue
	public Long getId() {
		return id;
	}

	@Field(name = "title", analyze = Analyze.YES, store = Store.YES)
	public String getTitle() {
		return title;
	}

	public void setTitle(final String title) {
		this.title = title;
	}

	public void setId(final Long id) {
		this.id = id;
	}

	public void setAuthor(final String author) {
		this.author = author;
	}

	@Field(analyze = Analyze.NO, store = Store.YES)
	public String getAuthor() {
		return author;
	}

	@Field(analyze = Analyze.NO, store = Store.YES)
	public String getCategory() {
		return category;
	}

	public void setCategory(final String category) {
		this.category = category;
	}

	@Field(analyze = Analyze.NO, store = Store.YES)
	public int getPrice() {
		return price;
	}

	public void setPrice(final int price) {
		this.price = price;
	}

}

Bootstrapping

Now we should create a bunch of book entities .. the following bootstrap class will be called by the following two search implementations..

package com.hascode.tutorial;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;

public class BookSetup {
	public static void createBooks(final EntityManager em,
			final EntityTransaction tx) {
		tx.begin();
		Book book1 = new Book();
		book1.setTitle("The big book of nothing");
		book1.setCategory("Adventure");
		book1.setAuthor("fred");
		book1.setPrice(12);

		Book book2 = new Book();
		book2.setTitle("Exciting stories I");
		book2.setCategory("Horror");
		book2.setAuthor("selma");
		book2.setPrice(22);

		Book book3 = new Book();
		book3.setTitle("My life");
		book3.setCategory("Horror");
		book3.setAuthor("fred");
		book3.setPrice(10);

		Book book4 = new Book();
		book4.setTitle("Some science book");
		book4.setCategory("Science");
		book4.setAuthor("tim");
		book4.setPrice(35);

		Book book5 = new Book();
		book5.setTitle("The universe and stuff");
		book5.setCategory("Science");
		book5.setAuthor("charlize");
		book5.setPrice(65);

		Book book6 = new Book();
		book6.setTitle("Indiana Bones XII");
		book6.setCategory("Adventure");
		book6.setAuthor("charles");
		book6.setPrice(14);

		Book book7 = new Book();
		book7.setTitle("A day at work without coffee");
		book7.setCategory("Horror");
		book7.setAuthor("me");
		book7.setPrice(25);

		Book book8 = new Book();
		book8.setTitle("Horror Pirate Cyber Ninjas from Hell III");
		book8.setCategory("Cartoon");
		book8.setAuthor("peter");
		book8.setPrice(51);

		em.persist(book1);
		em.persist(book2);
		em.persist(book3);
		em.persist(book4);
		em.persist(book5);
		em.persist(book6);
		em.persist(book7);
		em.persist(book8);
		tx.commit();
	}
}

A discrete facet search allows us to group the queries e.g by a given category – a real world example would be a typical search on Amazon.com

amazon discrete query
Figure 1. Amazon.com - Group by category

In this example we’re grouping by the book’s category..

package com.hascode.tutorial;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import org.apache.lucene.search.Query;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.engine.spi.FacetManager;
import org.hibernate.search.query.facet.Facet;
import org.hibernate.search.query.facet.FacetSortOrder;
import org.hibernate.search.query.facet.FacetingRequest;

public class DiscreteFacetingSearch {
	public static void main(final String[] args) {
		final EntityManagerFactory emf = Persistence
				.createEntityManagerFactory("hascode-local");
		final EntityManager em = emf.createEntityManager();
		final EntityTransaction tx = em.getTransaction();

		BookSetup.createBooks(em, tx);

		FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search
				.getFullTextEntityManager(em);
		QueryBuilder builder = fullTextEntityManager.getSearchFactory()
				.buildQueryBuilder().forEntity(Book.class).get();

		FacetingRequest categoryFacetingRequest = builder.facet()
				.name("categoryFaceting").onField("category").discrete()
				.orderedBy(FacetSortOrder.COUNT_DESC).includeZeroCounts(false)
				.createFacetingRequest();

		Query luceneQuery = builder.all().createQuery();
		org.hibernate.search.jpa.FullTextQuery fullTextQuery = fullTextEntityManager
				.createFullTextQuery(luceneQuery);
		FacetManager facetManager = fullTextQuery.getFacetManager();
		facetManager.enableFaceting(categoryFacetingRequest);

		List<Facet> facets = facetManager.getFacets("categoryFaceting");
		for (Facet f : facets) {
			System.out.println(f.getValue() + " (" + f.getCount() + ")");
			List<Book> books = fullTextEntityManager.createFullTextQuery(
					f.getFacetQuery()).getResultList();
			for (final Book b : books) {
				System.out.println("\t" + b.getTitle() + " (" + b.getAuthor()
						+ ")");
			}
		}
		em.close();
		emf.close();
	}
}

Running the code above should give the following output:

Horror (3)
	Exciting stories I (selma)
	My life (fred)
	A day at work without coffee (me)
Adventure (2)
	The big book of nothing (fred)
	Indiana Bones XII (charles)
Science (2)
	Some science book (tim)
	The universe and stuff (charlize)
Cartoon (1)
	Horror Pirate Cyber Ninjas from Hell III (peter)

Now we’re grouping the search results by several price ranges that we’ve defined..

package com.hascode.tutorial;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import org.apache.lucene.search.Query;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.engine.spi.FacetManager;
import org.hibernate.search.query.facet.Facet;
import org.hibernate.search.query.facet.FacetSortOrder;
import org.hibernate.search.query.facet.FacetingRequest;

public class RangeFacetingSearch {
	public static void main(final String[] args) {
		final EntityManagerFactory emf = Persistence
				.createEntityManagerFactory("hascode-local");
		final EntityManager em = emf.createEntityManager();
		final EntityTransaction tx = em.getTransaction();

		BookSetup.createBooks(em, tx);

		FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search
				.getFullTextEntityManager(em);
		QueryBuilder builder = fullTextEntityManager.getSearchFactory()
				.buildQueryBuilder().forEntity(Book.class).get();

		FacetingRequest priceFacetingRequest = builder.facet()
				.name("priceFaceting").onField("price").range().below(20)
				.from(30).to(40).from(40).to(50).above(50).excludeLimit()
				.orderedBy(FacetSortOrder.RANGE_DEFINITION_ODER)
				.createFacetingRequest();
		Query luceneQuery = builder.all().createQuery();
		org.hibernate.search.jpa.FullTextQuery fullTextQuery = fullTextEntityManager
				.createFullTextQuery(luceneQuery);
		FacetManager facetManager = fullTextQuery.getFacetManager();
		facetManager.enableFaceting(priceFacetingRequest);

		List<Facet> facets = facetManager.getFacets("priceFaceting");
		for (Facet f : facets) {
			System.out.println("Price range: " + f.getValue() + " ("
					+ f.getCount() + ")");
		}
		em.close();
		emf.close();
	}
}

Running the example above should produce a similar output to this:

Price range: [, 20] (3)
Price range: [30, 40] (1)
Price range: [40, 50] (0)
Price range: (50, ] (2)

Tutorial Sources

I have put the source from this tutorial on my GitHub repository – download it there or check it out using Git:

git clone https://github.com/hascode/hibernate-search-faceting-samples.git