CLOVER🍀

That was when it all began.

Hibernate Searchでファセットを使う - 単一Entity編

検索でよく使われそうな機能の中に、ファセットというものがあります。

ファセットがどういうものかは、見た方が早いですね。下記は、Amazonで「カテゴリ」に「本」を選び、キーワード「java」で検索した時に左ナビに表示されるものです。

また、こういう範囲っぽいものもあります。

このように、特定のグルーピングに対して、それに対する件数を取得するような機能ですね。

Hibernate Searchでもこの機能が使えるようなので、試してみましょう。

Faceting
http://docs.jboss.org/hibernate/search/5.0/reference/en-US/html_single/#query-faceting

準備

まずは、Maven依存関係。

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.3.8.Final</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-search-orm</artifactId>
      <version>5.0.1.Final</version>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-analyzers-kuromoji</artifactId>
      <version>4.10.3</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.34</version>
      <scope>runtime</scope>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>1.7.1</version>
      <scope>test</scope>
    </dependency>

テストの依存関係は、サンプルの都合上で。データベースは、MySQLです。

DDL。

CREATE TABLE book(
    id INT AUTO_INCREMENT,
    title VARCHAR(255),
    price INT,
    tag VARCHAR(255),
    PRIMARY KEY(id) 
);

というわけで、ここでは本を題材にして、「price」カラムおよび「tag」というカラムでファセットを作ることを考えます。

また、persistence.xmlはこのようにしました。
src/test/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
  <persistence-unit name="search.pu" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>org.littlewings.hibernate.facet.Book</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/practice?useUnicode=true&amp;characterEncoding=utf-8&amp;characterSetResults=utf-8&amp;useServerPrepStmts=true&amp;useLocalSessionState=true&amp;elideSetAutoCommits=true&amp;alwaysSendSetIsolation=false" />
      <property name="javax.persistence.jdbc.user" value="kazuhira" />
      <property name="javax.persistence.jdbc.password" value="password" />

      <!-- Hibernate -->
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
      <property name="hibernate.show_sql" value="true" />
      <property name="hibernate.format_sql" value="true" />

      <!-- Hibernate Search -->
      <property name="hibernate.search.default.directory_provider" value="ram" />
      <property name="hibernate.search.analyzer" value="org.apache.lucene.analysis.ja.JapaneseAnalyzer" />
      <property name="hibernate.search.lucene_version" value="LUCENE_4_10_3" />
    </properties>
  </persistence-unit>
</persistence>

デフォルトのAnalyzerは、JapaneseAnalyzerで形態素解析です。Entityの定義は、この次に。

Entityクラス

用意したJPAのEntityは、このようにしました。
src/main/java/org/littlewings/hibernate/facet/Book.java

package org.littlewings.hibernate.facet;

import javax.persistence.*;

import org.hibernate.search.annotations.*;

@Entity
@Table(name = "book")
@Indexed
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    @Field
    private String title;

    @Column
    @Field
    private Integer price;

    @Column
    @Fields({
            @Field,
            @Field(name = "tag_facet", analyze = Analyze.NO)
        })
    private String tag;

    public Book() { }

    public Book(String title, Integer price, String tag) {
        this.title = title;
        this.price = price;
        this.tag = tag;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public Integer getPrice() { return price; }
    public void setPrice(Integer price) { this.price = price; }
    public String getTag() { return tag; }
    public void setTag(String tag) { this.tag = tag; }
}

ポイントは、tagフィールドに設定している@Fieldsおよび@Fieldアノテーションですね。

    @Column
    @Fields({
            @Field,
            @Field(name = "tag_facet", analyze = Analyze.NO)
        })
    private String tag;

なぜ@Fieldの定義が2つあるかというところですが、ファセットで利用する@Fieldの部分は、Analyzerで解析する対象としてはいけません。よって、「analye = Analyze.NO」を指定します。もうひとつ@Fieldがあるのは、通常の検索用です。また、ファセット用のフィールドは「tag_facet」としています。

