CLOVER🍀

That was when it all began.

Infinispan Server 14.0の設定まわりのファイルをメモしておく

これは、なにをしたくて書いたもの?

Infinispan Serverの設定まわりを見ていて、いくつかポイントとなるファイルを目にしていたのでメモしておこうかなと。

対象はInfinispan 14.0.3.Finalとし、GitHub上のソースコードの位置で示していきます。

起動スクリプト

起動スクリプトは、こちらにあります。

https://github.com/infinispan/infinispan/tree/14.0.3.Final/server/runtime/src/main/server/bin

Infinispan Serverのbinディレクトリ内に入るファイルですね。

Infninispan Serverを起動する時はserver.shを使いますが、

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/server.sh

変数やオプションの内容を追う時は以下も見ることになると思います。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/common.sh

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/server.conf

読んでいると、JAVA_OPTSが使えたりするようなこともわかります。

設定ファイル

デフォルトの設定ファイルは、こちらにあります。

https://github.com/infinispan/infinispan/tree/14.0.3.Final/server/runtime/src/main/server/server/conf

Infinispan Serverのserver/confディレクトリにあるファイルですね。

たとえば、infinispan.xml

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/server/conf/infinispan.xml

設定ファイルはこれだけではなくて、全体のデフォルト設定もあります。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/resources/infinispan-defaults.xml

このinfinispan-defaults.xmlが読み込まれた後に、

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L298-L299

infinispan.xmlのような個別の設定が読み込まれるようになっています。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L315-L320

この時、いくつか以下のような${変数名}のような記述がありますが、これはシステムプロパティで指定することができます。

      <global-state>
         <persistent-location path="${infinispan.server.data.path}"/>
         <shared-persistent-location path="${infinispan.server.data.path}"/>
         <overlay-configuration-storage/>
      </global-state>

設定ファイルのパース時に解決される、という感じですね。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/core/src/main/java/org/infinispan/configuration/parsing/ParseUtils.java#L255

https://github.com/infinispan/infinispan/blob/14.0.3.Final/commons/all/src/main/java/org/infinispan/commons/util/StringPropertyReplacer.java

デフォルト値は、以下あたりを見るとだいたいわかるのですが。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L175-L188

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/common.sh

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/server.conf

infinispan.server.home.pathは、server.shに混じったりしています。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/server.sh#L18

server.confは、ここで読まれています。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/common.sh#L134-L140

ちなみに、Infinispan Serverはこちらのドキュメントを見るのがまずは筋だと思うのですが

Guide to Infinispan Server

infinispan.server.data.pathinfinispan.server.lib.pathといったシステムプロパティでの上書きなどは書かれておらず、それはこちらに
記述があったりします。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/README.adoc

Infinispan Serverにシステムプロパティを渡す時は、環境変数JAVA_OPTSに指定するか、server.shのあとにコマンドライン引数として
追加しても認識してくれます。

-Dで始まっている引数については、common.shがJavaVMへの引数としてハンドリングしてくれます。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/bin/common.sh#L45-L47

Infinispan Serverのmainクラス

Infinispan Serverの起動時に指定されるmainクラスは、Bootstrapクラスです。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Bootstrap.java

BootstrapクラスがServerクラスのインスタンスを生成するので、この2つを見ると設定情報の解決はだいたいわかるのではないかと思います。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java

デフォルト値はだいたいServerクラスに書かれていますしね。

また、server.shで起動時に指定可能なオプションのうち、システムプロパティでの指定に置き換えられそうなものに読めるものは、
Bootstrapクラス内で読み替えているようです。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Bootstrap.java#L83-L124

caches.xml

Infinispan Serverで動的にCacheを作成するとserver/dataディレクトリ内にcaches.xmlというファイルが作成されます。

こちらについてはInfinispan Server固有の話ではなく、infinispan-coreで決まっている挙動のようです。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/core/src/main/java/org/infinispan/globalstate/impl/OverlayLocalConfigurationStorage.java

設定ファイル、オプション等の列挙

最後に、デフォルトの設定ファイルの中身やinfinispan.xmlの中身、server.shのオプションを記載しておきましょう。

$の記述に着目するとどのあたりがシステムプロパティで上書きできそうかわかりますし、対応するserver.shのオプションがないかどうかも
見ておくとよいでしょう。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/resources/infinispan-defaults.xml

<?xml version="1.0" encoding="UTF-8"?>

<!-- N.B. This is *not* meant to be a usable cache configuration -->
<!-- This file supplies the internal configuration defaults per cache mode -->
<infinispan
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="urn:infinispan:config:14.0 https://infinispan.org/schemas/infinispan-config-14.0.xsd"
        xmlns="urn:infinispan:config:14.0">

   <cache-container shutdown-hook="DONT_REGISTER">
      <global-state>
         <persistent-location path="${infinispan.server.data.path}"/>
         <shared-persistent-location path="${infinispan.server.data.path}"/>
         <overlay-configuration-storage/>
      </global-state>
      <local-cache-configuration name="org.infinispan.LOCAL" statistics="true">
         <locking acquire-timeout="15000" striping="false" concurrency-level="1000"/>
      </local-cache-configuration>
      <replicated-cache-configuration name="org.infinispan.REPL_SYNC" remote-timeout="17500" configuration="org.infinispan.LOCAL">
         <state-transfer timeout="60000"/>
      </replicated-cache-configuration>
      <replicated-cache-configuration name="org.infinispan.REPL_ASYNC" mode="ASYNC" configuration="org.infinispan.LOCAL">
         <state-transfer timeout="60000"/>
      </replicated-cache-configuration>
      <distributed-cache-configuration name="org.infinispan.DIST_SYNC" remote-timeout="17500" configuration="org.infinispan.LOCAL">
         <state-transfer timeout="60000"/>
      </distributed-cache-configuration>
      <distributed-cache-configuration name="example.PROTOBUF_DIST" remote-timeout="17500" configuration="org.infinispan.LOCAL">
         <!-- Template for a queryable cache. Warning: may be removed in future versions -->
         <encoding media-type="application/x-protostream"/>
         <state-transfer timeout="60000"/>
      </distributed-cache-configuration>
      <distributed-cache-configuration name="org.infinispan.DIST_ASYNC" mode="ASYNC" configuration="org.infinispan.LOCAL">
         <state-transfer timeout="60000"/>
      </distributed-cache-configuration>
      <invalidation-cache-configuration name="org.infinispan.INVALIDATION_SYNC" remote-timeout="17500" configuration="org.infinispan.LOCAL"/>
      <invalidation-cache-configuration name="org.infinispan.INVALIDATION_ASYNC" mode="ASYNC" configuration="org.infinispan.LOCAL"/>
      <scattered-cache-configuration name="org.infinispan.SCATTERED_SYNC" remote-timeout="17500" configuration="org.infinispan.LOCAL"/>
   </cache-container>

