Luceneでドキュメントに数値を保存する時、数値向けのフィールドとしてIntFieldやLongField、DoubleFieldなどを使うことができます。ですが、これらはTermとしてはちょっと別物になるので、普通のクエリでは検索できずNumericRangeQueryを使うことになります。
Luceneを試していた時は、最初この差を知らなくて検索できなかったり、たまにコロッと忘れてハマったりするのですが…Hibernate Search越しだとどうなるのかがちょっと気になって試してみました。
結果からいうと、Query DSLでもあまり意識することなく使えました。
まあ、見ていってみましょう。
準備
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>
データベースにはMySQL、テストコードにはJUnitとAssertJを使用します。
また、persistence.xmlはあまり今回言うところがないので端折ります。
テーブル定義とEntity
今回対象とするテーブルは、このようなものとします。
CREATE TABLE book( id INT AUTO_INCREMENT, title VARCHAR(255), price INT, summary VARCHAR(255), PRIMARY KEY(id) );
お題は書籍。
対応するEntity。
src/main/java/org/littlewings/hibernate/numeric/Book.java
package org.littlewings.hibernate.numeric; 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 @Field private String summary; public Book() { } public Book(String title, Integer price, String summary) { this.title = title; this.price = price; this.summary = summary; } 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 getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } }
今回、priceが数値型のフィールドですね。
@Column @Field private Integer price;
ちなみに、本来数値型のフィールドには@NumericFieldアノテーションを付与するものらしいのですが
@NumericField
http://docs.jboss.org/hibernate/search/5.0/reference/en-US/html_single/#numeric-field-annotation
Hibernate Search 5からは、数値型のフィールドには自動的に付与されるような扱いになったみたいです。
Caution
Prior to Search 5, numeric field encoding was only chosen if explicitly requested via @NumericField. As of Search 5 this encoding is automatically chosen for numeric types. To avoid numeric encoding you can explicitly specify a non numeric field bridge via @Field.bridge or @FieldBridge. The package org.hibernate.search.bridge.builtin contains a set of bridges which encode numbers as strings, for example org.hibernate.search.bridge.builtin.IntegerBridge.
http://docs.jboss.org/hibernate/search/5.0/reference/en-US/html_single/#field-annotation
ですので、今回はこのままいきます。
テストコードの雛形
テストコードのために、用意した雛形メソッドなど。
src/test/java/org/littlewings/hibernate/numeric/NumericFieldTest.java
package org.littlewings.hibernate.numeric; 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.junit.*; public class NumericFieldTest { private static EntityManager em; @BeforeClass public static void setUpClass() { EntityManagerFactory emf = Persistence.createEntityManagerFactory("search.pu"); em = emf.createEntityManager(); } @After public void tearDown() { EntityTransaction tx = em.getTransaction(); tx.begin(); em.createQuery("DELETE FROM Book").executeUpdate(); tx.commit(); } @AfterClass public static void tearDownClass() { EntityTransaction tx = em.getTransaction(); tx.begin(); em.createNativeQuery("TRUNCATE TABLE book").executeUpdate(); tx.commit(); } private void addBooks(Book... books) { EntityTransaction tx = em.getTransaction(); tx.begin(); Arrays.asList(books).stream().forEach(em::persist); tx.commit(); } // ここに、テストを書く! }
FullTextEntityManagerを使ったり、データ登録のヘルパーメソッドとかですね。
では、テストを書いていきます。
キーワードクエリを投げてみる
とりあえず、最もオーソドックスと思われるキーワードクエリを投げてみます。
addBooks(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")); FullTextEntityManager ftem = Search.getFullTextEntityManager(em); QueryBuilder queryBuilder = ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get(); org.apache.lucene.search.Query luceneQuery = queryBuilder.keyword().onField("price").matching(3200).createQuery(); Query jpaQuery = ftem.createFullTextQuery(luceneQuery, Book.class); List<Book> books = jpaQuery.getResultList(); assertThat(luceneQuery) .isInstanceOf(org.apache.lucene.search.NumericRangeQuery.class); assertThat(luceneQuery.toString()) .isEqualTo("price:[3200 TO 3200]"); assertThat(books) .hasSize(1); assertThat(books.get(0).getTitle()) .isEqualTo("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築");
結果はもう書いていますが、このようなクエリが
org.apache.lucene.search.Query luceneQuery = queryBuilder.keyword().onField("price").matching(3200).createQuery();
なんとNumericRangeQueryとして扱われます。
assertThat(luceneQuery) .isInstanceOf(org.apache.lucene.search.NumericRangeQuery.class); assertThat(luceneQuery.toString()) .isEqualTo("price:[3200 TO 3200]");
これは素晴らしい。
範囲クエリ
今度は、Rangeを使うクエリ。
addBooks(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")); FullTextEntityManager ftem = Search.getFullTextEntityManager(em); QueryBuilder queryBuilder = ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get(); org.apache.lucene.search.Query luceneQuery = queryBuilder.range().onField("price").from(3200).to(3486).createQuery(); Query jpaQuery = ftem.createFullTextQuery(luceneQuery, Book.class); List<Book> books = jpaQuery.getResultList(); assertThat(luceneQuery) .isInstanceOf(org.apache.lucene.search.NumericRangeQuery.class); assertThat(luceneQuery.toString()) .isEqualTo("price:[3200 TO 3486]"); assertThat(books) .hasSize(2); assertThat(books.get(0).getTitle()) .isEqualTo("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築"); assertThat(books.get(1).getTitle()) .isEqualTo("改訂2版 パーフェクトJava");
今度は、このfromとtoが
org.apache.lucene.search.Query luceneQuery = queryBuilder.range().onField("price").from(3200).to(3486).createQuery();
このようになります。
assertThat(luceneQuery.toString())
.isEqualTo("price:[3200 TO 3486]");
上記の例では、下限、上限を含む形でしたが、境界値を外す場合はexcludeLimitを使用します。
addBooks(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")); FullTextEntityManager ftem = Search.getFullTextEntityManager(em); QueryBuilder queryBuilder = ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get(); org.apache.lucene.search.Query luceneQuery = queryBuilder.range().onField("price").from(3200).to(3486).excludeLimit().createQuery(); Query jpaQuery = ftem.createFullTextQuery(luceneQuery, Book.class); List<Book> books = jpaQuery.getResultList(); assertThat(luceneQuery) .isInstanceOf(org.apache.lucene.search.NumericRangeQuery.class); assertThat(luceneQuery.toString()) .isEqualTo("price:[3200 TO 3486}"); assertThat(books) .hasSize(1); assertThat(books.get(0).getTitle()) .isEqualTo("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築");
この例では、3486より小さい、となっています。
上限だけ決める場合。
addBooks(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")); FullTextEntityManager ftem = Search.getFullTextEntityManager(em); QueryBuilder queryBuilder = ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get(); org.apache.lucene.search.Query luceneQuery = queryBuilder.range().onField("price").below(3200).excludeLimit().createQuery(); Query jpaQuery = ftem.createFullTextQuery(luceneQuery, Book.class); List<Book> books = jpaQuery.getResultList(); assertThat(luceneQuery) .isInstanceOf(org.apache.lucene.search.NumericRangeQuery.class); assertThat(luceneQuery.toString()) .isEqualTo("price:[* TO 3200}"); assertThat(books) .hasSize(3); assertThat(books.get(0).getTitle()) .isEqualTo("はじめてのSpring Boot 「Spring Framework」で簡単Javaアプリ開発"); assertThat(books.get(1).getTitle()) .isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server"); assertThat(books.get(2).getTitle()) .isEqualTo("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!]");
こちらも、普通に使えます。下限のみを決める場合は、aboveを使えばOK。excludeLimitも、もちろん使えます。
普通のNumericRangeQueryを投げる
当然といえば当然ですが、LuceneのNumericRangeQueryを直接構築してもOKです。
この説明と過去の記憶から、てっきり自分でNumericRangeQueryを構築するのかと思っていましたよ…。
short, Short, integer, Integer, long, Long, float, Float, double, Double
http://docs.jboss.org/hibernate/search/5.0/reference/en-US/html_single/#section-built-in-bridges
Are per default indexed numerically using a Trie structure. You need to use a NumericRangeQuery to search for values. See also Section 4.1.1.2, “@Field” and Section 4.1.1.3, “@NumericField”
利用例。
addBooks(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")); FullTextEntityManager ftem = Search.getFullTextEntityManager(em); QueryBuilder queryBuilder = ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get(); org.apache.lucene.search.Query luceneQuery = org.apache.lucene.search.NumericRangeQuery.newIntRange("price", 3200, 3486, true, false); Query jpaQuery = ftem.createFullTextQuery(luceneQuery, Book.class); List<Book> books = jpaQuery.getResultList(); assertThat(luceneQuery.toString()) .isEqualTo("price:[3200 TO 3486}"); assertThat(books) .hasSize(1); assertThat(books.get(0).getTitle()) .isEqualTo("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築");
こちらのクエリが、
org.apache.lucene.search.Query luceneQuery = org.apache.lucene.search.NumericRangeQuery.newIntRange("price", 3200, 3486, true, false);
このクエリと同じ意味になります。
org.apache.lucene.search.Query luceneQuery = queryBuilder.range().onField("price").from(3200).to(3486).excludeLimit().createQuery();
というわけで、NumericRangeQuery#newIntRangeの第4、5引数のboolean値で、境界値値を含むかどうかを決められます。
いやぁ、けっこう素直に使えるものなんですね〜。