極端な話、このフィールドをファセットでしか使用しないというのであれば、以下のようにしてもかまいません。

    @Column
    @Field(analyze = Analyze.NO)
    private String tag;

このあたりは、用途に応じて…。

なお、priceフィールドは、数値型なのでAnalyzerでの解析対象となりません。

それでは、このEntityを利用してファセット機能を試してみましょう。

Hibernate Searchで使うファセットには、Discrete FacetとRange Facetがあるようなので、それらを順次テストコードと一緒に見ていきます。

テストコードの雛形は、このように。
src/test/java/org/littlewings/hibernate/facet/FacetTest.java

package org.littlewings.hibernate.facet;

import static org.assertj.core.api.Assertions.*;

import java.util.*;
import javax.persistence.*;

import org.hibernate.search.jpa.*;
import org.hibernate.search.query.dsl.*;
import org.hibernate.search.query.facet.*;
import org.hibernate.search.query.engine.spi.*;

import org.junit.*;

public class FacetTest {
    private static EntityManager createEntityManager() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("search.pu");
        return emf.createEntityManager();
    }

    @After
    public void tearDown() {
        EntityManager em = createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        em.createQuery("DELETE FROM Book").executeUpdate();

        tx.commit();
    }

    private void addBooks(EntityManager em, List<Book> books) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        books.stream().forEach(em::persist);

        tx.commit();
    }

    // ここに、テストコードを書く
}

今回のコードは、ファセット機能の特性上(?)、EntityManagerを都度生成する形としました。ここは、後で少し補足します。

テストの大半で利用するデータは、このようなものとします。

    private List<Book> createBooks() {
        return Arrays.asList(new Book("はじめてのSpring Boot 「Spring Framework」で簡単Javaアプリ開発",
                                      2700,
                                      "Spring"),
                             new Book("高速スケーラブル検索エンジン ElasticSearch Server",
                                      3024,
                                      "全文検索"),
                             new Book("[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン",
                                      3888,
                                      "全文検索"),
                             new Book("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
                                      3200,
                                      "全文検索"),
                             new Book("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!]",
                                      2138,
                                      "Java"),
                             new Book("改訂2版 パーフェクトJava",
                                      3486,
                                      "Java"));
    }

ここでの価格とタグ(JavaとかSpringとか全文検索とか)を、ファセットで使用します。

Discrete Facet

まずは、Discrete Facetから。Discreteは、不連続のとか、別個のとかいう意味みたいです。ですので、ここではtagフィールドを使用してファセットを作ります。

先ほどのAmazonの例だと、こちらになります。

基本的な使い方

Hibernte Searchとはあまり関係ないですが、最初にEntityManagerを取得してデータを登録します。

        EntityManager em = createEntityManager();
        addBooks(em, createBooks());

続いて、Hibernate SearchのFullTextEntityManagerとQueryBuilderを取得します。

        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

取得したQueryBuilderを使用して、ファセット用のリクエストを組み立てます。

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("tagFaceting")
            .onField("tag_facet")
            .discrete()
            .orderedBy(FacetSortOrder.COUNT_DESC)
            .includeZeroCounts(false)
            .createFacetingRequest();

以下、説明。

  • QueryBuilderに対して、facetメソッドを呼び出すことで、ファセットリクエストの開始
  • nameメソッドでは任意の名前を指定。ファセットの結果を取得する時に、この値が再度必要となる
  • onFieldでは、ファセットの対象となるフィールドを指定する。今回は、「tag_facet」
  • discreteメソッドを呼び出すことで、Discrete Facetを使うことが決定
  • orderedByは、ファセットの並び順をFacetSortOrder列挙型で指定する。Discrete Facetの場合、ファセット内の件数の昇順、降順、ファセットフィールドの順でソート可能
  • includeZeroCountsは、件数がゼロのファセットを含めるかどうか。デフォルトはtrueで、「含める」みたい
  • 最後に、createFacetingRequestでファセットリクエストの作成

Luceneのクエリおよび、Hibernate Searchのクエリの作成。ここでは、全件取得のクエリとします。

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.all().createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

