Infinispan 6.0から、Query DSLというものが追加されました。ちょっと気になっていたので、試してみようと思います。
前提知識
InfinispanのQueryですが、Lucene+Hibernate Searchという形で実現されています。よって、最終的にはLuceneのQueryが投げられるということは、押さえていた方がよいと思います。
現状は、Query DSL以外の方法でクエリを投げる場合は、LuceneのQueryを直接組み立てるか、Hibernate SearchのDSLを使用します。
って言ってますが、自分はHibernate Searchには詳しくないですけどね…。
将来的には、検索の機能をLucene/Hibernate Search非依存にすることも考えているみたいです。現状は、Lucene/Hibernate Searchですが。
準備
読むべきドキュメントは、以下の2つですね。
Querying Infinispan
http://infinispan.org/docs/6.0.x/user_guide/user_guide.html#_querying_infinispan
Infinispan’s Query DSL
http://infinispan.org/docs/6.0.x/user_guide/user_guide.html#_infinispan_s_query_dsl
Query DSLについて書かれているのは、Querying Infinispanの中にあるのですが、クローズアップした方がよいでしょう。
依存関係の定義は、ひとまずこんな感じ。
build.sbt
fork in test := true libraryDependencies ++= Seq( "org.infinispan" % "infinispan-query" % "6.0.0.Final", "net.jcip" % "jcip-annotations" % "1.0", "org.scalatest" %% "scalatest" % "2.0" % "test" )
InfinispanのQuery Moduleを入れれば、Query DSLも付いてきます。Query DSL自体は別のアーティファクトとして切り出されていますが、APIだけなので現状query-dslだけを追加しても無効です。
そういえば、sbtを0.13.1にアップデートしたら、Infinispanの依存関係が解決できるようになりました!素晴らしい!
Infinispanの設定。
src/main/resources/infinispan.xml
<?xml version="1.0" encoding="UTF-8"?> <infinispan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:infinispan:config:6.0 http://www.infinispan.org/schemas/infinispan-config-6.0.xsd" xmlns="urn:infinispan:config:6.0"> <global> <globalJmxStatistics enabled="true" jmxDomain="org.infinispan" cacheManagerName="DefaultCacheManager" /> <shutdown hookBehavior="REGISTER"/> </global> <default> <indexing enabled="true" indexLocalOnly="true"> <properties> <property name="default.directory_provider" value="infinispan" /> <property name="lucene_version" value="LUCENE_36" /> </properties> </indexing> </default> </infinispan>
なんとなく、LuceneのDirectory ProviderはInfinispanにしてみました。今回は、あまり意味がありませんが。「indexLocalOnly」などは、ドキュメント参照。
検索対象のEntity。
src/main/scala/org/littlewings/infinispan/querydsl/entity/Book.scala
package org.littlewings.infinispan.querydsl.entity import scala.collection._ import scala.collection.JavaConverters._ import java.util.{Date, Objects} import org.hibernate.search.annotations.{Analyze, Field, DateBridge, Indexed, IndexedEmbedded, Resolution, Store} object Book { def apply(isbn: String, title: String, summary: String, price: Int, publisherDate: Date, authors: Author*): Book = { val book = new Book book.isbn = isbn book.title = title book.summary = summary book.price = price book.publisherDate = publisherDate book.authorsAsJava = Set(authors: _*).asJava book } } @SerialVersionUID(1L) @Indexed class Book extends Serializable { @Field(analyze = Analyze.NO, store = Store.YES) // Projectionを使用する場合は、Store.YES var isbn: String = _ @Field(analyze = Analyze.NO, store = Store.YES) // Projectionを使用する場合は、Store.YES var title: String = _ @Field(analyze = Analyze.NO) var summary: String = _ @Field(analyze = Analyze.NO) var price: Int = _ @Field(analyze = Analyze.NO) @DateBridge(resolution = Resolution.DAY) var publisherDate: Date = _ @IndexedEmbedded var authorsAsJava: java.util.Set[Author] = _ var authors: mutable.Set[Author] = authorsAsJava.asScala override def equals(other: Any): Boolean = other match { case ob: Book => isbn == ob.isbn && title == ob.title && summary == ob.summary && price == ob.price && publisherDate == ob.publisherDate && authorsAsJava == ob.authorsAsJava case _ => false } override def hashCode: Int = Objects.hash(isbn, title, summary, Integer.valueOf(price), publisherDate, authorsAsJava) override def toString: String = s"""Book[isbn = $isbn, | title = $title, | summary = $summary, | price = $price, | publisherDate = $publisherDate, | authors = { ${Option(authorsAsJava).getOrElse(new java.util.HashSet).asScala.mkString(", ")} }]""".stripMargin }
EmbeddedなEntity。
src/main/scala/org/littlewings/infinispan/querydsl/entity/Author.scala
package org.littlewings.infinispan.querydsl.entity import java.util.Objects import org.hibernate.search.annotations.{Analyze, Field} object Author { def apply(name: String): Author = { val author = new Author author.name = name author } } @SerialVersionUID(1L) class Author extends Serializable { @Field(analyze = Analyze.NO) var name: String = _ override def equals(other: Any): Boolean = other match { case oa: Author => name == oa.name case _ => false } override def hashCode: Int = Objects.hash(name) override def toString: String = s"Author[name = $name]" }
注意点ですが、Query DSLを使用する時はFieldアノテーションは必ず
@Field(analyze = Analyze.NO)
とアナライズしないようにしてください。また、Projectionを使用する場合は値を保存することが必要です。
@Field(analyze = Analyze.NO, store = Store.YES) // Projectionを使用する場合は、Store.YES
使ってみる
では、この定義したEntityをCacheに放り込んで、Query DSLを試してみましょう。
以下のような、雛形コードを用意。
src/test/scala/org/littlewings/infinispan/querydsl/BookQueryDslSpec.scala
package org.littlewings.infinispan.querydsl import scala.collection.JavaConverters._ import java.text.SimpleDateFormat import org.infinispan.Cache import org.infinispan.manager.DefaultCacheManager import org.infinispan.query.Search import org.infinispan.query.dsl.{Query, SortOrder} // import org.infinispan.query.dsl.impl.AttributeCondition import org.littlewings.infinispan.querydsl.entity.{Book, Author} import org.scalatest.{BeforeAndAfterAll, FunSpec} import org.scalatest.Matchers._ class BookQueryDslSpec extends FunSpec { val toDate = (dateString: String) => new SimpleDateFormat("yyyy/MM/dd").parse(dateString) val infinispanBook: Book = Book("978-1849518222", "Infinispan Data Grid Platform", "Making use of data grids for performance and scalability in Enterprise Java, using Infinispan from JBoss", 3186, toDate("2012/06/30"), Author("Francesco Marchioni"), Author("Manik Surtani")) val hazelcastBook: Book = Book("978-1782167303", "Getting Started With Hazelcast", "An easy-to-follow and hands-on introduction to the highly scalable data distribution system, Hazelcast, and its advanced features.", 4336, toDate("2013/08/27"), Author("Mat Johns")) val luceneBook: Book = Book("978-1933988177", "Lucene in Action", "New edition of top-selling book on the new version of Lucene the core open-source technology behind most full-text search and Intelligent Web applications.", 5421, toDate("2010/06/30"), Author("Michael McCandless"), Author("Erik Hatcher"), Author("Otis Gospodnetic")) val books = Array(infinispanBook, hazelcastBook, luceneBook) describe("infinispan query dsl") { // ここにテストを書く! } def withCache[K, V](fileName: String, cacheName: String)(fun: Cache[K, V] => Unit): Unit = { val manager = new DefaultCacheManager(fileName) try { fun(manager.getCache[K, V](cacheName)) } finally { manager.stop() } } }
とりあえず、Infinispan、Hazelcast、Luceneの書籍を扱うことにしましょう。また、テストケースごとに、InfinispanのCacheを作成・破棄します。
まずは、簡単な使い方。
it("search simple in") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .having("isbn") .in("978-1782167303") .toBuilder .build query.toString should include ("query=isbn:978-1782167303") query.list[Book] should have size 1 query.list[Book].get(0) should be (hazelcastBook) } }
SearchクラスからSearchManagerを取得、SearchManagerからQueryFactoryを取得します。
val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory
ここで取得したQueryFactoryに対して、DSLを組んでQueryを組み立てます。
val query: Query = queryFactory .from(classOf[Book]) .having("isbn") .in("978-1782167303") .toBuilder .build
fromでEntityを指定し、havingで検索を行うためのフィールド名を指定します。その後は、
- eq
- gt
- gte
- lt
- lte
- in
- like
- between
- contains
- isNull
- and
- or
などが使えます。詳しくは、以下のインターフェース定義を見てみるとよいでしょう。
QueryFactory
https://docs.jboss.org/infinispan/6.0/apidocs/org/infinispan/query/dsl/QueryFactory.html
QueryBuilder
https://docs.jboss.org/infinispan/6.0/apidocs/org/infinispan/query/dsl/QueryBuilder.html
FilterConditionContext
https://docs.jboss.org/infinispan/6.0/apidocs/org/infinispan/query/dsl/FilterConditionContext.html
FilterConditionEndContext
https://docs.jboss.org/infinispan/6.0/apidocs/org/infinispan/query/dsl/FilterConditionEndContext.html
また、今回の検索条件だと、以下のようなLucene Queryが投げられ、1件書籍がヒットします。
query.toString should include ("query=isbn:978-1782167303") query.list[Book] should have size 1 query.list[Book].get(0) should be (hazelcastBook)
なぜ1件探しているだけなのに、「eq」を使っていないのかは、Scalaでこのコードを書いているからです…。理由は、以下のエントリに書きました。
Scalaから、Javaのインターフェースに定義されたeq(Object)メソッドを呼べない?
http://d.hatena.ne.jp/Kazuhira/20131215/1387109792
続いて、いくつか試してみましょう。以後、こんな感じでDSLと実際に投げられるLucene Queryをペアで書いていきます。
like。
it("search simple like") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .having("summary") .like("%data%") .toBuilder .build query.toString should include ("query=summary:*data*") query.list[Book].asScala should contain theSameElementsAs (Array(infinispanBook, hazelcastBook)) } }
between(range)。
it("search price range") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .orderBy("price", SortOrder.ASC) .having("price") .between(3000, 4500) .toBuilder .build query.toString should include ("query=price:[3000 TO 4500]") query.list[Book].asScala should contain theSameElementsInOrderAs (Array(infinispanBook, hazelcastBook)) } }
gte、lte。まあ、今回の場合はbetweenでもいいですが…。
it("search date gte lte") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .orderBy("price", SortOrder.ASC) .having("publisherDate") .gte(toDate("2012/01/01")) .and .having("publisherDate") .lte(toDate("2013/12/31")) .toBuilder .build query.toString should include ("query=+publisherDate:[20111231 TO *] +publisherDate:[* TO 20131230]") query.list[Book].asScala should contain theSameElementsInOrderAs (Array(infinispanBook, hazelcastBook)) } }
likeをandでつなげて。
it("search like and like") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .having("summary") .like("%data%") .and .having("title") .like("%Infinispan%") .toBuilder .build query.toString should include ("query=+summary:*data* +title:*Infinispan*") query.list[Book].asScala should contain theSameElementsAs (Array(infinispanBook)) } }
sortとワイルドカード。
it("search wildcard and sort") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .orderBy("price", SortOrder.DESC) .having("isbn") .like("*") .toBuilder .build query.toString should include ("query=isbn:*") query.list[Book].asScala should contain theSameElementsInOrderAs (Array(luceneBook, hazelcastBook, infinispanBook)) } }
EmbeddedなEntityに対しての条件指定。
it("search embedded entity") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .having("authorsAsJava.name") .like("Manik%") .toBuilder .build query.toString should include ("query=authorsAsJava.name:Manik*") query.list should have size 1 query.list[Book].asScala.apply(0) should be (infinispanBook) } }
条件のネスト。
it("search nested condition") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .orderBy("price", SortOrder.ASC) .having("summary") .like("%data%") .and(queryFactory .having("title") .like("%Infinispan%") .or .having("title") .like("%Hazelcast%")) .toBuilder .build query.toString should include ("query=+summary:*data* +(title:*Infinispan* title:*Hazelcast*)") query.list[Book].asScala should contain theSameElementsInOrderAs (Array(infinispanBook, hazelcastBook)) } }
Projection。
it("search projection") { withCache[String, Book]("infinispan.xml", "bookCache") { cache => books.foreach(book => cache.put(book.isbn, book)) val searchManager = Search.getSearchManager(cache) val queryFactory = searchManager.getQueryFactory val query: Query = queryFactory .from(classOf[Book]) .setProjection("isbn", "title") .having("isbn") .like("978-1849518222") .toBuilder .build query.toString should include ("query=isbn:978-1849518222") query.list should have size 1 // Projectionの場合、Objectの配列が返ってくる query .list[Any] .asScala .apply(0) .asInstanceOf[Array[Any]] should contain theSameElementsInOrderAs (Array(infinispanBook.isbn, infinispanBook.title)) } }
Projectionの場合は、Entityが返るのではなく指定した項目を含むObjecの配列(のList)が戻ってくるので注意です。このため、Store.YESにする必要があるのだと思われます。
内部的には、JPAのクエリに変換してHibernate Searchに投げているみたいです。この時、Analyze.NOにしていなかった場合は例外が飛んできます。
LuceneのQueryって、最初にAnalyzerを指定するのでQuery DSLではそのあたりどうするのかなぁ?と思っていましたが(fromでAnalyzerを指定しない)、むしろアナライズするなということですね。
転置インデックスを活用した全文検索というよりは、普通の検索っぽいのである意味わかりやすいですかね。実際、あとで日本語を使った検索とかしようと考えていたのですが、Analyzerの定義をEntityにしていくことを考えると、ちょっと気が重くなりましたし…。
とりあえず、けっこう単純に使えるので良いと思います。DSL以外にも、JPQLとか投げられるといいのですが…それは、OGMの方を待っていた方がよいでしょうか。
今回書いたコードは、こちらにアップしています。
https://github.com/kazuhira-r/infinispan-examples/tree/master/infinispan-query-dsl