CLOVER🍀

That was when it all began.

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

InfinispanのApache SparkとのConnectorのドキュメントを読んでいて、あれっ?というものに気付きまして。

Can be ommited if entities are annotated with protobuf encoding information. Protobuf encoding is required to filter the RDD by Query

https://github.com/infinispan/infinispan-spark#supported-configurations

なにやら、Protocol Buffersの情報をアノテーションを使うことで省略できるという感じみたいです。アノテーションから、Protocol BuffersのIDLを生成できる雰囲気。

というわけで、ドキュメントが指しているHot Rod Clientのテストコードや、その他をちょっと見てみると

https://github.com/infinispan/infinispan/blob/8.1.0.Final/client/hotrod-client/src/test/java/org/infinispan/client/hotrod/marshall/ProtoStreamMarshallerWithAnnotationsTest.java#L39

https://github.com/infinispan/infinispan/blob/8.1.0.Final/client/hotrod-client/src/test/java/org/infinispan/client/hotrod/query/RemoteQueryWithProtostreamAnnotationsTest.java#L37

なるほど、ありました。

では、せっかくなのでちょっと試してみましょう。

なお、過去にRemote Queryを試した時は例外なくハマっているのですが、今回も例に漏れませんでした…。

以前の足跡)
InfinispanのRemote Queryを試してみる - CLOVER

Infinispan(Server)の検索機能を使ってみる - CLOVER

Infinispan ServerのダウンロードとCacheの作成

まずは、Infinispan Serverが必要です。ダウンロードページからInfinispan Serverをダウンロードして、zipファイルを展開しておきます。

$ unzip infinispan-server-8.1.0.Final-bin.zip
$ cd infinispan-server-8.1.0.Final/
$ bin/standalone.sh -c clustered.xml

続いて、Remote Queryを使うにはインデキシングの設定を有効にしたCacheが必要なので、Cacheを新たに作成します。

