CLOVER🍀

That was when it all began.

Infinispan 9のIckle Queryを、Remote Queryで試す

この前、Infinispan 9で追加されたIckle QueryをEmbedded Modeで試しました。

Infinispan 9で追加された、Ickle Queryを試す - CLOVER

このIckle Query、どうもRemote Queryでも使えそうな感じなので、試してみようと思います。

それはそれは、たくさんハマりましたが…。

ちなみにこの機能、Infinispanにはドキュメントはないのですが、JBoss Data Gridにはドキュメントがあるので、
こちらが参考になるかもしれません。

Chapter 20. Remote Querying - Red Hat Customer Portal

テーマ

基本的に、Embedded Modeの時と同じような内容をなぞっていきます。検索のテーマは、書籍とします。

使うCacheとQueryとしては、

  • 通常のCacheにEntityを登録してリフレクションベースのQueryを試す
  • インデキシングを有効にしたCacheに対してインデックス設定を入れたEntityを登録、Full Text Queryを試す

という2つのパターンを試してみたいと思います。

準備

まずは、Infinispan Serverが必要です。

今回は、Infinispan Server 9.0.1.Finalをダウンロードして展開、実行します。

$ wget http://downloads.jboss.org/infinispan/9.0.1.Final/infinispan-server-9.0.1.Final-bin.zip
$ unzip infinispan-server-9.0.1.Final-bin.zip

起動は、クラスタリングを前提にして行いましょう。ただ、今回のNode数はひとつとします。

$ cd infinispan-server-9.0.1.Final
$ bin/standalone.sh -c clustered.xml

続いて、Cacheの登録を行います。操作は管理CLIで行いますが、最初に管理用のユーザーを作成。「ispn-admin」とします。

$ bin/add-user.sh -u ispn-admin -p password

このユーザーを使って、管理CLIを起動します。

$ bin/ispn-cli.sh -c -u=ispn-admin -p=password
[standalone@localhost:9990 /] 

最初に、インデックスを利用しないCacheの作成。Distributed Cacheで、「bookCache」とします。

