これは、なにをしたくて書いたもの?
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
インデックスに関する変更は他にもあるようですが、今回はアノテーションに関して見ていこうと思います。
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のクエリーやインデックスに関するドキュメントは、こちらです。
Infinispanのクエリーのうち、インデックスを使ったものはHibernate SearchとApache Luceneによって実現されています。
クエリーは、Ickle QueryというJPQLライクな言語を使って記述します。
Querying Infinispan caches / Creating Ickle queries
今回の新しいアノテーションについてはドキュメントには書かれていないようなので、以下のブログやソースコードを頼りに見ていくことに
します。
Infinispan 14 indexing & query news
なお、この変更が入ったのは以下のPull Requestのようです。
ブログエントリーには、このようなアノテーションの紹介は行われているのですが、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
… 特に文字列変換処理を行わないフィールド@Keyword
…String
フィールドにノーラマイザーを適用する@Text
…String
フィールドにアナライザーを適用する
ソートやプロジェクションができるかどうかは、sortable
やprojectable
で指定します。使用できない組み合わせもあり、たとえば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}
コメントを見ると、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のドキュメントを見るのがよいでしょう。
- Hibernate Search / Reference Documentation / Getting started / Mapping
- Hibernate Search / Reference Documentation / Mapping Hibernate ORM entities to indexes / Mapping a property to an index field with @GenericField, @FullTextField, …
- Hibernate Search / Reference Documentation / Mapping Hibernate ORM entities to indexes / Mapping associated elements with @IndexedEmbedded
- Hibernate Search / Reference Documentation / Mapping Hibernate ORM entities to indexes / Mapping geo-point types
Infinispan側は、あとはテストコードを参考に。
今回は、新しいInifinispanのインデックス用アノテーションを使って、インデックスとクエリーを使ってみましょう。
少し脱線: Elasticsearchバックエンド
その前に、少し脱線して。
そういえば、Infinispan 9.0から実験的な機能としてElasticsearchをバックエンドにした検索機能がありましたが、11.0で削除されていた
みたいですね。
[ISPN-11646] Drop Elasticsearch support - Red Hat Issue Tracker
いや、けっこう前から気づいていたんですが、挙げる機会がなかったので…。
お題
今回のお題は、以下のようにしたいと思います。
- 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-hotrod
、infinispan-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、CLI、REST API、Hot Rod ClientからAPI呼び出し、SerializationContextInitializer
インターフェースの実装を
含んだJARファイルをInfinispan Serverにデプロイ…といくつか方法がありますが、今回はHot Rod ClientからAPI呼び出しで登録する
ことにしました。
登録した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)
説明は、このあたりにありますね。
インデックスを有効にして
.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");
明示的にsortable
をtrue
にする必要があります。
@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とどのような関係があるのでしょうか?
答えは、このあたりにありそうです。
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())); } }
どうやら、Hibernate Searchのカスタムマッピングアノテーションという仕組みを使っているみたいですね。
@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
からの
相対パスとして解釈されるようです。
この設定を構築しているのはこちらのようですが、
Inifnispan Serverのデフォルト設定を読み込んでいるのはこちら。
設定ファイルを見ると、${infinispan.server.data.path}
が指す値がデフォルト値になっています。
その実際の値はというと、server/data
ですね。
インデックスを適当に試している時はメモリ上に保存していることが多かったので、今回ちょっと興味を持って見てみました。
まとめ
Infinispanの新しいインデックス用のアノテーションを試してみました。
実際のところはHibernate Searchの処理に読み替えられるので、特になにか新しい動作が加わったわけでもありません。なので、情報自体は
なかったのですがそれほど困らずに使えました。
Hibernate Searchとどういう関係にあるんだろう?と調べたりするところは、そこそこ時間がかかりましたけどね。
今回作成したソースコードは、こちらに置いています。