</infinispan>

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/server/server/conf/infinispan.xml

<infinispan
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="urn:infinispan:config:14.0 https://infinispan.org/schemas/infinispan-config-14.0.xsd
                            urn:infinispan:server:14.0 https://infinispan.org/schemas/infinispan-server-14.0.xsd"
      xmlns="urn:infinispan:config:14.0"
      xmlns:server="urn:infinispan:server:14.0">

   <cache-container name="default" statistics="true">
      <transport cluster="${infinispan.cluster.name:cluster}" stack="${infinispan.cluster.stack:tcp}" node-name="${infinispan.node.name:}"/>
      <security>
         <authorization/>
      </security>
   </cache-container>

   <server xmlns="urn:infinispan:server:14.0">
      <interfaces>
         <interface name="public">
            <inet-address value="${infinispan.bind.address:127.0.0.1}"/>
         </interface>
      </interfaces>

      <socket-bindings default-interface="public" port-offset="${infinispan.socket.binding.port-offset:0}">
         <socket-binding name="default" port="${infinispan.bind.port:11222}"/>
         <socket-binding name="memcached" port="11221"/>
      </socket-bindings>

      <security>
         <credential-stores>
            <credential-store name="credentials" path="credentials.pfx">
               <clear-text-credential clear-text="secret"/>
            </credential-store>
         </credential-stores>
         <security-realms>
            <security-realm name="default">
               <!-- Uncomment to enable TLS on the realm -->
               <!-- server-identities>
                  <ssl>
                     <keystore path="application.keystore"
                               password="password" alias="server"
                               generate-self-signed-certificate-host="localhost"/>
                  </ssl>
               </server-identities-->
               <properties-realm groups-attribute="Roles">
                  <user-properties path="users.properties"/>
                  <group-properties path="groups.properties"/>
               </properties-realm>
            </security-realm>
         </security-realms>
      </security>

      <endpoints socket-binding="default" security-realm="default" />
   </server>
</infinispan>
$ bin/server.sh --help

Infinispan Server 14.0.3.Final (Flying Saucer)
Copyright (C) Red Hat Inc. and/or its affiliates and other contributors
License Apache License, v. 2.0. http://www.apache.org/licenses/LICENSE-2.0
Usage:
  -b, --bind-address=<address>  Binds the server endpoint to a specific address.
  -c, --server-config=<config>  Specifies a server configuration file. Defaults to `infinispan.xml`. Can be repeated, in which case the configurations are layered.
  -l, --logging-config=<config> Specifies a logging configuration file. Defaults to `log4j2.xml`.
  -g, --cluster-name=<name>     Sets the name of the cluster. Default set by configuration expression
  -h, --help                    Displays usage information and exits.
  -j, --cluster-stack=<name>    Specifies the JGroups stack for clustering. Default set by configuration expression
  -k, --cluster-address=<name>  Specifies the JGroups bind address for clustering.
  -n, --node-name=<name>        Sets the name of this node. Must be unique across the cluster.
  -o, --port-offset=<offset>    Adds a numeric offset to all ports.
  -p, --bind-port=<port>        Binds the server to a specific port. Defaults to `11222`.
  -s, --server-root=<path>      Specifies the root path for the server. Defaults to `server`.
  -v, --version                 Displays version information and exits.
  -D<name>=<value>              Sets a system property to the specified value.
  -P, --properties=<file>       Sets system properties from the specified file.

Infinispan 14.0の新しいインデックス用のアノテーションをHot Rodで試す

これは、なにをしたくて書いたもの?

Infinispanで全文検索インデックスを使用する場合、Cacheに保存するエントリーの各プロパティに対してHibernate Searchの
アノテーションを使用して設定を行っていました。

これがInfinispan 14.0で、Infinispan自身がインデックス用のアノテーションを提供するようになったようです。

Native Infinispan indexing annotations which finally replace the legacy Hibernate Query annotations we’ve used in past versions

Infinispan 14.0.0.Final

インデックスに関する変更は他にもあるようですが、今回はアノテーションに関して見ていこうと思います。

Index startup mode to determine what happens to indexes on cache start

Dynamic index schema updates allow you to evolve your schema at runtime with near-zero impact to your queries

Infinispanによる新しいインデックス用アノテーション

まず、Infinispanのクエリーやインデックスに関するドキュメントは、こちらです。

Querying Infinispan caches

Infinispanのクエリーのうち、インデックスを使ったものはHibernate SearchとApache Luceneによって実現されています。

クエリーは、Ickle QueryというJPQLライクな言語を使って記述します。

Querying Infinispan caches / Creating Ickle queries

今回の新しいアノテーションについてはドキュメントには書かれていないようなので、以下のブログやソースコードを頼りに見ていくことに
します。

Infinispan 14 indexing & query news

なお、この変更が入ったのは以下のPull Requestのようです。

ISPN-9893 New API indexing annotations by fax4ever · Pull Request #9942 · infinispan/infinispan · GitHub

ブログエントリーには、このようなアノテーションの紹介は行われているのですが、import文もないのでどのパッケージなのかも
わかりません。

@Indexed
public class Poem {
    private Author author;
    private String description;
    private Integer year;

    @Embedded(includeDepth = 2, structure = Structure.NESTED)
    public Author getAuthor() {
        return author;
    }

    @Text(projectable = true, analyzer = "whitespace", termVector = TermVector.WITH_OFFSETS)
    public String getDescription() {
        return description;
    }

    @Basic(projectable = true, sortable = true, indexNullAs = "1800")
    public Integer getYear() {
    return year;
    }
}

@Indexed
public class Author {
    private String name;

    public Author(String name) {
        this.name = name;
    }

    @Keyword(projectable = true, sortable = true, normalizer = "lowercase", indexNullAs = "unnamed", norms = false)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

説明は少し書かれています。

まず、先にも書きましたが。今回のアノテーションHibernate Searchのアノテーションを置き換えるInfinispanのインデックス用アノテーション
なります。Embedded、Remoteのどちらでも使えるようです。

We are going to replace Hibernate annotations with Infinispan indexing annotations. The new annotations can be used in the same way for both embedded and remote queries.

アノテーション自体にも触れています。