[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=bookCacheConfiguration:add(start=EAGER,mode=SYNC)
{"outcome" => "success"}
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/distributed-cache=bookCache:add(configuration=bookCacheConfiguration)
{"outcome" => "success"}

続いて、インデックスを有効にしたCacheの作成。同じくDistributed Cacheで、こちらは「indexedBookCache」とします。

[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=indexedBookCacheConfiguration:add(start=EAGER,mode=SYNC)
{"outcome" => "success"}
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=indexedBookCacheConfiguration/indexing=INDEXING:add(indexing=LOCAL,indexing-properties={default.directory_provider=ram,lucene_version=LUCENE_CURRENT})
{"outcome" => "success"}
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/distributed-cache=indexedBookCache:add(configuration=indexedBookCacheConfiguration)
{"outcome" => "success"}

特に変わった設定はしていません。インデックスの保存先はメモリ上とし、Analyzerなどもデフォルトです。

ここまでで、Infinispan Serverの準備はおしまいです。

続いて、アプリケーション側。ライブラリの依存関係としては、このくらいがあればOKです。

libraryDependencies ++= Seq(
  "org.infinispan" % "infinispan-client-hotrod" % "9.0.1.Final" % Compile,
  "org.infinispan" % "infinispan-remote-query-client" % "9.0.1.Final" % Compile,
  "org.infinispan" % "infinispan-query-dsl" % "9.0.1.Final" % Provided,
  "net.jcip" % "jcip-annotations" % "1.0" % Provided,
  "org.scalatest" %% "scalatest" % "3.0.3" % Test
)

ScalaTestは、テスト用です。今回はScalaで扱っているので、「jcip-annotations」は少なくともJavaで扱う際には
不要です。「infinispan-query-dsl」もしかしたら不要かもしれません。

テストコードの雛形

動作確認はテストコードを使って行いますが、雛形としては以下とします。
src/test/scala/org/littlewings/infinispan/icklequery/IckeQuerySpec.scala

package org.littlewings.infinispan.icklequery

import org.infinispan.client.hotrod.configuration.ConfigurationBuilder
import org.infinispan.client.hotrod.exceptions.HotRodClientException
import org.infinispan.client.hotrod.marshall.ProtoStreamMarshaller
import org.infinispan.client.hotrod.{RemoteCache, RemoteCacheManager, Search}
import org.infinispan.protostream.annotations.ProtoSchemaBuilder
import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants
import org.scalatest.{FunSuite, Matchers}

class IckeQuerySpec extends FunSuite with Matchers {
  // ここに、テストコードを書く!

  protected def withCache[K, V](cacheName: String)(fun: RemoteCache[K, V] => Unit): Unit = {
    val manager =
      new RemoteCacheManager(
        new ConfigurationBuilder()
          .addServer()
          .host("localhost")
          .port(11222)
          .marshaller(new ProtoStreamMarshaller)
          .build()
      )

    try {
      val cache = manager.getCache[K, V](cacheName)
      fun(cache)
      cache.stop()
    } finally {
      manager.stop()
    }
  }
}

Infinispan Serverに接続する部分については、共通化しました。

ちょっと困ったこと

Remote Queryを使うためには、Protocol BufferesのIDLをInfinispan Serverの内部Cacheに登録する必要があります。このCacheは、アクセス時に
動的に作成されるのですが、これがローカルアドレスからのアクセス以外だと認証必須となり、ちょっと今回は設定をいったん諦めて
Infinispan Serverと確認用のアプリケーションを同じホストで動作させています。

Hot Rodの認証の設定は、また今度確認したいと思います。

追記
このリモートアクセスの場合の認証必須の設定は、Infinispan 9.1.0.Final/9.0.3.Finalで削除されました。
※しかも、それ以前はどうやっても認証を通過できなかった…

[JDG-508] Missing schema_manager role, when attempting to register protofile to ___protobuf_metadata cache - JBoss Issue Tracker

[ISPN-7814] Remove auth check in CacheDecodeContext - JBoss Issue Tracker

ISPN-7814 Remove protected cache security check by galderz · Pull Request #5206 · infinispan/infinispan · GitHub

https://github.com/infinispan/infinispan/commit/61b3d835e4838d76425dffab532fa507d7954ca8

それほど効果がないと判断されたもよう…。

頑張って設定する場合は、このあたりを参考に…。

8.3. Role Mapping - Red Hat Customer Portal
8.4. Configuring Authentication and Role Mapping using JBoss EAP Login Modules - Red Hat Customer Portal
8.5. Configuring Red Hat JBoss Data Grid for Authorization - Red Hat Customer Portal
8.2. Permissions - Red Hat Customer Portal

JBoss Data Grid 7.0 - Remote Query via Hot Rod Java, Register protobuf marshaller and schema

設定するなら、こういう感じでしょう。

## ユーザー追加
$ bin/add-user.sh -u ispn-admin -p password
$ bin/add-user.sh -a -u ispn-user -p password
# グループに入れる場合
$ bin/add-user.sh -a -u ispn-user -p password -ro super


## 設定
$ bin/ispn-cli.sh -c -u=ispn-admin -p=password

[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/security=SECURITY:add()
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/security=SECURITY/authorization=AUTHORIZATION:add(mapper=org.infinispan.security.impl.IdentityRoleMapper)
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/security=SECURITY/authorization=AUTHORIZATION/role=ROLE:add(name=ispn-user,permissions=[ALL])
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan-endpoint/hotrod-connector=hotrod-connector/authentication=AUTHENTICATION:add(security-realm=ApplicationRealm)
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan-endpoint/hotrod-connector=hotrod-connector/authentication=AUTHENTICATION/sasl=SASL:add(server-name=myserver,mechanisms=[DIGEST-MD5])

リフレクションベースのQuery

Embedded Modeと同様、InfinispanのQuery DSLで扱えるQueryには、2種類あります。Cacheのindexingを有効にしている場合はFull Text Queryですが、
そうでない場合はリフレクションベースのQueryになります(全件スキャン)。

まずは、リフレクションベースのQueryから扱っていきたいと思います。

Cacheに登録するEntityは、こちら。
src/main/scala/org/littlewings/infinispan/icklequery/Book.scala

package org.littlewings.infinispan.icklequery

import org.infinispan.protostream.annotations.{ProtoField, ProtoMessage}
import org.infinispan.protostream.descriptors.Type

import scala.annotation.meta.beanGetter
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
  }
}

@ProtoMessage(name = "Book")
class Book {
  @BeanProperty
  @(ProtoField @beanGetter)(number = 1, name = "isbn", `type` = Type.STRING)
  var isbn: String = _

  @BeanProperty
  @(ProtoField @beanGetter)(number = 2, name = "title", `type` = Type.STRING)
  var title: String = _

  @BeanProperty
  @(ProtoField @beanGetter)(number = 3, name = "price", `type` = Type.INT32, defaultValue = "0")
  var price: Int = _

  @BeanProperty
  @(ProtoField @beanGetter)(number = 4, name = "category", `type` = Type.STRING)
  var category: String = _
}

Queryの使い方などはEmbedded Modeとそう変わらないのですが、Remote QueryとEmbedded ModeでのQueryで差がある部分を中心に説明していきたいと
思います。

Remote Queryを使うためには、Protocol BuffersのIDLをInfinispan Serverに登録する必要があります。直接IDLを書いてもいいのですが、
今回はIDLをEntityクラスに付与したアノテーションに自動生成させることにします。

以前、同じことはやったのですが、書き方はもうちょっと見直しました。

InfinispanのRemote Queryで、Protocol BuffersのIDLをアノテーションから生成して使う - CLOVER

ポイントとしては、@ProtoMessageアノテーションのnameに指定した値が、Ickle QueryのEntity名になるようです。

@ProtoMessage(name = "Book")
class Book {

あと、これはScalaで使っているからの話ですが、@ProtoFieldアノテーションは、@BeanPropertyで生成されるgetterに付与されるように
@beanGetterアノテーションを併用しています。
※@ProtoFieldアノテーションをフィールドに付与する場合、非publicなフィールドには付与できないからです

  @BeanProperty
  @(ProtoField @beanGetter)(number = 1, name = "isbn", `type` = Type.STRING)
  var isbn: 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")
  )

では、Ickle QueryをRemote Queryで使ってみます。

  test("index-less simple Ickle Query") {
    withCache[String, Book]("bookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("book.proto")
          .addClass(classOf[Book])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("book.proto", idl)

      books.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|from 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).isbn should be("978-4048662024")
      resultBooks.get(0).title should be("高速スケーラブル検索エンジン ElasticSearch Server")
      resultBooks.get(0).price should be(6915)
      resultBooks.get(0).category should be("全文検索")
    }
  }