ここで、FullTextQueryからFacetManagerを取得し、先ほど作成したFacetingRequestを設定します。

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

で、普通に検索します。

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .hasSize(6);

全件取得なので、6件です。

ファセットは、別途取得します。この時に、ファセットリクエストを作成した時のnameの値が必要です。

        List<Facet> facets = facetManager.getFacets("tagFaceting");

        assertThat(facets)
            .hasSize(3);
        assertThat(facets.get(0).getValue())
            .isEqualTo("全文検索");
        assertThat(facets.get(0).getCount())
            .isEqualTo(3);
        assertThat(facets.get(1).getValue())
            .isEqualTo("Java");
        assertThat(facets.get(1).getCount())
            .isEqualTo(2);
        assertThat(facets.get(2).getValue())
            .isEqualTo("Spring");
        assertThat(facets.get(2).getCount())
            .isEqualTo(1);

今回は、ソート順は降順です。ファセットっぽい結果が得られていますね?

もう少し例を

全件取得ではなくて、普通のクエリと一緒に使用。

        EntityManager em = createEntityManager();
        addBooks(em, createBooks());

        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("tagFaceting")
            .onField("tag_facet")
            .discrete()
            .orderedBy(FacetSortOrder.COUNT_DESC)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.keyword().onField("title").matching("Apache").createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .hasSize(2);

        List<Facet> facets = facetManager.getFacets("tagFaceting");

        assertThat(facets)
            .hasSize(1);
        assertThat(facets.get(0).getValue())
            .isEqualTo("全文検索");
        assertThat(facets.get(0).getCount())
            .isEqualTo(2);

ファセットの結果も変化しましたね?

検索結果0件のクエリに対して、実行。

        EntityManager em = createEntityManager();
        addBooks(em, createBooks());

        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("tagFaceting")
            .onField("tag_facet")
            .discrete()
            .orderedBy(FacetSortOrder.COUNT_DESC)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.keyword().onField("title").matching("Groovy").createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .isEmpty();

        List<Facet> facets = facetManager.getFacets("tagFaceting");

        assertThat(facets)
            .isEmpty();

includeZeroCountsをfalseにしているので、ファセットも空。

ファセットのカラムも一緒に絞り込む。

        EntityManager em = createEntityManager();
        addBooks(em, createBooks());

        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("tagFaceting")
            .onField("tag_facet")
            .discrete()
            .orderedBy(FacetSortOrder.COUNT_DESC)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder
            .bool()
            .must(queryBuilder.keyword().onField("title").matching("Java").createQuery())
            .must(queryBuilder.keyword().onField("tag_facet").matching("全文検索").createQuery())
            .createQuery();
        
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .hasSize(1);
        assertThat(books.get(0).getTitle())
            .isEqualTo("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築");

        List<Facet> facets = facetManager.getFacets("tagFaceting");

        assertThat(facets)
            .hasSize(1);
        assertThat(facets.get(0).getValue())
            .isEqualTo("全文検索");
        assertThat(facets.get(0).getCount())
            .isEqualTo(1);

通常のフィールドと、ファセット用のフィールドでANDを取りました。ファセットで絞り込みを行う場合などは、こうなるのでしょうか?
※別途補足

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder
            .bool()
            .must(queryBuilder.keyword().onField("title").matching("Java").createQuery())
            .must(queryBuilder.keyword().onField("tag_facet").matching("全文検索").createQuery())
            .createQuery();

なお、この場合ファセット用のフィールドはAnalyzerがかからないことに注意です。

ファセットを得るためには、EntityのListを取得しないといけないの?

FullTextQuery#getResultListでEntityのListを引っこ抜くと、件数が多い場合にはちょっと気になります…。