  • @Basic … 特に文字列変換処理を行わないフィールド
  • @KeywordStringフィールドにノーラマイザーを適用する
  • @TextStringフィールドにアナライザーを適用する

ソートやプロジェクションができるかどうかは、sortableprojectableで指定します。使用できない組み合わせもあり、たとえばsortable
アナライズを行う@Textは組み合わせることができません。

ネストしたオブジェクトをインデックスに保存する際には、@Embeddedアノテーションを使用します。この時、ネストしたオブジェクトを
どのようにインデックスに保存するかはstructure属性で指定します。NESTEDがデフォルトで、オブジェクトの構造を維持します。
つまり、ネストしたオブジェクトは別のインデックスエントリーとして扱われます。もうひとつはFLATTENEDで、ネストしたオブジェクトは
親オブジェクトの一部として保存されます。

Infinispanのソースコードから、該当のアノテーションを探してみましょう。org.infinispan.api.annotations.indexingパッケージ配下に
あるようです。

$ grep -rE 'Simplified version for Infinispan of|Infinispan version of' api/src/main/java/org/infinispan/api/annotations/indexing
api/src/main/java/org/infinispan/api/annotations/indexing/option/TermVector.java: * Simplified version for Infinispan of {@link org.hibernate.search.engine.backend.types.TermVector}
api/src/main/java/org/infinispan/api/annotations/indexing/option/Structure.java: * Simplified version for Infinispan of {@link org.hibernate.search.engine.backend.types.ObjectStructure}
api/src/main/java/org/infinispan/api/annotations/indexing/model/Point.java: * Simplified version for Infinispan of {@link org.hibernate.search.engine.spatial.GeoPoint}
api/src/main/java/org/infinispan/api/annotations/indexing/GeoCoordinates.java: * Infinispan version of {@link org.hibernate.search.mapper.pojo.bridge.builtin.annotation.GeoPointBinding}
api/src/main/java/org/infinispan/api/annotations/indexing/Longitude.java: * Infinispan version of {@link org.hibernate.search.mapper.pojo.bridge.builtin.annotation.Longitude}
api/src/main/java/org/infinispan/api/annotations/indexing/Text.java: * Simplified version for Infinispan of {@link org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField}
api/src/main/java/org/infinispan/api/annotations/indexing/Embedded.java: * Simplified version for Infinispan of {@link org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded}
api/src/main/java/org/infinispan/api/annotations/indexing/Latitude.java: * Infinispan version of {@link org.hibernate.search.mapper.pojo.bridge.builtin.annotation.Latitude}
api/src/main/java/org/infinispan/api/annotations/indexing/Keyword.java: * Simplified version for Infinispan of {@link org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField}
api/src/main/java/org/infinispan/api/annotations/indexing/Decimal.java: * Simplified version for Infinispan of {@link org.hibernate.search.mapper.pojo.mapping.definition.annotation.ScaledNumberField}
api/src/main/java/org/infinispan/api/annotations/indexing/Basic.java: * Simplified version for Infinispan of {@link org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField}
api/src/main/java/org/infinispan/api/annotations/indexing/Indexed.java: * Simplified version for Infinispan of {@link org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed}

https://github.com/infinispan/infinispan/tree/14.0.3.Final/api/src/main/java/org/infinispan/api/annotations/indexing

コメントを見ると、Hibernate SearchのアノテーションのInfinispan版であることが書かれていますね。

対応表を作ってみましょう。

Infinispanのアノテーション Hibernate Searchのアノテーション
org.infinispan.api.annotations.indexing.@Indexed org.hibernate.search.mapper.pojo.mapping.definition.annotation.@Indexed
org.infinispan.api.annotations.indexing.@Text org.hibernate.search.mapper.pojo.mapping.definition.annotation.@FullTextField
org.infinispan.api.annotations.indexing.@Basic org.hibernate.search.mapper.pojo.mapping.definition.annotation.@GenericField
org.infinispan.api.annotations.indexing.@Keyword org.hibernate.search.mapper.pojo.mapping.definition.annotation.@KeywordField
org.infinispan.api.annotations.indexing.@Decimal org.hibernate.search.mapper.pojo.mapping.definition.annotation.@ScaledNumberField
org.infinispan.api.annotations.indexing.@GeoCoordinates org.hibernate.search.mapper.pojo.bridge.builtin.annotation.@GeoPointBinding
org.infinispan.api.annotations.indexing.@Latitude org.hibernate.search.mapper.pojo.bridge.builtin.annotation.@Latitude
org.infinispan.api.annotations.indexing.@Longitude org.hibernate.search.mapper.pojo.bridge.builtin.annotation.@Longitude
org.infinispan.api.annotations.indexing.@Embedded org.hibernate.search.mapper.pojo.mapping.definition.annotation.@IndexedEmbedded
Infinispanのクラスやenum Hibernate Searchのクラスやenum
org.infinispan.api.annotations.indexing.model.Point org.hibernate.search.engine.spatial.GeoPoint
org.infinispan.api.annotations.indexing.option.Structure org.hibernate.search.engine.backend.types.ObjectStructure
org.infinispan.api.annotations.indexing.option.TermVector org.hibernate.search.engine.backend.types.TermVector

このあたりの説明は、Hibernate Searchのドキュメントを見るのがよいでしょう。

Infinispan側は、あとはテストコードを参考に。

https://github.com/infinispan/infinispan/tree/14.0.3.Final/query/src/test/java/org/infinispan/query/dsl/embedded/testdomain

https://github.com/infinispan/infinispan/tree/14.0.3.Final/query/src/test/java/org/infinispan/query/model

https://github.com/infinispan/infinispan/tree/14.0.3.Final/query/src/test/java/org/infinispan/query/api

https://github.com/infinispan/infinispan/tree/14.0.3.Final/query/src/test/java/org/infinispan/query/indexedembedded

https://github.com/infinispan/infinispan/tree/14.0.3.Final/query/src/test/java/org/infinispan/query/queries

https://github.com/infinispan/infinispan/tree/14.0.3.Final/query/src/test/java/org/infinispan/query/test

https://github.com/infinispan/infinispan/tree/14.0.3.Final/client/hotrod-client/src/test/java/org/infinispan/client/hotrod/annotation/model

今回は、新しいInifinispanのインデックス用アノテーションを使って、インデックスとクエリーを使ってみましょう。

少し脱線: Elasticsearchバックエンド

その前に、少し脱線して。

そういえば、Infinispan 9.0から実験的な機能としてElasticsearchをバックエンドにした検索機能がありましたが、11.0で削除されていた
みたいですね。

[ISPN-11646] Drop Elasticsearch support - Red Hat Issue Tracker

ISPN-11646 Drop Support for Elasticsearch by gustavocoding · Pull Request #8204 · infinispan/infinispan · GitHub

いや、けっこう前から気づいていたんですが、挙げる機会がなかったので…。

お題

今回のお題は、以下のようにしたいと思います。