後半はEmbedded Modeで使う場合とほとんど同じなのですが、Remote Queryの場合は先にProtocol BufferesのIDLをInfinispan Serverに登録しておく
必要があります。

IDLは、RemoteCacheManagerとEntityクラスを使用して生成します。

      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("book.proto")
          .addClass(classOf[Book])
          .build(context)

生成したIDLは、Protocol Bufferesのメタデータを登録する専用のCacheに登録します。

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("book.proto", idl)

このCacheなのですが、先にも書いていますがアクセス時にInfinispan Server側で生成されるものなのですが、ローカルアクセス以外からだと
認証がかかっていないとエラーになります。今回は、ここを端折っています。

あとはデータを登録して、Queryを実行すればOKです。

      books.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|from Book b
             |where b.price > 5000
             |and b.title = '高速スケーラブル検索エンジン ElasticSearch Server'""".stripMargin)

      val resultBooks = query.list[Book]()

この時、Queryに指定するEntityの名前は@ProtoMessageアノテーションで指定したnameの値となります。Embedded Modeの場合は、クラスのFQCNでした。

        queryFactory.create(
          """|from Book b

このあたりを把握すれば、Embedded Modeとそう変わらない感じで使うことができると思います。

Queryへのパラメーターの適用や

  test("index-less simple Ickle Query, parameterized") {
    withCache[String, Book]("bookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("book.proto")
          .addClass(classOf[Book])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("book.proto", idl)

      books.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|from 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("全文検索")
    }
  }

Aggregationsも可能です。

  test("index-less simple Ickle Query, aggregation") {
    withCache[String, Book]("bookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("book.proto")
          .addClass(classOf[Book])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("book.proto", idl)

      books.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|select b.category, sum(b.price)
             |from 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.infinispan.protostream.annotations.{ProtoDoc, ProtoField, ProtoMessage}
import org.infinispan.protostream.descriptors.Type

import scala.annotation.meta.beanGetter
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
  }
}

@ProtoDoc("@Indexed")
@ProtoMessage(name = "IndexedBook")
class IndexedBook {
  @BeanProperty
  @(ProtoDoc @beanGetter)("@Field")
  @(ProtoField @beanGetter)(number = 1, name = "isbn", `type` = Type.STRING)
  var isbn: String = _

  @BeanProperty
  @(ProtoDoc @beanGetter)("@Field(analyze = Analyze.YES)")
  @(ProtoField @beanGetter)(number = 2, name = "title", `type` = Type.STRING)
  var title: String = _

  @BeanProperty
  @(ProtoDoc @beanGetter)("@Field")
  @(ProtoField @beanGetter)(number = 3, name = "price", `type` = Type.INT32, defaultValue = "0")
  var price: Int = _

  @BeanProperty
  @(ProtoDoc @beanGetter)("@Field")
  @(ProtoField @beanGetter)(number = 4, name = "category", `type` = Type.STRING)
  var category: String = _
}

先ほどのリフレクションベースで使っていたEntityと異なる点として、@ProtoDocアノテーションを使用して@Indexedを付与しています。Hibernate Searchの
アノテーションでしょうね。

