先日、Infinispan 9(9.0.0.Final)がリリースされました。
8系のリリースから1年くらいかかっていますが、まあいろいろ変わって新機能も追加されました。
これから順次見ていこうと思います。
Ickle Query
で、最初にInfinispanに新しく導入されたQuery、「Ickle」を試してみたいと思います。
ドキュメントには、またTODOになっていて記載がありません…。
現時点で参考になるリソースは、以下になります。
Infinispan Query - Konstanz, October 2016 - Google スライド
あとはテストコードでしょう。
Ickleとは、InfinispanのQueryの一種ですが、これまでQuery DSLで書いていたものを、Stringとして表現できるように
したものです。
※もうちょっと前にこの方針にして欲しかったかも
ブログによると、特徴は以下のとおりです。JP-QLのサブセットで、JOINとか計算式、サブクエリなどは使えなかったりします。
・ is a light and small subset of JP-QL, hence the lovely name
http://blog.infinispan.org/2016/12/meet-ickle.html
・ queries Java classes and supports Protocol Buffers too
・ queries can target a single entity type・
・ queries can filter on properties of embedded objects too, including collections
・ supports projections, aggregations, sorting, named parameters
・ supports indexed and non-indexed execution
・ supports complex boolean expressions
・ does not support computations in expressions (eg. user.age > sqrt(user.shoeSize + 3) is not allowed but user.age >= 18 is fine)
・ does not support joins
・ but, navigations along embedded entities are implicit joins and are allowed
・ joining on embedded collections is allowed
・ other join types not supported
・ subqueries are not supported
・ besides the normal relational operators it offers full-text operators, similar to Lucene’s query parser
・ is now supported across various Infinispan APIs, wherever a Query produced by the QueryBuilder is accepted (even for continuous queries or in event filters for listeners!)
また、InfinispanのCacheのindexingを有効にしていれば、LuceneのFull Text Queryも使うことができます。
早速試してみましょう。
準備
今回利用した依存関係は、以下のとおり。
libraryDependencies ++= Seq( "org.infinispan" % "infinispan-query" % "9.0.0.Final" % Compile, "net.jcip" % "jcip-annotations" % "1.0" % Provided, "org.apache.lucene" % "lucene-analyzers-kuromoji" % "5.5.4" % Compile, "org.scalatest" %% "scalatest" % "3.0.1" % Test )
Queryを使うだけであれば、「infinispan-query」があればOKです。テスト用にScalaTestと、Full Text Query用に
なんとなくKuromojiも追加しています。
Infinispanの設定は以下としますが、Cacheの定義そのものはあとで記載します。
src/test/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:9.0 http://www.infinispan.org/schemas/infinispan-config-9.0.xsd" xmlns="urn:infinispan:config:9.0"> <jgroups> <stack-file name="udp" path="default-configs/default-jgroups-udp.xml"/> </jgroups> <cache-container> <jmx duplicate-domains="true"/> <transport cluster="test-cluster" stack="udp"/> <!-- Cacheの定義はあとで --> </cache-container> </infinispan>
JGroupsの設定は、デフォルトとします。
テストコードの雛形
動作確認はテストコードを使って行っていきますが、雛形としては以下とします。
src/test/scala/org/littlewings/infinispan/icklequery/IckleQuerySpec.scala
package org.littlewings.infinispan.icklequery import org.infinispan.Cache import org.infinispan.manager.DefaultCacheManager import org.infinispan.objectfilter.ParsingException import org.infinispan.query.Search import org.scalatest.{FunSuite, Matchers} class IckleQuerySpec extends FunSuite with Matchers { // ここに、テストを書く! protected def withCache[K, V](cacheName: String, numInstances: Int = 1)(fun: Cache[K, V] => Unit): Unit = { val managers = (1 to numInstances).map(_ => new DefaultCacheManager("infinispan.xml")) managers.foreach(_.getCache(cacheName)) try { val cache = managers(0).getCache[K, V](cacheName) fun(cache) cache.stop() } finally { managers.foreach(_.stop()) } } }
簡易的にクラスタを構成するヘルパーメソッド付き。
以降、テストコードを埋めていきます。
リフレクションベースのQuery
InfinispanのQuery DSLで扱えるQueryには、2種類あります。Cacheのindexingを有効にしている場合はFull Text Queryですが、
そうでない場合はリフレクションベースのQueryになります(全件スキャン)。
最初は、リフレクションベースのQueryからいってみます。お題は書籍としましょう。
まずは、Cacheの定義。Distributed Cacheとします。
<distributed-cache name="bookCache"/>
他には特に設定は入れません。
Cacheに登録するEntityクラスは、こちら。
src/main/scala/org/littlewings/infinispan/icklequery/Book.scala
package org.littlewings.infinispan.icklequery import scala.beans.BeanProperty object Book { def apply(isbn: String, title: String, price: Int, category: String): Book = { val book = new Book book.isbn = isbn book.title = title book.price = price book.category = category book } } @SerialVersionUID(1L) class Book extends Serializable { @BeanProperty var isbn: String = _ @BeanProperty var title: String = _ @BeanProperty var price: Int = _ @BeanProperty var category: String = _ }
テストデータ。
val books: Array[Book] = Array( Book("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320, "Spring"), Book("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104, "Spring"), Book("978-4774161631", "[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン", 3888, "全文検索"), Book("978-4048662024", "高速スケーラブル検索エンジン ElasticSearch Server", 6915, "全文検索"), Book("978-4774183169", "パーフェクト Java EE", 3456, "Java EE"), Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104, "Java EE") )
では、使ってみたコードはこちら。
test("index-less simple Ickle Query") { withCache[String, Book]("bookCache", 3) { cache => books.foreach(b => cache.put(b.isbn, b)) val queryFactory = Search.getQueryFactory(cache) val query = queryFactory.create( """|from org.littlewings.infinispan.icklequery.Book b |where b.price > 5000 |and b.title = '高速スケーラブル検索エンジン ElasticSearch Server'""".stripMargin) val resultBooks = query.list[Book]() resultBooks should have size (1) resultBooks.get(0).getIsbn should be("978-4048662024") resultBooks.get(0).getTitle should be("高速スケーラブル検索エンジン ElasticSearch Server") resultBooks.get(0).getPrice should be(6915) resultBooks.get(0).getCategory should be("全文検索") } }
これまでのQuery DSL同様、Search#getQueryFactoryでQueryFactoryを取得するところから始まります。
val queryFactory = Search.getQueryFactory(cache)
Queryの作成は割と単純で、QueryFactory#createでQueryStringを渡せばOKです。
val query = queryFactory.create( """|from org.littlewings.infinispan.icklequery.Book b |where b.price > 5000 |and b.title = '高速スケーラブル検索エンジン ElasticSearch Server'""".stripMargin)
from句のEntity名は、FQCNで指定する必要があります。
結果の受け取り方は、Query DSLと変わりません。
val resultBooks = query.list[Book]() resultBooks should have size (1) resultBooks.get(0).getIsbn should be("978-4048662024") resultBooks.get(0).getTitle should be("高速スケーラブル検索エンジン ElasticSearch Server") resultBooks.get(0).getPrice should be(6915) resultBooks.get(0).getCategory should be("全文検索")
というか、Queryのインスタンスの作り方が違うだけですからね。そりゃあそうですねと…。
Queryに対して、パラメーターをバインドすることもできます。「:」を付けてQueryStringに埋め込めばOKです。
test("index-less simple Ickle Query, parameterized") { withCache[String, Book]("bookCache", 3) { cache => books.foreach(b => cache.put(b.isbn, b)) val queryFactory = Search.getQueryFactory(cache) val query = queryFactory.create( """|from org.littlewings.infinispan.icklequery.Book b |where b.price > :price |and b.title = :title""".stripMargin) query.setParameter("price", 5000) query.setParameter("title", "高速スケーラブル検索エンジン ElasticSearch Server") val resultBooks = query.list[Book]() resultBooks should have size (1) resultBooks.get(0).getIsbn should be("978-4048662024") resultBooks.get(0).getTitle should be("高速スケーラブル検索エンジン ElasticSearch Server") resultBooks.get(0).getPrice should be(6915) resultBooks.get(0).getCategory should be("全文検索") } }
こんな感じです。
queryFactory.create( """|from org.littlewings.infinispan.icklequery.Book b |where b.price > :price |and b.title = :title""".stripMargin)
値の設定自体は、Query#setParameterで行います。Query DSLと同じです。
query.setParameter("price", 5000) query.setParameter("title", "高速スケーラブル検索エンジン ElasticSearch Server")
Group Byといった、Aggregationも可能です。この場合、結果はObjectの配列になってしまいますが。
※Projectionも同様だと思います
※しれっとHavingとOrder Byも入れています
test("index-less simple Ickle Query, aggregation") { withCache[String, Book]("bookCache", 3) { cache => books.foreach(b => cache.put(b.isbn, b)) val queryFactory = Search.getQueryFactory(cache) val query = queryFactory.create( """|select b.category, sum(b.price) |from org.littlewings.infinispan.icklequery.Book b |where b.price > :price |group by b.category |having sum(b.price) > :sumPrice |order by sum(b.price) desc""".stripMargin) query.setParameter("price", 4000) query.setParameter("sumPrice", 5000) val results = query.list[Array[AnyRef]]() results should have size (2) results.get(0)(0) should be("Spring") results.get(0)(1) should be(8424) results.get(1)(0) should be("全文検索") results.get(1)(1) should be(6915) } }
Full Text Query
続いては、Full Text Queryを使ってみたいと思います。
今度は、先にEntityから。
src/main/scala/org/littlewings/infinispan/icklequery/IndexedBook.scala
package org.littlewings.infinispan.icklequery import org.apache.lucene.analysis.ja.JapaneseAnalyzer import org.hibernate.search.annotations._ import scala.beans.BeanProperty object IndexedBook { def apply(isbn: String, title: String, price: Int, category: String): IndexedBook = { val book = new IndexedBook book.isbn = isbn book.title = title book.price = price book.category = category book } } @Indexed @Analyzer(impl = classOf[JapaneseAnalyzer]) @SerialVersionUID(1L) class IndexedBook extends Serializable { @Field(analyze = Analyze.NO) @BeanProperty var isbn: String = _ @Field @BeanProperty var title: String = _ @Field @BeanProperty var price: Int = _ @Field(analyze = Analyze.NO) @BeanProperty var category: String = _ }
Hibernate Searchの、各種アノテーションを付与します。Analyzerは、なんとなくKuromojiに…。
Cacheの設定は、このようにしました。
<distributed-cache name="indexedBookCache"> <indexing index="LOCAL" auto-config="true"> <indexed-entities> <indexed-entity>org.littlewings.infinispan.icklequery.IndexedBook</indexed-entity> </indexed-entities> <property name="lucene_version">LUCENE_CURRENT</property> </indexing> </distributed-cache>
現在のInfinispanでは、インデックスに登録する対象のEntityは、設定として明示する方針となっています。また、設定自体に
ついては今回は「auto-config」を使ってはしょりました。
auto-configについては、こちら。
今回はDistributed Cacheを選んでいるので、LuceneのインデックスはInfinispanに保存されます。
テストデータは、内容的には先ほどと同じです。
val indexedBooks: Array[IndexedBook] = Array( IndexedBook("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320, "Spring"), IndexedBook("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104, "Spring"), IndexedBook("978-4774161631", "[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン", 3888, "全文検索"), IndexedBook("978-4048662024", "高速スケーラブル検索エンジン ElasticSearch Server", 6915, "全文検索"), IndexedBook("978-4774183169", "パーフェクト Java EE", 3456, "Java EE"), IndexedBook("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104, "Java EE") )
では、使ってみます。APIの使い方自体は先ほどと同じなのですが、「フィールド名:」のようなLuceneのQueryParserで
使えるような書式が指定できるようになります。
test("indexed entity Ickle Query, full text query") { withCache[String, IndexedBook]("indexedBookCache", 3) { cache => indexedBooks.foreach(b => cache.put(b.isbn, b)) val queryFactory = Search.getQueryFactory(cache) val query = queryFactory.create( """|from org.littlewings.infinispan.icklequery.IndexedBook b |where b.price < 5000 |and b.title: '全文検索'""".stripMargin) val resultBooks = query.list[IndexedBook]() resultBooks should have size (1) resultBooks.get(0).getIsbn should be("978-4774161631") resultBooks.get(0).getTitle should be("[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン") resultBooks.get(0).getPrice should be(3888) resultBooks.get(0).getCategory should be("全文検索") } }
なお、インデックスを有効にしていないCacheで、このようなQueryStringを指定すると例外がスローされます。
test("index-less Ickle Query, can't applied full text predicate") { withCache[String, Book]("bookCache", 3) { cache => books.foreach(b => cache.put(b.isbn, b)) val queryFactory = Search.getQueryFactory(cache) val thrown = the[ParsingException] thrownBy queryFactory.create( """|from org.littlewings.infinispan.icklequery.Book b |where b.title: '高速スケーラブル検索エンジン ElasticSearch Server'""".stripMargin) thrown.getMessage should be("ISPN028521: Full-text queries cannot be applied to property 'title' in type org.littlewings.infinispan.icklequery.Book unless the property is indexed and analyzed.") } }
反対にインデックスを有効かつAnalyzedなフィールドに対して「=」などの演算子を使用すると、こちらも例外がスローされます。
test("indexed entity Ickle Query, analyzed field can't applied eq") { withCache[String, IndexedBook]("indexedBookCache", 3) { cache => indexedBooks.foreach(b => cache.put(b.isbn, b)) val queryFactory = Search.getQueryFactory(cache) val thrown = the[ParsingException] thrownBy queryFactory.create( """|from org.littlewings.infinispan.icklequery.IndexedBook b |where b.price > 5000 |and b.title = '全文検索'""".stripMargin) thrown.getMessage should be("ISPN028522: No relational queries can be applied to property 'title' in type org.littlewings.infinispan.icklequery.IndexedBook since the property is analyzed.") } }
まあ、Queryの実行対象は意識しましょうね、と。
Full Text Queryに対しても、Aggregationが可能です。
test("indexed entity Ickle Query, full text query, aggregation") { withCache[String, IndexedBook]("indexedBookCache", 3) { cache => indexedBooks.foreach(b => cache.put(b.isbn, b)) val queryFactory = Search.getQueryFactory(cache) val query = queryFactory.create( """|select b.category, sum(b.price) |from org.littlewings.infinispan.icklequery.IndexedBook b |where b.title: (+'入門' and -'検索') |group by b.category |order by sum(b.price) desc""".stripMargin) val results = query.list[Array[AnyRef]]() results should have size (2) results.get(0)(0) should be("Spring") results.get(0)(1) should be(8424) results.get(1)(0) should be("Java EE") results.get(1)(1) should be(4104) } }
良いですね。
ざっと使い方の導入部としては、こんな感じでしょう。
もうちょっと中身を
で、このQueryStringですが、パーサーの部分はANTLRが使われているようです。
パーサーのもととなる定義は、こちらのようです。
https://github.com/infinispan/infinispan/tree/9.0.0.Final/object-filter/src/main/antlr3/org/infinispan/objectfilter/impl/ql/parse
また、最終的に生成されるQueryは、リフレクションベースのQueryならEmbeddedQueryになりますし
https://github.com/infinispan/infinispan/blob/9.0.0.Final/query/src/main/java/org/infinispan/query/dsl/embedded/impl/EmbeddedQuery.java
Full Text QueryであればEmbeddedLuceneQueryになります。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/query/src/main/java/org/infinispan/query/dsl/embedded/impl/EmbeddedLuceneQuery.java
構文的には同じですが、今までのQuery DSL同様、検索対象によってQueryも分かれます、と…。
また、パラメーターを当て込んでいる箇所は、このあたりになったりします。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/object-filter/src/main/java/org/infinispan/objectfilter/impl/syntax/parser/QueryRendererDelegateImpl.java
バインドパラメーターを見ている部分もあるので、中身を追う時には見ることになるんじゃないかなぁと。
困ったこと
バインドパラメーターが使えない箇所がある
最初にこれにハマりました。JDBCと同じノリでバインドパラメーターが使えると思いきや、使える演算子は現状だと限定的だったりします。
例えば、比較演算子はバインドパラメーターが使えますが、LIKEは使えません。また、Full Text系の演算子みはバインドパラメーターが
まったく使えなかったりします。
Full Text Queryの演算子で、適用されるフィールドがanalyzedかどうかが決まっている
RANGEで数値を指定した時に、見事にこれにハマりました…。基本的にFull Text系の演算子には対象のフィールドがanalyzedなことが
要求されます。
それがQueryのパース時に確認されるので、コケるのですが…これだと数値型のフィールドに対して、Full TextなRANGE Queryが投げられない…。
これはどうなんでしょう…。
とまあ、困ったのはこんなところです。
このあたりの話は、さっきも出てきましたがこちらを見ることになります。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/object-filter/src/main/java/org/infinispan/objectfilter/impl/syntax/parser/QueryRendererDelegateImpl.java
このあたりの内容はドキュメントがない現状では、ソースコードを見ることになりますね。
また、Queryをパースしている間の関連クラスはこのあたりのパッケージに入っているので、ANTLRの定義と合わせて追うことになるでしょう。
https://github.com/infinispan/infinispan/tree/9.0.0.Final/object-filter/src/main/java/org/infinispan/objectfilter/impl/syntax
https://github.com/infinispan/infinispan/tree/9.0.0.Final/object-filter/src/main/java/org/infinispan/objectfilter/impl/syntax/parser
まとめ
Infinispan 9から導入された、Ickle Queryを試してみました。
今までHibernate Search、もしくはInfinispan自身の提供するQuery DSLしかQueryを使う方法がなかったのですが、こうやってStringでも
表現できる実装が追加されたのは嬉しいなぁと思います。
もうちょっと習熟していかないと。
今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-ickle-query