  • Hot Rod Clientを使う(Remote Cacheを使う)
  • Cacheに格納するエンティティは、インデックス定義なし、ありの2種類を使う
    • Cacheもそれぞれに作成する
  • どちらのCacheにも、クエリーを投げてみる

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-135-generic", arch: "amd64", family: "unix"

Infinispan Serverは、172.18.0.2〜172.18.0.4の3つのノードで動作し、クラスターを構成しているものとします。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment Temurin-17.0.5+8 (build 17.0.5+8)
OpenJDK 64-Bit Server VM Temurin-17.0.5+8 (build 17.0.5+8, mixed mode, sharing)


$ bin/server.sh --version

Infinispan Server 14.0.3.Final (Flying Saucer)
Copyright (C) Red Hat Inc. and/or its affiliates and other contributors
License Apache License, v. 2.0. http://www.apache.org/licenses/LICENSE-2.0

起動コマンドは、こちら。

$ bin/server.sh \
    -b 0.0.0.0 \
    -Djgroups.tcp.address=$(hostname -i)

準備

Maven依存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-api</artifactId>
            <version>14.0.3.Final</version>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>14.0.3.Final</version>
        </dependency>
        <dependency>
           <groupId>org.infinispan</groupId>
           <artifactId>infinispan-remote-query-client</artifactId>
            <version>14.0.3.Final</version>
        </dependency>
        <dependency>
           <groupId>org.infinispan</groupId>
           <artifactId>infinispan-query-dsl</artifactId>
            <version>14.0.3.Final</version>
        </dependency>
        <dependency>
            <groupId>org.infinispan.protostream</groupId>
            <artifactId>protostream-processor</artifactId>
            <version>4.5.0.Final</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>14.0.3.Final</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.23.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

依存関係として、infinispan-apiを追加しておきます。また、Remote Queryを使うのでinfinispan-client-hotrodinfinispan-remote-query-client
infinispan-query-dslも使用します。

各Infinispan Serverには、それぞれ管理用ユーザー、アプリケーション用ユーザーを作成しておきます。

$ bin/cli.sh user create -g admin -p password ispn-admin
$ bin/cli.sh user create -g application -p password ispn-user

テストコードの雛形を作成する

動作確認は、テストコードで行います。まずはその雛形を書いておきましょう。

src/test/java/org/littlewings/infinispan/remote/query/RemoteQueryTest.java

package org.littlewings.infinispan.remote.query;

import java.util.List;
import java.util.function.Consumer;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.client.hotrod.Search;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.IndexStartupMode;
import org.infinispan.configuration.cache.IndexStorage;
import org.infinispan.query.dsl.Query;
import org.infinispan.query.dsl.QueryFactory;
import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class RemoteQueryTest {
    static String createUri(String userName, String password) {
        return String.format(
                "hotrod://%s:%s@172.18.0.2:11222,172.18.0.3:11222,172.18.0.4:11222"
                        + "?context-initializers=org.littlewings.infinispan.remote.query.EntitiesInitializerImpl",
                userName,
                password
        );
    }

    @BeforeAll
    static void setUpAll() {
        String uri = createUri("ispn-admin", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            // create permission
            manager.getConfiguration().getContextInitializers().forEach(serializationContextInitializer -> {
                RemoteCache<String, String> protoCache = manager.getCache(ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME);
                protoCache.put("entities.proto", serializationContextInitializer.getProtoFile());
            });

            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration indexLessDistCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            admin.getOrCreateCache("bookCache", indexLessDistCacheConfiguration);

            org.infinispan.configuration.cache.Configuration indexedDistCacheConfiguration =
                    new ConfigurationBuilder()
                            .clustering()
                            .cacheMode(CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            // indexing
                            .indexing()
                            .enable()
                            .addIndexedEntities("entity.IndexedBook")
                            .storage(IndexStorage.FILESYSTEM)
                            .path("index/indexedBookCache")
                            .startupMode(IndexStartupMode.REINDEX)
                            .reader().refreshInterval(0L)
                            .writer().commitInterval(1000)
                            .build();

            admin.getOrCreateCache("indexedBookCache", indexedDistCacheConfiguration);
        }
    }

    <K, V> void withCache(String cacheName, Consumer<RemoteCache<K, V>> func) {
        String uri = createUri("ispn-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCache<K, V> cache = manager.getCache(cacheName);

            func.accept(cache);
        }
    }

    // ここに、テストを書く!!

}

Cacheを作成する処理などがあるのですが、それは後で説明します。

インデックスなしでクエリーを使う

最初は、インデックスなしでクエリーを使ってみましょう。

エンティティの定義。

src/main/java/org/littlewings/infinispan/remote/query/Book.java

package org.littlewings.infinispan.remote.query;

import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;
import org.infinispan.protostream.descriptors.Type;

public class Book {
    @ProtoField(number = 1, type = Type.STRING)
    String isbn;

    @ProtoField(number = 2, type = Type.STRING)
    String title;

    @ProtoField(number = 3, type = Type.INT32, defaultValue = "0")
    int price;

    @ProtoFactory
    public static Book create(String isbn, String title, int price) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

MarshallingはProtoStreamとします。

SerializationContextInitializerインターフェースを継承したインターフェースを作成。

src/main/java/org/littlewings/infinispan/remote/query/EntitiesInitializer.java

package org.littlewings.infinispan.remote.query;

import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;

@AutoProtoSchemaBuilder(
        includeClasses = {Book.class},
        schemaFileName = "entities.proto",
        schemaFilePath = "proto/",
        schemaPackageName = "entity"
)
public interface EntitiesInitializer extends SerializationContextInitializer {
}

このインターフェースを作成し、依存関係にprotostream-processorを加えているとコンパイルした時にProtocol BuffersのIDLが
生成されるようになります。

target/classes/proto/entities.proto

// File name: entities.proto
// Generated from : org.littlewings.infinispan.remote.query.EntitiesInitializer

syntax = "proto2";

package entity;



message Book {

   optional string isbn = 1;

   optional string title = 2;

   optional int32 price = 3 [default = 0];
}

次に、テストコードに移ります。

接続URLの作成から。context-initializersパラメーターに、先ほど作成したSerializationContextInitializerインターフェースを継承したものから
生成された実装クラスのFQCNを指定する必要があります。

    static String createUri(String userName, String password) {
        return String.format(
                "hotrod://%s:%s@172.18.0.2:11222,172.18.0.3:11222,172.18.0.4:11222"
                        + "?context-initializers=org.littlewings.infinispan.remote.query.EntitiesInitializerImpl",
                userName,
                password
        );
    }

Cacheを作成しましょう。

    @BeforeAll
    static void setUpAll() {
        String uri = createUri("ispn-admin", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            // create permission
            manager.getConfiguration().getContextInitializers().forEach(serializationContextInitializer -> {
                RemoteCache<String, String> protoCache = manager.getCache(ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME);
                protoCache.put("entities.proto", serializationContextInitializer.getProtoFile());
            });

            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration indexLessDistCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            admin.getOrCreateCache("bookCache", indexLessDistCacheConfiguration);

            〜省略〜
        }
    }

生成されたProtocol BuffersのIDLをInfinispan Serverに登録します。

            // create permission
            manager.getConfiguration().getContextInitializers().forEach(serializationContextInitializer -> {
                RemoteCache<String, String> protoCache = manager.getCache(ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME);
                protoCache.put("entities.proto", serializationContextInitializer.getProtoFile());
            });

IDLを登録する方法は、Web UI、CLIREST API、Hot Rod ClientからAPI呼び出し、SerializationContextInitializerインターフェースの実装を
含んだJARファイルをInfinispan Serverにデプロイ…といくつか方法がありますが、今回はHot Rod ClientからAPI呼び出しで登録する
ことにしました。

Encoding Infinispan caches and marshalling data / Marshalling custom objects with ProtoStream / Creating serialization context initializers / Registering Protobuf schemas with Infinispan Server

登録したIDLは、CLIで確認することもできます。

[infinispan-server-29760@cluster//containers/default]> schema ls
[{"name":"entities.proto","error":null}]
[infinispan-server-29760@cluster//containers/default]> schema get entities.proto
[infinispan-server-29760@cluster//containers/default]> schema get entities.proto
// File name: entities.proto
// Generated from : org.littlewings.infinispan.remote.query.EntitiesInitializer

syntax = "proto2";

package entity;



message Book {

   optional string isbn = 1;

   optional string title = 2;

   optional int32 price = 3 [default = 0];
}

また、先ほどのコードで登録されるDistributed Cacheの定義は以下になります。

server/data/caches.xml

<?xml version="1.0"?>
<infinispan xmlns="urn:infinispan:config:14.0">
    <cache-container>
        <caches>
            <distributed-cache name="bookCache" mode="SYNC" remote-timeout="17500" statistics="true">
                <encoding>
                    <key media-type="application/x-protostream"/>
                    <value media-type="application/x-protostream"/>
                </encoding>
                <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
                <state-transfer timeout="60000"/>
            </distributed-cache>
        </caches>
    </cache-container>
</infinispan>

あとはデータを登録して、クエリーを投げるプログラムを書けばOKです。

    @Test
    public void indexLessQuery() {
        this.<String, Book>withCache("bookCache", cache -> {
            List<Book> books = List.of(
                    Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 5242),
                    Book.create("978-4048917353", "Redis入門 インメモリKVSによる高速データ管理", 3400),
                    Book.create("978-4798045733", "RDB技術者のためのNoSQLガイド", 3400),
                    Book.create("978-1785285332", "Getting Started with Hazelcast - Second Edition", 4129),
                    Book.create("978-1789347531", "Apache Ignite Quick Start Guide: Distributed data caching and processing made easy", 3476)
            );

            books.forEach(book -> cache.put(book.getIsbn(), book));

            assertThat(cache.size()).isEqualTo(5);

            QueryFactory queryFactory = Search.getQueryFactory(cache);
            Query<Book> query =
                    queryFactory.create("from entity.Book where price > :price order by price desc");
            query.setParameter("price", 4000);

            List<Book> results = query.execute().list();

            assertThat(results).hasSize(2);
            assertThat(results.get(0).getTitle()).isEqualTo("Infinispan Data Grid Platform Definitive Guide");
            assertThat(results.get(1).getTitle()).isEqualTo("Getting Started with Hazelcast - Second Edition");
        });
    }

Ickle Queryを使うためのAPIの紹介はこちら。

Querying Infinispan caches / Creating Ickle queries / Ickle queries

そして、インデックスを使わない場合のクエリーの構文はこちらです。

Querying Infinispan caches / Creating Ickle queries / Ickle query language syntax

今回のクエリーはDistributed Cacheに対してソートを行っていますが

            Query<Book> query =
                    queryFactory.create("from entity.Book where price > :price order by price desc");

インデックスがない状態でこのようなソートを行うと、Infinispan Server側でWARNログが出力されることになります。

2022-12-02 15:11:52,527 WARN  (blocking-thread--p3-t1) [org.infinispan.query.core.impl.BaseEmbeddedQuery] ISPN014827: Distributed sort not supported for non-indexed query 'from entity.Book where price > :price order by price desc'. Consider using an index for optimal performance.

パフォーマンスが良くないですよ、ということですね。

インデックスを定義してクエリーを実行する

続いては、インデックスを定義してクエリーを実行してみましょう。

エンティティの定義。

src/main/java/org/littlewings/infinispan/remote/query/IndexedBook.java

package org.littlewings.infinispan.remote.query;

import org.infinispan.api.annotations.indexing.Basic;
import org.infinispan.api.annotations.indexing.Indexed;
import org.infinispan.api.annotations.indexing.Keyword;
import org.infinispan.api.annotations.indexing.Text;
import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;
import org.infinispan.protostream.descriptors.Type;

@Indexed
public class IndexedBook {
    @Keyword(sortable = true)
    @ProtoField(number = 1, type = Type.STRING)
    String isbn;

    // デフォルトのAnalyzerはstandard
    @Text(analyzer = "standard")
    @ProtoField(number = 2, type = Type.STRING)
    String title;

    @Basic(sortable = true)
    @ProtoField(number = 3, type = Type.INT32, defaultValue = "0")
    int price;

    @ProtoFactory
    public static IndexedBook create(String isbn, String title, int price) {
        IndexedBook book = new IndexedBook();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

ここで、今回の主題であるInfinispanのインデックス用アノテーションが出てきます。

すでにアノテーション自体は紹介していますが、これらのアノテーションinfinispan-apiに含まれています。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-api</artifactId>
            <version>14.0.3.Final</version>
        </dependency>

SerializationContextInitializerインターフェースを継承したインターフェース。

src/main/java/org/littlewings/infinispan/remote/query/EntitiesInitializer.java

package org.littlewings.infinispan.remote.query;

import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;

@AutoProtoSchemaBuilder(
        includeClasses = {IndexedBook.class},
        schemaFileName = "entities.proto",
        schemaFilePath = "proto/",
        schemaPackageName = "entity"
)
public interface EntitiesInitializer extends SerializationContextInitializer {
}

生成されたProtocol BuffersのIDL。アノテーションの記述がコメントとして残っています。

target/classes/proto/entities.proto

// File name: entities.proto
// Generated from : org.littlewings.infinispan.remote.query.EntitiesInitializer

syntax = "proto2";

package entity;



/**
 * @Indexed
 */
message IndexedBook {

   /**
    * @Keyword(sortable=true)
    */
   optional string isbn = 1;

   /**
    * @Text(analyzer="standard")
    */
   optional string title = 2;

   /**
    * @Basic(sortable=true)
    */
   optional int32 price = 3 [default = 0];
}

Cacheの定義。

    @BeforeAll
    static void setUpAll() {
        String uri = createUri("ispn-admin", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            // create permission
            manager.getConfiguration().getContextInitializers().forEach(serializationContextInitializer -> {
                RemoteCache<String, String> protoCache = manager.getCache(ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME);
                protoCache.put("entities.proto", serializationContextInitializer.getProtoFile());
            });

            RemoteCacheManagerAdmin admin = manager.administration();

            〜省略〜

            org.infinispan.configuration.cache.Configuration indexedDistCacheConfiguration =
                    new ConfigurationBuilder()
                            .clustering()
                            .cacheMode(CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            // indexing
                            .indexing()
                            .enable()
                            .addIndexedEntities("entity.IndexedBook")
                            .storage(IndexStorage.FILESYSTEM)
                            .path("${infinispan.server.data.path}/index/indexedBookCache")
                            //.path("index/indexedBookCache")
                            .startupMode(IndexStartupMode.REINDEX)
                            .reader().refreshInterval(0L)
                            .writer().commitInterval(1000)
                            .build();

            admin.getOrCreateCache("indexedBookCache", indexedDistCacheConfiguration);
        }
    }

Infinispan Serverに登録されたProtocol BuffersのIDL。

[infinispan-server-48098@cluster//containers/default]> schema get entities.proto
// File name: entities.proto
// Generated from : org.littlewings.infinispan.remote.query.EntitiesInitializer

syntax = "proto2";

package entity;



/**
 * @Indexed
 */
message IndexedBook {

   /**
    * @Keyword(sortable=true)
    */
   optional string isbn = 1;

   /**
    * @Text(analyzer="standard")
    */
   optional string title = 2;

   /**
    * @Basic(sortable=true)
    */
   optional int32 price = 3 [default = 0];
}

Infinispan Server側で生成されるCacheの定義。

server/data/caches.xml

<?xml version="1.0"?>
<infinispan xmlns="urn:infinispan:config:14.0">
    <cache-container>
        <caches>
            <distributed-cache name="indexedBookCache" mode="SYNC" remote-timeout="17500" statistics="true">
                <encoding>
                    <key media-type="application/x-protostream"/>
                    <value media-type="application/x-protostream"/>
                </encoding>
                <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
                <indexing enabled="true" storage="filesystem" startup-mode="REINDEX" path="/opt/infinispan-server/server/data/index/indexedBookCache">
                    <index-writer commit-interval="1000"/>
                    <indexed-entities>
                        <indexed-entity>entity.IndexedBook</indexed-entity>
                    </indexed-entities>
                </indexing>
                <state-transfer timeout="60000"/>
            </distributed-cache>
        </caches>
    </cache-container>
</infinispan>

このあたりがインデックスの設定なのですが

                            // indexing
                            .indexing()
                            .enable()
                            .addIndexedEntities("entity.IndexedBook")
                            .storage(IndexStorage.FILESYSTEM)
                            .path("${infinispan.server.data.path}/index/indexedBookCache")
                            //.path("index/indexedBookCache")
                            .startupMode(IndexStartupMode.REINDEX)
                            .reader().refreshInterval(0L)
                            .writer().commitInterval(1000)

説明は、このあたりにありますね。

Querying Infinispan caches / Indexing Infinispan caches / Configuring Infinispan to index caches / Index configuration

インデックスを有効にして

                            .indexing()
                            .enable()

インデックス保存の対象とするエンティティを登録。

                            .addIndexedEntities("entity.IndexedBook")

インデックスの保存場所は、今回はファイルシステムとしました。

                            .storage(IndexStorage.FILESYSTEM)
                            .path("${infinispan.server.data.path}/index/indexedBookCache")

ちなみに、こう書いても保存場所は変わりません。

                            .storage(IndexStorage.FILESYSTEM)
                            .path("index/indexedBookCache")

この場合、${infinispan.server.data.path}からの相対パスとして解決されます。

                <indexing enabled="true" storage="filesystem" startup-mode="REINDEX" path="index/indexedBookCache">
                    <index-writer commit-interval="1000"/>
                    <indexed-entities>
                        <indexed-entity>entity.IndexedBook</indexed-entity>
                    </indexed-entities>
                </indexing>

こちらについては、後で少し書きます。

クエリーを使ったテストコード。

    @Test
    public void indexedQuery() {
        this.<String, IndexedBook>withCache("indexedBookCache", cache -> {
            List<IndexedBook> books = List.of(
                    IndexedBook.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 5242),
                    IndexedBook.create("978-4048917353", "Redis入門 インメモリKVSによる高速データ管理", 3400),
                    IndexedBook.create("978-4798045733", "RDB技術者のためのNoSQLガイド", 3400),
                    IndexedBook.create("978-1785285332", "Getting Started with Hazelcast - Second Edition", 4129),
                    IndexedBook.create("978-1789347531", "Apache Ignite Quick Start Guide: Distributed data caching and processing made easy", 3476)
            );

            books.forEach(book -> cache.put(book.getIsbn(), book));

            assertThat(cache.size()).isEqualTo(5);

            QueryFactory queryFactory = Search.getQueryFactory(cache);

            Query<IndexedBook> query1 =
                    queryFactory.create("from entity.IndexedBook where title: 'guide' and price: [4000 to *] order by price");

            List<IndexedBook> results1 = query1.execute().list();

            assertThat(results1).hasSize(1);
            assertThat(results1.get(0).getTitle()).isEqualTo("Infinispan Data Grid Platform Definitive Guide");

            Query<IndexedBook> query2 =
                    queryFactory.create("from entity.IndexedBook where isbn = '978-4048917353'");

            List<IndexedBook> results2 = query2.execute().list();

            assertThat(results2).hasSize(1);
            assertThat(results2.get(0).getTitle()).isEqualTo("Redis入門 インメモリKVSによる高速データ管理");

            Query<IndexedBook> query3 =
                    queryFactory.create("from entity.IndexedBook where (title: 'data' and title: 'guide') or price: [* to 3400] order by price desc, isbn asc");

            List<IndexedBook> results3 = query3.execute().list();

            assertThat(results3).hasSize(4);
            assertThat(results3.get(0).getTitle()).isEqualTo("Infinispan Data Grid Platform Definitive Guide");
            assertThat(results3.get(1).getTitle()).isEqualTo("Apache Ignite Quick Start Guide: Distributed data caching and processing made easy");
            assertThat(results3.get(2).getTitle()).isEqualTo("Redis入門 インメモリKVSによる高速データ管理");
            assertThat(results3.get(3).getTitle()).isEqualTo("RDB技術者のためのNoSQLガイド");
        });
    }

インデックスを有効にしているフィールドに対するクエリー構文は、こちら。

Querying Infinispan caches / Creating Ickle queries / Full-text queries

このあたりですね。

            Query<IndexedBook> query1 =
                    queryFactory.create("from entity.IndexedBook where title: 'guide' and price: [4000 to *] order by price");


            〜省略〜


            Query<IndexedBook> query3 =
                    queryFactory.create("from entity.IndexedBook where (title: 'data' and title: 'guide') or price: [* to 3400] order by price desc, isbn asc");

※ フルテキストクエリーに対しては、バインドパラメータは一部動作しないようなので、今回対象外にしています

@Keywordアノテーションはアナライズを行わないため、通常のクエリーの構文になります。

            Query<IndexedBook> query2 =
                    queryFactory.create("from entity.IndexedBook where isbn = '978-4048917353'");

また、ソートをしたいフィールドに対しては

            Query<IndexedBook> query1 =
                    queryFactory.create("from entity.IndexedBook where title: 'guide' and price: [4000 to *] order by price");


            〜省略〜


            Query<IndexedBook> query3 =
                    queryFactory.create("from entity.IndexedBook where (title: 'data' and title: 'guide') or price: [* to 3400] order by price desc, isbn asc");

明示的にsortabletrueにする必要があります。

    @Keyword(sortable = true)
    @ProtoField(number = 1, type = Type.STRING)
    String isbn;

    〜省略〜

    @Basic(sortable = true)
    @ProtoField(number = 3, type = Type.INT32, defaultValue = "0")
    int price;

アナライズを行う@Textのようなフィールドに対しては、ソートは有効にできません。

保存されたインデックスは、こちら。

$ ll server/data/index/indexedBookCache/entity.IndexedBook
total 24
drwxr-xr-x 2 xxxxx xxxxx 4096 Dec  2 15:32 ./
drwxr-xr-x 3 xxxxx xxxxx 4096 Dec  2 15:32 ../
-rw-r--r-- 1 xxxxx xxxxx  479 Dec  2 15:32 _0.cfe
-rw-r--r-- 1 xxxxx xxxxx 3632 Dec  2 15:32 _0.cfs
-rw-r--r-- 1 xxxxx xxxxx  373 Dec  2 15:32 _0.si
-rw-r--r-- 1 xxxxx xxxxx  154 Dec  2 15:32 segments_2
-rw-r--r-- 1 xxxxx xxxxx    0 Dec  2 15:32 write.lock

とりあえず、簡単にですがInfinispanの新しいインデックス用のアノテーションを試してみました。

2つのエンティティの内容を合わせた定義

ここまで2つのエンティティをそれぞれ別々に使いましたが、合わせた時の定義も載せておきます。オマケ的ですが。

src/main/java/org/littlewings/infinispan/remote/query/EntitiesInitializer.java

package org.littlewings.infinispan.remote.query;

import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;

@AutoProtoSchemaBuilder(
        includeClasses = {Book.class, IndexedBook.class},
        schemaFileName = "entities.proto",
        schemaFilePath = "proto/",
        schemaPackageName = "entity"
)
public interface EntitiesInitializer extends SerializationContextInitializer {
}
[infinispan-server-63828@cluster//containers/default]> schema get entities.proto
// File name: entities.proto
// Generated from : org.littlewings.infinispan.remote.query.EntitiesInitializer

syntax = "proto2";

package entity;



/**
 * @Indexed
 */
message IndexedBook {

   /**
    * @Keyword(sortable=true)
    */
   optional string isbn = 1;

   /**
    * @Text(analyzer="standard")
    */
   optional string title = 2;

   /**
    * @Basic(sortable=true)
    */
   optional int32 price = 3 [default = 0];
}


message Book {

   optional string isbn = 1;

   optional string title = 2;

   optional int32 price = 3 [default = 0];
}

こちらの2つのCacheを作成した際に、

            org.infinispan.configuration.cache.Configuration indexLessDistCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            admin.getOrCreateCache("bookCache", indexLessDistCacheConfiguration);

            org.infinispan.configuration.cache.Configuration indexedDistCacheConfiguration =
                    new ConfigurationBuilder()
                            .clustering()
                            .cacheMode(CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            // indexing
                            .indexing()
                            .enable()
                            .addIndexedEntities("entity.IndexedBook")
                            .storage(IndexStorage.FILESYSTEM)
                            .path("${infinispan.server.data.path}/index/indexedBookCache")
                            //.path("index/indexedBookCache")
                            .startupMode(IndexStartupMode.REINDEX)
                            .reader().refreshInterval(0L)
                            .writer().commitInterval(1000)
                            .build();

            admin.getOrCreateCache("indexedBookCache", indexedDistCacheConfiguration);

Infinispan Server側で生成されるCache定義。

server/data/caches.xml

<?xml version="1.0"?>
<infinispan xmlns="urn:infinispan:config:14.0">
    <cache-container>
        <caches>
            <distributed-cache name="bookCache" mode="SYNC" remote-timeout="17500" statistics="true">
                <encoding>
                    <key media-type="application/x-protostream"/>
                    <value media-type="application/x-protostream"/>
                </encoding>
                <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
                <state-transfer timeout="60000"/>
            </distributed-cache>
            <distributed-cache name="indexedBookCache" mode="SYNC" remote-timeout="17500" statistics="true">
                <encoding>
                    <key media-type="application/x-protostream"/>
                    <value media-type="application/x-protostream"/>
                </encoding>
                <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
                <indexing enabled="true" storage="filesystem" startup-mode="REINDEX" path="/opt/infinispan-server/server/data/index/indexedBookCache">
                    <index-writer commit-interval="1000"/>
                    <indexed-entities>
                        <indexed-entity>entity.IndexedBook</indexed-entity>
                    </indexed-entities>
                </indexing>
                <state-transfer timeout="60000"/>
            </distributed-cache>
        </caches>
    </cache-container>
</infinispan>

少し実装を追ってみる

今回はInfinispanの新しいインデックス用のアノテーションを見てきたのですが、これらはHibernate Searchとどのような関係があるのでしょうか?

答えは、このあたりにありそうです。

https://github.com/infinispan/infinispan/tree/14.0.3.Final/api/src/main/java/org/infinispan/api/common/annotations/indexing/_private

TextProcessorを例に見てみましょう。

package org.infinispan.api.common.annotations.indexing._private;

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.processing.PropertyMappingAnnotationProcessor;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.processing.PropertyMappingAnnotationProcessorContext;
import org.hibernate.search.mapper.pojo.mapping.definition.programmatic.PropertyMappingFullTextFieldOptionsStep;
import org.hibernate.search.mapper.pojo.mapping.definition.programmatic.PropertyMappingStep;
import org.infinispan.api.annotations.indexing.Text;

public class TextProcessor implements PropertyMappingAnnotationProcessor<Text> {

   @Override
   public void process(PropertyMappingStep mapping, Text annotation, PropertyMappingAnnotationProcessorContext context) {
      String name = annotation.name();
      PropertyMappingFullTextFieldOptionsStep fullTextField = (name.isEmpty()) ?
            mapping.fullTextField() : mapping.fullTextField(name);

      fullTextField.analyzer(annotation.analyzer());

      String searchAnalyzer = annotation.searchAnalyzer();
      if (!searchAnalyzer.isEmpty()) {
         fullTextField.searchAnalyzer(searchAnalyzer);
      }

      fullTextField.norms(Options.norms(annotation.norms()));
      fullTextField.termVector(Options.termVector(annotation.termVector()));
      fullTextField.projectable(Options.projectable(annotation.projectable()));
      fullTextField.searchable(Options.searchable(annotation.searchable()));
   }
}

https://github.com/infinispan/infinispan/blob/14.0.3.Final/api/src/main/java/org/infinispan/api/common/annotations/indexing/_private/TextProcessor.java

どうやら、Hibernate Searchのカスタムマッピングアノテーションという仕組みを使っているみたいですね。

Hibernate Search: Reference Documentation / Mapping Hibernate ORM entities to indexes / Custom mapping annotations

@TextアノテーションおよびTextProcessorの宣言を見ると、関連があるのがわかりますね。

@Documented
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Text.List.class)
@PropertyMapping(processor = @PropertyMappingAnnotationProcessorRef(type = TextProcessor.class, retrieval = BeanRetrieval.CONSTRUCTOR))
public @interface Text {


public class TextProcessor implements PropertyMappingAnnotationProcessor<Text> {

この仕組みがいつ動作しているかというと、フィールドにアノテーションが付与されているのを検出したタイミングのようですが、
Hot Rod Client自体はHibernate Searchには依存していないので、Infinispan Server内で動作していることになりますね。

というわけで、見た目はInfinispanオリジナルのインデックス用アノテーションなのですが、実際にはHibernate Searchの処理に変換されて
動作する、というのが裏舞台のようです。

また、アノテーションとは関係ないのですが、最初はインデックスの保存先を以下のように相対パスに指定するとどこに保存されるのかなと
思って試してみたのですが

                            .indexing()
                            .enable()
                            .addIndexedEntities("entity.IndexedBook")
                            .storage(IndexStorage.FILESYSTEM)
                            .path("index/indexedBookCache")

結果は、[Infinispan Serverのインストールディレクトリ]/server/dataディレクトリ内の相対パスとして解釈されました。

どういう解決になっているのかな?と思って見てみました。

以下を見ると、パス指定が絶対パスの場合はそのまま使い、相対パスの場合はGlobalStateConfiguration#persistentLocationからの
相対パスとして解釈されるようです。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/query/src/main/java/org/infinispan/query/impl/config/SearchPropertyExtractor.java#L157-L166

この設定を構築しているのはこちらのようですが、

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L307

Inifnispan Serverのデフォルト設定を読み込んでいるのはこちら。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L298-L299

設定ファイルを見ると、${infinispan.server.data.path}が指す値がデフォルト値になっています。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/resources/infinispan-defaults.xml#L12

その実際の値はというと、server/dataですね。

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L254

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L176

https://github.com/infinispan/infinispan/blob/14.0.3.Final/server/runtime/src/main/java/org/infinispan/server/Server.java#L179

インデックスを適当に試している時はメモリ上に保存していることが多かったので、今回ちょっと興味を持って見てみました。

まとめ

Infinispanの新しいインデックス用のアノテーションを試してみました。

実際のところはHibernate Searchの処理に読み替えられるので、特になにか新しい動作が加わったわけでもありません。なので、情報自体は
なかったのですがそれほど困らずに使えました。

Hibernate Searchとどういう関係にあるんだろう?と調べたりするところは、そこそこ時間がかかりましたけどね。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-query-infinispan-annotation