ですが、FullTextQuery#getResultSizeでもOKみたいですよ?

        EntityManager em = createEntityManager();
        addBooks(em, createBooks());

        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("tagFaceting")
            .onField("tag_facet")
            .discrete()
            .orderedBy(FacetSortOrder.COUNT_DESC)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.all().createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        int hitsCount = fullTextQuery.getResultSize();
        assertThat(hitsCount)
            .isEqualTo(6);

        List<Facet> facets = facetManager.getFacets("tagFaceting");

        assertThat(facets)
            .hasSize(3);
        assertThat(facets.get(0).getValue())
            .isEqualTo("全文検索");
        assertThat(facets.get(0).getCount())
            .isEqualTo(3);
        assertThat(facets.get(1).getValue())
            .isEqualTo("Java");
        assertThat(facets.get(1).getCount())
            .isEqualTo(2);
        assertThat(facets.get(2).getValue())
            .isEqualTo("Spring");
        assertThat(facets.get(2).getCount())
            .isEqualTo(1);

ちょっと一息

ここで少し、気になるところを。

ファセットの結果は、累積する?

どうもJPAのコンテキストが生きている間は、ファセットの結果をFacetManagerが覚えているようです。なので、2回クエリを投げたりすると、結果が累積されたりします…これにちょっとハマって、今回はEntityManagerを都度作る方向にしました。

一応、ドリルダウンする機能も備えているようなのですが、これっていつまで覚えているんでしょう…?

Restricting query results
http://docs.jboss.org/hibernate/search/5.0/reference/en-US/html_single/#_restricting_query_results

ファセットってWebアプリケーションでよく使うものだと思うのですが、リクエストを跨いではさすがに覚えていないでしょうし。

このあたり、自分にHibernate自体の知識がないんだなぁと思います。

ファセットの実装

Luceneでファセットを使うには、標準だと以下の2つの方法があります。

facetモジュールは、いろいろ準備がいるので面倒なので、groupingを使うのかなぁと思っていました(Solrのファセットは、groupingの方を使って実装しています)。LuceneのAPIの使い方を参考にしたいなどもあったので、少し実装を追ってみました。

Hibernate Searchはそのいずれでもなく、独自実装です。

LuceneのCollectorのチェインを作り、検索時にファセットの情報を溜め込んでいるようです。

以下、Hibernate Searchによるファセット用のCollectorです。
https://github.com/hibernate/hibernate-search/blob/5.0.1.Final/engine/src/main/java/org/hibernate/search/query/collector/impl/FacetCollector.java

検索時に有効にしているのは、ここです。
https://github.com/hibernate/hibernate-search/blob/5.0.1.Final/engine/src/main/java/org/hibernate/search/query/engine/impl/QueryHits.java#L255

でも、こう書くとFullTextQuery#getResultSizeでファセットが取れるのは、ちょっと不思議な気がします(Entityの情報を抜いていないので)。

実際、件数取得時にファセットを組んでいるようには見えません。
https://github.com/hibernate/hibernate-search/blob/5.0.1.Final/engine/src/main/java/org/hibernate/search/query/engine/impl/HSQueryImpl.java#L326

FacetManager#getFacetsしている時も、そんな素振り見えないですし。
https://github.com/hibernate/hibernate-search/blob/5.0.1.Final/engine/src/main/java/org/hibernate/search/query/engine/impl/FacetManagerImpl.java#L96

と思ったら、上記の呼び先でロードするようになっていました…ここ、ちょっとわかりにくかったです…。
https://github.com/hibernate/hibernate-search/blob/5.0.1.Final/engine/src/main/java/org/hibernate/search/query/engine/impl/HSQueryImpl.java#L314

FullTextQuery#getResultSizeを呼び出した時に、ファセットの結果を持っていなかったらクエリを投げる、というわけですね。

とまあ、調べた時の小ネタでした。

では、続けましょう。

Range Facet

今度は、Range Facetです。ここでは、priceフィールドを使用して、ファセットを作ります。

先ほどのAmazonの例だと、こちらになります。

基本的な使い方