$ bin/ispn-cli.sh -c
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=indexedCache1:add(start=EAGER,mode=SYNC,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=indexedCache1:add(configuration=indexedCache1)
{"outcome" => "success"}
[standalone@localhost:9990 /] /subsystem=datagrid-infinispan/cache-container=clustered/distributed-cache=indexedCache2:add(configuration=indexedCache1)
{"outcome" => "success"}

ここまでできたら、Infinispan Serverを1度再起動します。

依存関係の定義

続いて、Hot Rod Client側の準備。sbtの定義は、以下のように行いました。
build.sbt

name := "remote-query-less-proto"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.7"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

parallelExecution in Test := false

libraryDependencies ++= Seq(
  "org.infinispan" % "infinispan-client-hotrod" % "8.1.0.Final",
  "org.infinispan" % "infinispan-remote-query-client" % "8.1.0.Final",
  "org.infinispan" % "infinispan-query-dsl" % "8.1.0.Final",
  "net.jcip" % "jcip-annotations" % "1.0" % "provided",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

Hot Rod Clientと、Query DSL、Remote Query Clientがあればよいみたいです。

Entity

Cacheに登録するEntityを作成してみます。

まずは、比較のために単純な実装から。
src/test/scala/org/littlewings/infinispan/remotequery/SimpleBook.scala

package org.littlewings.infinispan.remotequery

object SimpleBook {
  def apply(isbn: String, title: String, price: Int, summary: String): SimpleBook = {
    val book = new SimpleBook
    book.isbn = isbn
    book.title = title
    book.price = price
    book.summary = summary
    book
  }
}

@SerialVersionUID(1L)
class SimpleBook extends Serializable {
  var isbn: String = _
  var title: String = _
  var price: Int = _
  var summary: String = _
}

とりあえず、Serializableだけ実装したクラスです。

続いて、Protocol BuffersのIDLを実行時に生成するように定義したEntity。
src/test/scala/org/littlewings/infinispan/remotequery/Book.scala

package org.littlewings.infinispan.remotequery

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

object Book {
  def apply(isbn: String, title: String, price: Int, summary: String): Book = {
    val book = new Book
    book.isbn = isbn
    book.title = title
    book.price = price
    book.summary = summary
    book
  }
}

@ProtoDoc("@Indexed")
@ProtoMessage(name = "Book")
class Book {
  var isbn: String = _
  var title: String = _
  var price: Int = _
  var summary: String = _

  @ProtoDoc("@IndexedField")
  @ProtoField(number = 1, name = "isbn", `type` = Type.STRING)
  def getIsbn: String = isbn

  def setIsbn(isbn: String): Unit = this.isbn = isbn

  @ProtoDoc("@IndexedField")
  @ProtoField(number = 2, name = "title", `type` = Type.STRING)
  def getTitle: String = title

  def setTitle(title: String): Unit = this.title = title

  @ProtoDoc("@IndexedField(index = false, store=false)")
  @ProtoField(number = 3, name = "price", `type` = Type.INT32, required = true, defaultValue = "0")
  def getPrice: Int = price

  def setPrice(price: Int): Unit = this.price = price

  @ProtoDoc("@IndexedField")
  @ProtoField(number = 4, name = "summary", `type` = Type.STRING)
  def getSummary: String = summary

  def setSummary(summary: String): Unit = this.summary = summary
}

実は、Serializableが不要だったりします。

こちらの場合、クラス定義に@ProtoDocおよび@ProtoMessageアノテーション

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

getterに、以下のように定義します。

  @ProtoDoc("@IndexedField")
  @ProtoField(number = 1, name = "isbn", `type` = Type.STRING)
  def getIsbn: String = isbn

  @ProtoDoc("@IndexedField(index = false, store=false)")
  @ProtoField(number = 3, name = "price", `type` = Type.INT32, required = true, defaultValue = "0")
  def getPrice: Int = price

@ProtoMessageや@ProtoFieldでProtocol Buffersの主要なIDL定義を行い、付加情報を@ProtoDocで付与する感じみたいです。@Indexedとか入れていますしね。

※注
intのようなプリミティブ型の場合は、required = trueにしておかないと、値が欠落してしまうようです。自分でIDLやMarshallerを実装する場合には、このような挙動にはならないのですが…。

アノテーション内の値も指定できそうな感じです。

  @ProtoDoc("@IndexedField(index = false, store=false)")

アノテーションにどのような値を指定できるかは…ソースを読んで知るしかない感じ?

https://github.com/infinispan/protostream/blob/3.0.4.Final/core/src/main/java/org/infinispan/protostream/annotations/ProtoMessage.java
https://github.com/infinispan/protostream/blob/3.0.4.Final/core/src/main/java/org/infinispan/protostream/annotations/ProtoDoc.java
https://github.com/infinispan/protostream/blob/3.0.4.Final/core/src/main/java/org/infinispan/protostream/annotations/ProtoField.java

ちなみに、@ProtoFieldを非publicなフィールドに付与していたり、publicな引数なしのコンストラクタを持っていなかったりすると、実行時にエラーになります。

このあたりのチェックは、以下のコードに書いてあります。

https://github.com/infinispan/protostream/blob/3.0.4.Final/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java

テストコードを書く

それでは、テストコードを書いて動かしてみます。

まずは雛形から。
src/test/scala/org/littlewings/infinispan/remotequery/RemoteQuerySpec.scala

package org.littlewings.infinispan.remotequery

import org.infinispan.client.hotrod.configuration.ConfigurationBuilder
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.dsl.{Query, SortOrder}
import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants
import org.scalatest.{FunSpec, Matchers}

class RemoteQuerySpec extends FunSpec with Matchers {
  describe("RemoteQuery Spec") {
    // ここに、テストを書く!
  }

  protected def withRemoteCache[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
    )

    val cache = manager.getCache[K, V](cacheName)

    try {
      fun(cache)
      cache.clear()
      cache.stop()
    } finally {
      manager.stop()
    }
  }
}

RemoteCacheManagerの起動/終了を行うためのヘルパー付き…ですが、通常と違うのはRemoteCacheManagerを作成する際に、ConfigurationBuilderにProtoStreamMarshallerを渡すことですね。

    val manager = new RemoteCacheManager(
      new ConfigurationBuilder()
        .addServer
        .host("localhost")
        .port(11222)
        .marshaller(new ProtoStreamMarshaller)
        .build
    )

あと、お題は書籍にしているので、テストデータはこんな感じで。

  val sourceSimpleBooks: Seq[SimpleBook] =
    Array(
      SimpleBook("978-4798042169",
        "わかりやすいJavaEEウェブシステム入門",
        3456,
        "JavaEE7準拠。ショッピングサイトや業務システムで使われるJavaEE学習書の決定版!"),
      SimpleBook("978-4798124605",
        "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        4410,
        "エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。"),
      SimpleBook("978-4774127804",
        "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
        3200,
        "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。"),
      SimpleBook("978-4774161631",
        "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
        3780,
        "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。"),
      SimpleBook("978-4048662024",
        "高速スケーラブル検索エンジン ElasticSearch Server",
        3024,
        "Apache Solrを超える全文検索エンジンとして注目を集めるElasticSearch Serverの日本初の解説書です。多くのサンプルを用いた実践的入門書になっています。"),
      SimpleBook("978-1933988177",
        "Lucene in Action",
        6301,
        "New edition of top-selling book on the new version of Lucene. the coreopen-source technology behind most full-text search and Intelligent Web applications.")
    )

  val sourceBooks: Seq[Book] =
    Array(
      Book("978-4798042169",
        "わかりやすいJavaEEウェブシステム入門",
        3456,
        "JavaEE7準拠。ショッピングサイトや業務システムで使われるJavaEE学習書の決定版!"),
      Book("978-4798124605",
        "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        4410,
        "エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。"),
      Book("978-4774127804",
        "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
        3200,
        "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。"),
      Book("978-4774161631",
        "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
        3780,
        "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。"),
      Book("978-4048662024",
        "高速スケーラブル検索エンジン ElasticSearch Server",
        3024,
        "Apache Solrを超える全文検索エンジンとして注目を集めるElasticSearch Serverの日本初の解説書です。多くのサンプルを用いた実践的入門書になっています。"),
      Book("978-1933988177",
        "Lucene in Action",
        6301,
        "New edition of top-selling book on the new version of Lucene. the coreopen-source technology behind most full-text search and Intelligent Web applications.")
    )
単純なEntityの場合

最初は、単純なEntityの場合。

    it("less .proto file, simple class") {
      withRemoteCache[String, SimpleBook]("indexedCache1") { cache =>
        val thrown = the[IllegalArgumentException] thrownBy sourceSimpleBooks.foreach(b => cache.put(b.isbn, b))
        thrown.getMessage should include("No marshaller registered for class org.littlewings.infinispan.remotequery.SimpleBook")
      }
    }

実装としては単純ですが、Protocol BuffersのIDLやProtoStream用のMarshallerを指定していないため、実行には失敗します。

IDLを手動で作成せず、アノテーションで解決するEntityの場合。

続いて、Protocol BuffersのIDL生成をInfinispan側に任せる場合のテストコード。

    it("less .proto file, with annotation class") {
      withRemoteCache[String, Book]("indexedCache2") { cache =>
        val manager = cache.getRemoteCacheManager

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

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

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

        val queryFactory = Search.getQueryFactory(cache)
        val query: Query =
          queryFactory
            .from(classOf[Book])
            .having("title")
            .like("%Java%")
            .and
            .having("title")
            .like("%全文検索%")
            .toBuilder
            .orderBy("price", SortOrder.ASC)
            .build

        query.getResultSize should be(1)

        val books = query.list.asInstanceOf[java.util.List[Book]]

        books should have size 1
        books.get(0).title should be("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築")
        books.get(0).price should be(3200)
      }
    }

特徴的なのは、この部分ですね。

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

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

SerializationContextを取得した後、ProtoSchemaBuilderで生成する際のファイル名、対象のクラスなどを指定します。そして、Protocol Buffers用のCacheに結果を登録する、と。

この部分で、IDLの自動生成、SerializationContextへの生成したIDLの登録、IDLとEntityに対するMarshallerを一気に自動生成します。

https://github.com/infinispan/protostream/blob/3.0.4.Final/core/src/main/java/org/infinispan/protostream/annotations/ProtoSchemaBuilder.java

ちなみに、この時に生成されるIDLはこんな感じですね。

// File name: /book.proto
// Scanned classes:
//   org.littlewings.infinispan.remotequery.Book

package remote_query;


/*
@Indexed
*/
message Book {
   /*
   @IndexedField
   */
   optional string isbn = 1;
   /*
   @IndexedField
   */
   optional string title = 2;
   /*
   @IndexedField(index = false, store=false)
   */
   required int32 price = 3 [default = 0];
   /*
   @IndexedField
   */
   optional string summary = 4;
}

このコードを実行すると、クエリを組み立てて検索、そして結果を取得できることを確認できます。

        val queryFactory = Search.getQueryFactory(cache)
        val query: Query =
          queryFactory
            .from(classOf[Book])
            .having("title")
            .like("%Java%")
            .and
            .having("title")
            .like("%全文検索%")
            .toBuilder
            .orderBy("price", SortOrder.ASC)
            .build

        query.getResultSize should be(1)

        val books = query.list.asInstanceOf[java.util.List[Book]]

        books should have size 1
        books.get(0).title should be("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築")
        books.get(0).price should be(3200)

まとめ

実はこれをやるまでにだいぶてこずったのですが、とりあえず動かせてよかったです。
※けっこうつらかったです…

前にやった時は、IDLを書いてコンパイルしてサーバーJMXで送信するとかやったりしていたのですが、書き方さえわかってしまえば随分と短い内容で書けるようになったのかなと思います。

まあ、触れば毎度ハマるのですが、とりあえず使えるようになってよかったです。

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