@ProtoDoc("@Indexed")
@ProtoMessage(name = "IndexedBook")
class IndexedBook {

フィールドに対しては、@ProtoDocアノテーションで@Fieldを付与します。getterに付与されるように、@beanGetterアノテーションを併用しています。

  @BeanProperty
  @(ProtoDoc @beanGetter)("@Field")
  @(ProtoField @beanGetter)(number = 1, name = "isbn", `type` = Type.STRING)
  var isbn: String = _

また、デフォルトではAnalyzeされないようなので、Analyzeしたいフィールドに対しては明示的に「analyze = Analyze.YES」を付与する必要があるみたいです。

  @BeanProperty
  @(ProtoDoc @beanGetter)("@Field(analyze = Analyze.YES)")
  @(ProtoField @beanGetter)(number = 2, name = "title", `type` = Type.STRING)
  var title: String = _

テストデータは、こちら。

  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")
  )

では、使ってみます。最初にリフレクションベースでQueryを書いた時と、そう大きくは変わりません。

  test("indexed entity Ickle Query, full text query") {
    withCache[String, IndexedBook]("indexedBookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("indexed-book.proto")
          .addClass(classOf[IndexedBook])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("indexed-book.proto", idl)

      indexedBooks.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|from 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("全文検索")
    }
  }

全文検索用の演算子が使えるようになっているくらいです。

             |and b.title: '全文検索'""".stripMargin)

範囲検索も可能です。

  test("indexed entity Ickle Query, full text query with range") {
    withCache[String, IndexedBook]("indexedBookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("indexed-book.proto")
          .addClass(classOf[IndexedBook])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("indexed-book.proto", idl)

      indexedBooks.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|from IndexedBook b
             |where b.price: [6000 TO 7000]
             |and b.title: '検索'""".stripMargin)

      val resultBooks = query.list[IndexedBook]()
      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("全文検索")
    }
  }

Embedded Queryの時と同様、インデックスを有効にしていないCacheおよびEntityに対して、Full Text Queryを使うと例外がスローされます。

  test("index-less Ickle Query, can't applied full text predicate") {
    withCache[String, Book]("bookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("book.proto")
          .addClass(classOf[Book])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("book.proto", idl)

      books.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|from Book b
             |where b.title: '高速スケーラブル検索エンジン ElasticSearch Server'""".stripMargin)

      val thrown =
        the[HotRodClientException] thrownBy query.list[Book]()

      thrown.getMessage should be("org.infinispan.objectfilter.ParsingException: ISPN028521: Full-text queries cannot be applied to property 'title' in type Book unless the property is indexed and analyzed.")
    }
  }

反対に、インデックスを有効かつAnalyzedなフィールドに対して「=」などの演算子を使用すると、例外がスローされるのもEmbedded Modeと同じです。

  test("indexed entity Ickle Query, analyzed field can't applied eq") {
    withCache[String, IndexedBook]("indexedBookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("indexed-book.proto")
          .addClass(classOf[IndexedBook])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("indexed-book.proto", idl)

      indexedBooks.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
            """|from IndexedBook b
               |where b.price > 5000
               |and b.title = '全文検索'""".stripMargin)

      val thrown =
        the[HotRodClientException] thrownBy query.list[IndexedBook]()
      thrown.getMessage should be("org.infinispan.objectfilter.ParsingException: ISPN028522: No relational queries can be applied to property 'title' in type IndexedBook since the property is analyzed.")
    }
  }

Aggregationsについては、ふつうに使えます。

  test("indexed entity Ickle Query, full text query, aggregation") {
    withCache[String, IndexedBook]("indexedBookCache") { cache =>
      val manager = cache.getRemoteCacheManager

      val context = ProtoStreamMarshaller.getSerializationContext(manager)
      val protoSchemaBuilder = new ProtoSchemaBuilder
      val idl =
        protoSchemaBuilder
          .fileName("indexed-book.proto")
          .addClass(classOf[IndexedBook])
          .build(context)

      val metaCache =
        manager.getCache[String, AnyRef](ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME)
      metaCache.put("indexed-book.proto", idl)

      indexedBooks.foreach(b => cache.put(b.isbn, b))

      val queryFactory = Search.getQueryFactory(cache)
      val query =
        queryFactory.create(
          """|select b.category, sum(b.price)
             |from 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)
    }
  }

Embedded Modeといくらか違いはありますが、けっこう似た感じで使うことができました。

まとめ

Infinispan 9から導入されたIckle Queryを、Remote Queryで試してみました。

Remote Queryは、Protol Bufferesを使う関係上ハマりどころもそれなりにあるのですが、Ickle Queryが使えるのは便利かなと思います。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-ickle-query