概ね、Discreteと同じですが、範囲指定や結果がちょっと変わりますね。まずはコードを載せましょう。

        EntityManager em = createEntityManager();
        addBooks(em, createBooks());

        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("priceFaceting")
            .onField("price")
            .range()
            .below(3000)
            .from(3001).to(3500)
            .above(3500).excludeLimit()
            .orderedBy(FacetSortOrder.RANGE_DEFINITION_ORDER)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.all().createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .hasSize(6);

        List<Facet> facets = facetManager.getFacets("priceFaceting");

        assertThat(facets)
            .hasSize(3);
        assertThat(facets.get(0).getValue())
            .isEqualTo("[, 3000]");
        assertThat(facets.get(0).getCount())
            .isEqualTo(2);
        assertThat(facets.get(1).getValue())
            .isEqualTo("[3001, 3500]");
        assertThat(facets.get(1).getCount())
            .isEqualTo(3);
        assertThat(facets.get(2).getValue())
            .isEqualTo("(3500, ]");
        assertThat(facets.get(2).getCount())
            .isEqualTo(1);

ファセットリクエストを作っているところが、ちょっと変わりました。

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("priceFaceting")
            .onField("price")
            .range()
            .below(3000)
            .from(3001).to(3500)
            .above(3500).excludeLimit()
            .orderedBy(FacetSortOrder.RANGE_DEFINITION_ORDER)
            .includeZeroCounts(false)
            .createFacetingRequest();

onFieldの後にrangeメソッドを呼び出すことで、作成するファセットリクエストがRange Facetになります。

belowで指定の値以下の範囲、fromとtoで指定の範囲内、aboveで指定の値以上を表します。excludeLimitを付けると、境界値を含めなくなります。
ソート順は、RANGE_DEFINITION_ORDERを指定することで、定義した範囲の順番になります。降順や昇順など、他のFacetSortOrderを指定しても構わないとは思いますが。

つまり、上記で作成したファセットは、

  • 3000以下
  • 3001 〜 3500
  • 3500より大きい

となります。

結果も、このように。

        assertThat(facets)
            .hasSize(3);
        assertThat(facets.get(0).getValue())
            .isEqualTo("[, 3000]");
        assertThat(facets.get(0).getCount())
            .isEqualTo(2);
        assertThat(facets.get(1).getValue())
            .isEqualTo("[3001, 3500]");
        assertThat(facets.get(1).getCount())
            .isEqualTo(3);
        assertThat(facets.get(2).getValue())
            .isEqualTo("(3500, ]");
        assertThat(facets.get(2).getCount())
            .isEqualTo(1);

境界値を含む倍は、getValueで取得できる値の括弧が「[]」となり、含まない場合は「()」となります。

もう少し例を

ここまでは、用意していた書籍のデータを使いまわしていましたが、ちょっとRange Facetにはサンプル不足なので、逐次定義していくことにします。

先ほどと範囲の定義は同じですが、境界値を含めた確認。

        EntityManager em = createEntityManager();
        addBooks(em, Arrays.asList(new Book("3000円の本",
                                            3000,
                                            "Tag"),
                                   new Book("3250円の本",
                                            3250,
                                            "Tag"),
                                   new Book("3500円の本",
                                            3500,
                                            "Tag"),
                                   new Book("3750円の本",
                                            3750,
                                            "Tag")));


        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("priceFaceting")
            .onField("price")
            .range()
            .below(3000)
            .from(3001).to(3500)
            .above(3500).excludeLimit()
            .orderedBy(FacetSortOrder.RANGE_DEFINITION_ORDER)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.all().createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .hasSize(4);

        List<Facet> facets = facetManager.getFacets("priceFaceting");

        assertThat(facets)
            .hasSize(3);
        assertThat(facets.get(0).getValue())
            .isEqualTo("[, 3000]");
        assertThat(facets.get(0).getCount())
            .isEqualTo(1);
        assertThat(facets.get(1).getValue())
            .isEqualTo("[3001, 3500]");
        assertThat(facets.get(1).getCount())
            .isEqualTo(2);
        assertThat(facets.get(2).getValue())
            .isEqualTo("(3500, ]");
        assertThat(facets.get(2).getCount())
            .isEqualTo(1);

