検索でよく使われそうな機能の中に、ファセットというものがあります。
ファセットがどういうものかは、見た方が早いですね。下記は、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&characterEncoding=utf-8&characterSetResults=utf-8&useServerPrepStmts=true&useLocalSessionState=true&elideSetAutoCommits=true&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")); }
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モジュールを使う(http://lucene.apache.org/core/4_10_3/facet/index.html)
- groupingモジュール内の、ファセットの機能を使う(http://lucene.apache.org/core/4_10_3/grouping/index.html)
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