続いて、excludeLimitの例。below、from、to、aboveのどこでも付けられます。以下のコードは、ひとつ前のコードと同じ意味です。
…特に意味はありませんが。

        EntityManager em = createEntityManager();
        addBooks(em, Arrays.asList(new Book("3000円の本",
                                            3000,
                                            "Tag"),
                                   new Book("3250円の本",
                                            3250,
                                            "Tag"),
                                   new Book("3500円の本",
                                            3500,
                                            "Tag"),
                                   new Book("3750円の本",
                                            3750,
                                            "Tag")));


        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("priceFaceting")
            .onField("price")
            .range()
            .below(3001).excludeLimit()
            .from(3000).excludeLimit().to(3501).excludeLimit()
            .above(3500).excludeLimit()
            .orderedBy(FacetSortOrder.RANGE_DEFINITION_ORDER)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.all().createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .hasSize(4);

        List<Facet> facets = facetManager.getFacets("priceFaceting");

        assertThat(facets)
            .hasSize(3);
        assertThat(facets.get(0).getValue())
            .isEqualTo("[, 3001)");
        assertThat(facets.get(0).getCount())
            .isEqualTo(1);
        assertThat(facets.get(1).getValue())
            .isEqualTo("(3000, 3501)");
        assertThat(facets.get(1).getCount())
            .isEqualTo(2);
        assertThat(facets.get(2).getValue())
            .isEqualTo("(3500, ]");
        assertThat(facets.get(2).getCount())
            .isEqualTo(1);

また、from、toは繰り返せるので、もっと多くの範囲を区切ることもできます。

        EntityManager em = createEntityManager();
        addBooks(em, Arrays.asList(new Book("3000円の本",
                                            3000,
                                            "Tag"),
                                   new Book("3250円の本",
                                            3250,
                                            "Tag"),
                                   new Book("3500円の本",
                                            3500,
                                            "Tag"),
                                   new Book("3750円の本",
                                            3750,
                                            "Tag"),
                                   new Book("4000円の本",
                                            4000,
                                            "Tag"),
                                   new Book("4250円の本",
                                            4250,
                                            "Tag"),
                                   new Book("4500円の本",
                                            4500,
                                            "Tag")));


        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
        QueryBuilder queryBuilder =
            ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();

        FacetingRequest facetingRequest =
            queryBuilder
            .facet()
            .name("priceFaceting")
            .onField("price")
            .range()
            .below(3000)
            .from(3001).to(3500)
            .from(3501).to(4000)
            .above(4000).excludeLimit()
            .orderedBy(FacetSortOrder.RANGE_DEFINITION_ORDER)
            .includeZeroCounts(false)
            .createFacetingRequest();

        org.apache.lucene.search.Query luceneQuery =
            queryBuilder.all().createQuery();
        FullTextQuery fullTextQuery = ftem.createFullTextQuery(luceneQuery, Book.class);

        FacetManager facetManager = fullTextQuery.getFacetManager();
        facetManager.enableFaceting(facetingRequest);

        List<Book> books = fullTextQuery.getResultList();
        assertThat(books)
            .hasSize(7);

        List<Facet> facets = facetManager.getFacets("priceFaceting");

        assertThat(facets)
            .hasSize(4);
        assertThat(facets.get(0).getValue())
            .isEqualTo("[, 3000]");
        assertThat(facets.get(0).getCount())
            .isEqualTo(1);
        assertThat(facets.get(1).getValue())
            .isEqualTo("[3001, 3500]");
        assertThat(facets.get(1).getCount())
            .isEqualTo(2);
        assertThat(facets.get(2).getValue())
            .isEqualTo("[3501, 4000]");
        assertThat(facets.get(2).getCount())
            .isEqualTo(2);
        assertThat(facets.get(3).getValue())
            .isEqualTo("(4000, ]");
        assertThat(facets.get(3).getCount())
            .isEqualTo(2);

だいぶ長くなりましたが、Hibernate Searchのファセットの基本的な使い方は、こんなところでしょうか。

これだけだと面白くないので、関連を使用したファセットの作成を次に書いています。

Hibernate Searchでファセットを使う - 関連Entity編
http://d.hatena.ne.jp/Kazuhira/20150125/1422188307