これは、なにをしたくて書いたもの?
QuarkusのInfinispan Client Extensionを使って、Queryを実行するのを試してみようかなと。
久しぶりに、InfinispanのRemote Queryを使ってみましょう、と。
お題
QuarkusのInfinispan Client ExtensionのQueryingのパートは、えらくあっさりしています。
ProtoStreamMarshallerを作成して、依存関係に「infinispan-query-dsl」を追加し、あとはInfinispanのドキュメントを参照してね、と。
InfinispanのClient/Server Modeでは、Remote Queryを使います。
ここでは、Query DSLとIckle Queryの2つを使うことができます。ドキュメント上は、Embedded Modeの方しかAPIの記載はしっかりと
書いていないですが…。
Remote Queryを使う時には、インデックスの有無、さらにインデックス有効時のAnalyzeの有効有無で利用できるAPIが異なります。
今回は、これらを網羅する以下の組み合わせで試していこうと思います。
- インデックスなし
- Query DSL
- Ickle Query
- インデックスあり、Analyzeなし
- Query DSL
- Ickle Query
- インデックスあり、Analyzeあり
- Ickle Query
インデックスを有効にして、さらにAnalyzeを有効にしておくとQuery DSLとは(Analyzedなフィールドを検索条件に使用する場合は)
併用不可になるので…。
お題は、書籍で。
各パターンごとに、以下をまとめて載せていく感じにします。
- Protocol BuffersのIDL
- Entity
- MarshallerとProducerの定義
- Queryを実行したりする、JAX-RSリソースクラス
- Infinispan ServerのCache定義
- 動作確認
環境
今回の環境は、こちら。
$ java -version openjdk version "1.8.0_212" OpenJDK Runtime Environment (build 1.8.0_212-8u212-b03-0ubuntu1.18.04.1-b03) OpenJDK 64-Bit Server VM (build 25.212-b03, mixed mode) $ mvn -version Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 1.8.0_212, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-54-generic", arch: "amd64", family: "unix" $ $GRAALVM_HOME/bin/native-image --version GraalVM Version 19.0.2 CE
Infinispan Serverは、利用するQuarkus 0.18.0に合わせて、10.0.0 Beta3を使います。
起動は以下のコマンドで、1台ですがクラスタリングを有効に。また、動作しているInfinispan ServerのIPアドレスは172.17.0.2とします。
$ bin/standalone.sh \ -Djboss.bind.address=`hostname -i` \ -c clustered.xml
準備
まずは、QuarkusのMaven Pluginを使って、プロジェクトを作成します。
$ mvn io.quarkus:quarkus-maven-plugin:0.18.0:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=infinispan-client-query \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions="resteasy-jsonb,infinispan-client"
Extensionとしては、RESTEasy JSONBと、Infinispan Clientを指定。
あと、InfinispanのQueryを使うので、QuarkusのInfinispan Client Extensionのドキュメントに従い、「infinispan-query-dsl」の
依存関係を追加しておきます。
<dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-query-dsl</artifactId> </dependency>
あとは、このプロジェクト内でソースコードを作成していきます。
$ cd infinispan-client-query
設定
Quarkusの設定ファイルには、 src/main/resources/application.properties
# Configuration file # key = value quarkus.infinispan-client.server-list=172.17.0.2:11222
インデックスなしの場合
まずは、インデックスを使用しない場合から。
Protocol BuffersのIDL。
src/main/resources/META-INF/grid-entity.proto
package org.littlewings.quarkus.infinispan; option indexed_by_default = false; message Book { required string isbn = 1; required string title = 2; required int32 price = 3; required string tag = 4; }
「indexed_by_default」というのは、インデックスが有効なCacheを作成した場合に、デフォルトで各フィールドに対してインデックスを
作成しようとするらしく、Infinispan Server側で警告されます。
Indexing of Protobuf encoded entries
ここではインデックスが有効なCacheは作成しませんが、これ以降のCacheではインデックスは有効にしていくので、今回は
一律このオプションを付与していくことにします。
続いて、EntityやMarshallerですが、ここは割と定型的です。
Entity。
src/main/java/org/littlewings/quarkus/infinispan/Book.java
package org.littlewings.quarkus.infinispan; public class Book { private String isbn; private String title; private int price; private String tag; // getter/setterは省略 }
Marshaller。
src/main/java/org/littlewings/quarkus/infinispan/BookMarshaller.java
package org.littlewings.quarkus.infinispan; import java.io.IOException; import org.infinispan.protostream.MessageMarshaller; public class BookMarshaller implements MessageMarshaller<Book> { @Override public Book readFrom(ProtoStreamReader reader) throws IOException { Book book = new Book(); book.setIsbn(reader.readString("isbn")); book.setTitle(reader.readString("title")); book.setPrice(reader.readInt("price")); book.setTag(reader.readString("tag")); return book; } @Override public void writeTo(MessageMarshaller.ProtoStreamWriter writer, Book book) throws IOException { writer.writeString("isbn", book.getIsbn()); writer.writeString("title", book.getTitle()); writer.writeInt("price", book.getPrice()); writer.writeString("tag", book.getTag()); } @Override public Class<? extends Book> getJavaClass() { return Book.class; } @Override public String getTypeName() { return "org.littlewings.quarkus.infinispan.Book"; } }
MarshallerのProducer。
src/main/java/org/littlewings/quarkus/infinispan/MarshallerFactory.java
package org.littlewings.quarkus.infinispan; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import org.infinispan.protostream.MessageMarshaller; @Dependent public class MarshallerFactory { @Produces @ApplicationScoped public MessageMarshaller bookMarshaller() { return new BookMarshaller(); } }
書籍情報を含めたCacheを扱う、JAX-RSリソースクラス。
src/main/java/org/littlewings/quarkus/infinispan/BookResource.java
package org.littlewings.quarkus.infinispan; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import io.quarkus.infinispan.client.runtime.Remote; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.Search; import org.infinispan.query.dsl.Expression; import org.infinispan.query.dsl.FilterConditionContextQueryBuilder; import org.infinispan.query.dsl.Query; import org.infinispan.query.dsl.QueryBuilder; import org.infinispan.query.dsl.QueryFactory; import org.infinispan.query.dsl.SortOrder; import org.jboss.logging.Logger; @Path("book") public class BookResource { Logger logger = Logger.getLogger(BookResource.class); @Inject @Remote("bookCache") RemoteCache<String, Book> bookCache; @PUT @Path("{isbn}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.TEXT_PLAIN) public String register(@PathParam("isbn") String isbn, Book book) { bookCache.put(isbn, book); return "OK!!"; } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Book findByIsbn(@PathParam("isbn") String isbn) { return bookCache.get(isbn); } @GET @Path("search-dsl") @Produces(MediaType.APPLICATION_JSON) public List<Book> searchByQueryDsl(@QueryParam("title") String title, @QueryParam("price") Integer price, @QueryParam("tag") String tag) { QueryFactory qf = Search.getQueryFactory(bookCache); QueryBuilder qb = qf.from(Book.class); FilterConditionContextQueryBuilder cqb = null; Map<String, Object> parameters = new HashMap<>(); if (title != null) { cqb = qb.having("title").like("%" + title + "%"); } if (price != null) { if (cqb != null) { cqb = cqb.and().having("price").gte(Expression.param("price")); } else { cqb = qb.having("price").gte(Expression.param("price")); } parameters.put("price", price); } if (tag != null) { if (cqb != null) { cqb = cqb.and().having("tag").equal(Expression.param("tag")); } else { cqb = qb.having("tag").equal(Expression.param("tag")); } parameters.put("tag", tag); } cqb.orderBy("price", SortOrder.DESC); Query query = cqb.build(); if (!parameters.isEmpty()) { query.setParameters(parameters); } logger.infof("query = %s", query.getQueryString()); return query.list(); } @GET @Path("search-ickle") @Produces(MediaType.APPLICATION_JSON) public List<Book> searchByIckleQuery(@QueryParam("title") String title, @QueryParam("price") Integer price, @QueryParam("tag") String tag) { QueryFactory qf = Search.getQueryFactory(bookCache); List<String> conditions = new ArrayList<>(); Map<String, Object> parameters = new HashMap<>(); if (title != null) { conditions.add("title like :title"); parameters.put("title", "%" + title + "%"); } if (price != null) { conditions.add("price >= :price"); parameters.put("price", price); } if (tag != null) { conditions.add("tag = :tag"); parameters.put("tag", tag); } Query query = qf.create( String.format( "from %s where %s order by price desc", Book.class.getName(), String.join(" and ", conditions) ) ); query.setParameters(parameters); logger.infof("query = %s", query.getQueryString()); return query.list(); } }
こちらは、少し解説を。
まず、Cacheのインジェクションと、データ登録、1件取得用のメソッドを作成。Cacheの名前は、「bookCache」とします。
@Inject @Remote("bookCache") RemoteCache<String, Book> bookCache; @PUT @Path("{isbn}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.TEXT_PLAIN) public String register(@PathParam("isbn") String isbn, Book book) { bookCache.put(isbn, book); return "OK!!"; } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Book findByIsbn(@PathParam("isbn") String isbn) { return bookCache.get(isbn); }
@GET @Path("search-dsl") @Produces(MediaType.APPLICATION_JSON) public List<Book> searchByQueryDsl(@QueryParam("title") String title, @QueryParam("price") Integer price, @QueryParam("tag") String tag) { QueryFactory qf = Search.getQueryFactory(bookCache); QueryBuilder qb = qf.from(Book.class); FilterConditionContextQueryBuilder cqb = null; Map<String, Object> parameters = new HashMap<>(); if (title != null) { cqb = qb.having("title").like("%" + title + "%"); } if (price != null) { if (cqb != null) { cqb = cqb.and().having("price").gte(Expression.param("price")); } else { cqb = qb.having("price").gte(Expression.param("price")); } parameters.put("price", price); } if (tag != null) { if (cqb != null) { cqb = cqb.and().having("tag").equal(Expression.param("tag")); } else { cqb = qb.having("tag").equal(Expression.param("tag")); } parameters.put("tag", tag); } cqb.orderBy("price", SortOrder.DESC); Query query = cqb.build(); if (!parameters.isEmpty()) { query.setParameters(parameters); } logger.infof("query = %s", query.getQueryString()); return query.list(); }
titleはLIKE検索、priceは条件以上の値、tagは等値で検索条件を付与。QueryStringに指定されたものだけ指定します…が、全部の条件が
指定されていない場合は考慮していません…。
if (title != null) { cqb = qb.having("title").like("%" + title + "%"); } if (price != null) { if (cqb != null) { cqb = cqb.and().having("price").gte(Expression.param("price")); } else { cqb = qb.having("price").gte(Expression.param("price")); } parameters.put("price", price); } if (tag != null) { if (cqb != null) { cqb = cqb.and().having("tag").equal(Expression.param("tag")); } else { cqb = qb.having("tag").equal(Expression.param("tag")); } parameters.put("tag", tag); }
検索条件に指定する値は、いずれもプレースホルダーにしています(title…というかLIKE検索除く)。
priceの降順にソート。
cqb.orderBy("price", SortOrder.DESC);
Queryの作成と、検索条件の値へのバインド。
Query query = cqb.build();
if (!parameters.isEmpty()) {
query.setParameters(parameters);
}
という感じです。以降のQueryも、だいたいこんな感じの雰囲気で作っていこうと思います。
Ickle Query。やっていることは、ほぼQuery DSLの時と同じです。こちらは、LIKE検索でもパラメーターへのバインドが可能です。
@GET @Path("search-ickle") @Produces(MediaType.APPLICATION_JSON) public List<Book> searchByIckleQuery(@QueryParam("title") String title, @QueryParam("price") Integer price, @QueryParam("tag") String tag) { QueryFactory qf = Search.getQueryFactory(bookCache); List<String> conditions = new ArrayList<>(); Map<String, Object> parameters = new HashMap<>(); if (title != null) { conditions.add("title like :title"); parameters.put("title", "%" + title + "%"); } if (price != null) { conditions.add("price >= :price"); parameters.put("price", price); } if (tag != null) { conditions.add("tag = :tag"); parameters.put("tag", tag); } Query query = qf.create( String.format( "from %s where %s order by price desc", Book.class.getName(), String.join(" and ", conditions) ) ); query.setParameters(parameters); logger.infof("query = %s", query.getQueryString()); return query.list(); }
では、動作確認の前にInfinispan ServerにCacheを作成します。名前は「bookCache」で、Distributed Cacheとします(Nodeひとつですが)。
※この操作は、Infinispan Serverが動作している環境で行っています
$ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=bookCacheConfiguration:add(start=EAGER,mode=SYNC)' {"outcome" => "success"} $ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/distributed-cache=bookCache:add(configuration=bookCacheConfiguration)' {"outcome" => "success"}
ビルドして実行。
$ mvn package $ java -jar target/infinispan-client-query-0.0.1-SNAPSHOT-runner.jar
データの登録。以降の例でも、登録するデータは同じものを使います。
$ curl -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4621303252 -d '{"isbn": "978-4621303252", "title": "Effective Java 第3版", "price":4320, "tag": "java"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4774189093 -d '{"isbn": "978-4774189, "title": "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", "price":3218, "tag": "java"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4774182179 -d '{"isbn": "978-4774182, "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", "price": 4104, "tag": "spring"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4295003915 -d '{"isbn": "978-4295003, "title": "Elasticsearch実践ガイド", "price": 3024, "tag": "全文検索"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4774161631 -d '{"isbn": "978-4774161, "title": "[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン", "price": 3888, "tag": "全文検索"}'
1件取得。
$ curl localhost:8080/book/978-4621303252 {"isbn":"978-4621303252","price":4320,"tag":"java","title":"Effective Java 第3版"}
検索してみます。Query DSLから。
$ curl -s 'localhost:8080/book/search-dsl?title=Java&price=4000&tag=java' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" } ] $ curl -s 'localhost:8080/book/search-dsl?title=Java&price=4000' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" }, { "isbn": "978-4774182179", "price": 4104, "tag": "spring", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ" } ]
ログに出てくるQuery Stringは、こんな感じです。
2019-07-06 22:48:33,899 INFO [org.lit.qua.inf.BookResource] (executor-thread-1) query = FROM org.littlewings.quarkus.infinispan.Book _gen0 WHERE _gen0.title LIKE '%Java%' AND _gen0.price >= :price AND _gen0.tag = :tag ORDER BY _gen0.price DESC 2019-07-06 22:48:43,263 INFO [org.lit.qua.inf.BookResource] (executor-thread-1) query = FROM org.littlewings.quarkus.infinispan.Book _gen0 WHERE _gen0.title LIKE '%Java%' AND _gen0.price >= :price ORDER BY _gen0.price DESC
Ickle Query。Queryを作るAPIが違うだけで意味は同じなので、当然ながらQuery DSLの時と同じ結果になります。
$ curl -s 'localhost:8080/book/search-ickle?title=Java&price=4000&tag=java' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" } ] $ curl -s 'localhost:8080/book/search-ickle?title=Java&price=4000' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" }, { "isbn": "978-4774182179", "price": 4104, "tag": "spring", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ" } ]
ログ出力されるQuery Stringは、こんな感じになります。。
2019-07-06 22:49:14,951 INFO [org.lit.qua.inf.BookResource] (executor-thread-1) query = from org.littlewings.quarkus.infinispan.Book where title like :title and price >= :price and tag = :tag order by price desc 2019-07-06 22:49:33,950 INFO [org.lit.qua.inf.BookResource] (executor-thread-1) query = from org.littlewings.quarkus.infinispan.Book where title like :title and price >= :price order by price desc
最後にネイティブイメージで確認、ですが、結果自体は同じになるのでそちらの掲載は割愛します。
$ mvn -P native package $ ./target/infinispan-client-query-0.0.1-SNAPSHOT-runner
インデックスあり、Analyzeなしの場合
次は、インデックスは使いますがAnalyzerは使わないパターンです。
Protocol BufferesのIDL。
src/main/resources/META-INF/grid-indexed-entity.proto
package org.littlewings.quarkus.infinispan; option indexed_by_default = false; /* @Indexed */ message IndexedBook { /* @Field(store = Store.YES, analyze = Analyze.NO) */ required string isbn = 1; /* @Field(store = Store.YES, analyze = Analyze.NO) */ required string title = 2; /* @Field(store = Store.YES, analyze = Analyze.NO) @SortableField */ required int32 price = 3; /* @Field(store = Store.YES, analyze = Analyze.NO) */ required string tag = 4; }
Infinispanのドキュメントに従い、デフォルトでフィールドはインデックス対象にせず、インデックスに保存するものは明示しました。
結局全部なんですけど。
また、priceフィールドについてはソートで使うので、@SortableFieldアノテーションを付与しています。
これらのアノテーションは、Hibernate Searchのものですね。
EntityからMarshallerまでを一気に。
Entity。
src/main/java/org/littlewings/quarkus/infinispan/IndexedBook.java
package org.littlewings.quarkus.infinispan; public class IndexedBook { private String isbn; private String title; private int price; private String tag; // getter/setterは省略 }
Marshaller。
src/main/java/org/littlewings/quarkus/infinispan/IndexedBookMarshaller.java
package org.littlewings.quarkus.infinispan; import java.io.IOException; import org.infinispan.protostream.MessageMarshaller; public class IndexedBookMarshaller implements MessageMarshaller<IndexedBook> { @Override public IndexedBook readFrom(ProtoStreamReader reader) throws IOException { IndexedBook indexedBook = new IndexedBook(); indexedBook.setIsbn(reader.readString("isbn")); indexedBook.setTitle(reader.readString("title")); indexedBook.setPrice(reader.readInt("price")); indexedBook.setTag(reader.readString("tag")); return indexedBook; } @Override public void writeTo(ProtoStreamWriter writer, IndexedBook indexedBook) throws IOException { writer.writeString("isbn", indexedBook.getIsbn()); writer.writeString("title", indexedBook.getTitle()); writer.writeInt("price", indexedBook.getPrice()); writer.writeString("tag", indexedBook.getTag()); } @Override public Class<? extends IndexedBook> getJavaClass() { return IndexedBook.class; } @Override public String getTypeName() { return "org.littlewings.quarkus.infinispan.IndexedBook"; } }
MarshallerのProducer。先ほど作成したクラスに、追加しています。
src/main/java/org/littlewings/quarkus/infinispan/MarshallerFactory.java
package org.littlewings.quarkus.infinispan; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import org.infinispan.protostream.MessageMarshaller; @Dependent public class MarshallerFactory { @Produces @ApplicationScoped public MessageMarshaller bookMarshaller() { return new BookMarshaller(); } @Produces @ApplicationScoped public MessageMarshaller indexedBookMarshaller() { return new IndexedBookMarshaller(); } }
JAX-RSリソースクラス。
src/main/java/org/littlewings/quarkus/infinispan/IndexedBookResource.java
package org.littlewings.quarkus.infinispan; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import io.quarkus.infinispan.client.runtime.Remote; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.Search; import org.infinispan.query.dsl.Expression; import org.infinispan.query.dsl.FilterConditionContextQueryBuilder; import org.infinispan.query.dsl.Query; import org.infinispan.query.dsl.QueryBuilder; import org.infinispan.query.dsl.QueryFactory; import org.infinispan.query.dsl.SortOrder; import org.jboss.logging.Logger; @Path("indexed-book") public class IndexedBookResource { Logger logger = Logger.getLogger(IndexedBookResource.class); @Inject @Remote("indexedBookCache") RemoteCache<String, IndexedBook> indexedBookCache; @PUT @Path("{isbn}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.TEXT_PLAIN) public String register(@PathParam("isbn") String isbn, IndexedBook indexedBook) { indexedBookCache.put(isbn, indexedBook); return "OK!!"; } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public IndexedBook findByIsbn(@PathParam("isbn") String isbn) { return indexedBookCache.get(isbn); } @GET @Path("search-dsl") @Produces(MediaType.APPLICATION_JSON) public List<IndexedBook> searchByQueryDsl(@QueryParam("title") String title, @QueryParam("price") Integer price, @QueryParam("tag") String tag) { QueryFactory qf = Search.getQueryFactory(indexedBookCache); QueryBuilder qb = qf.from(IndexedBook.class); FilterConditionContextQueryBuilder cqb = null; Map<String, Object> parameters = new HashMap<>(); if (title != null) { cqb = qb.having("title").like("%" + title + "%"); } if (price != null) { if (cqb != null) { cqb = cqb.and().having("price").gte(Expression.param("price")); } else { cqb = qb.having("price").gte(Expression.param("price")); } parameters.put("price", price); } if (tag != null) { if (cqb != null) { cqb = cqb.and().having("tag").equal(Expression.param("tag")); } else { cqb = qb.having("tag").equal(Expression.param("tag")); } parameters.put("tag", tag); } cqb.orderBy("price", SortOrder.DESC); Query query = cqb.build(); if (!parameters.isEmpty()) { query.setParameters(parameters); } logger.infof("query = %s", query.getQueryString()); return query.list(); } @GET @Path("search-ickle") @Produces(MediaType.APPLICATION_JSON) public List<IndexedBook> searchByIckleQuery(@QueryParam("title") String title, @QueryParam("price") Integer price, @QueryParam("tag") String tag) { QueryFactory qf = Search.getQueryFactory(indexedBookCache); List<String> conditions = new ArrayList<>(); Map<String, Object> parameters = new HashMap<>(); if (title != null) { conditions.add("title like :title"); parameters.put("title", "%" + title + "%"); } if (price != null) { conditions.add("price >= :price"); parameters.put("price", price); } if (tag != null) { conditions.add("tag = :tag"); parameters.put("tag", tag); } Query query = qf.create( String.format( "from %s where %s order by price desc", IndexedBook.class.getName(), String.join(" and ", conditions) ) ); query.setParameters(parameters); logger.infof("query = %s", query.getQueryString()); return query.list(); } }
実はこれ、先ほどのインデックスなしの場合と使っているCacheと
@Inject @Remote("indexedBookCache") RemoteCache<String, IndexedBook> indexedBookCache;
扱うEntityが違うだけで、構成は同じなのです。
QueryBuilder qb = qf.from(IndexedBook.class); Query query = qf.create( String.format( "from %s where %s order by price desc", IndexedBook.class.getName(), String.join(" and ", conditions) ) );
なので、ここでのAPIの解説は割愛…。
では、Infinispan ServerのCacheの作成。今回は、「indexedBookCache」という名前で作成します。
$ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=indexedBookCacheConfiguration:add(start=EAGER,mode=SYNC)' {"outcome" => "success"} $ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=indexedBookCacheConfiguration/indexing=INDEXING:add(indexing=LOCAL,indexing-properties={default.directory_provider=local-heap,lucene_version=LUCENE_CURRENT})' {"outcome" => "success"} $ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/distributed-cache=indexedBookCache:add(configuration=indexedBookCacheConfiguration)' {"outcome" => "success"}
データの登録。
$ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-book/978-4621303252 -d '{"isbn": "978-4621303252", "title": "Effective Java 第3版", "price":4320, "tag": "java"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-book/978-4774189093 -d '{"isbn": "9784189093", "title": "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", "price":3218, "tag": "java"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-book/978-4774182179 -d '{"isbn": "9784182179", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", "price": 4104, "tag": "spring"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-book/978-4295003915 -d '{"isbn": "9785003915", "title": "Elasticsearch実践ガイド", "price": 3024, "tag": "全文検索"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-book/978-4774161631 -d '{"isbn": "9784161631", "title": "[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン", "price": 3888, "tag": "全文検索"}' OK!!
Query DSLを使う方から、検索してみます。結果は、先ほどと同じ。
$ curl -s 'localhost:8080/indexed-book/search-dsl?title=Java&price=4000&tag=java' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" } ] $ curl -s 'localhost:8080/indexed-book/search-dsl?title=Java&price=4000' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" }, { "isbn": "978-4774182179", "price": 4104, "tag": "spring", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ" } ]
Query Stringも同じです。
2019-07-06 23:10:20,386 INFO [org.lit.qua.inf.IndexedBookResource] (executor-thread-1) query = FROM org.littlewings.quarkus.infinispan.IndexedBook _gen0 WHERE _gen0.title LIKE '%Java%' AND _gen0.price >= :price AND _gen0.tag = :tag ORDER BY _gen0.price DESC 2019-07-06 23:11:06,228 INFO [org.lit.qua.inf.IndexedBookResource] (executor-thread-1) query = FROM org.littlewings.quarkus.infinispan.IndexedBook _gen0 WHERE _gen0.title LIKE '%Java%' AND _gen0.price >= :price ORDER BY _gen0.price DESC
Ickle Query。
$ curl -s 'localhost:8080/indexed-book/search-ickle?title=Java&price=4000&tag=java' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" } ] $ curl -s 'localhost:8080/indexed-book/search-ickle?title=Java&price=4000' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" }, { "isbn": "978-4774182179", "price": 4104, "tag": "spring", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ" } ]
Query String。
2019-07-06 23:11:59,353 INFO [org.lit.qua.inf.IndexedBookResource] (executor-thread-1) query = from org.littlewings.quarkus.infinispan.IndexedBook where title like :title and price >= :price and tag = :tag order by price desc 2019-07-06 23:12:10,671 INFO [org.lit.qua.inf.IndexedBookResource] (executor-thread-1) query = from org.littlewings.quarkus.infinispan.IndexedBook where title like :title and price >= :price order by price desc
インデックスが有効になっただけで、見た感じは先ほどの例と変わりませんね。
インデックスあり、Analyzeありの場合
最後、インデックスありで、Analyzeもありの場合です。
Protocol BuffersのIDL。
src/main/resources/META-INF/grid-indexed-analyzed-entity.proto
package org.littlewings.quarkus.infinispan; option indexed_by_default = false; /* @Indexed */ message IndexedAnalyzedBook { /* @Field(store = Store.YES, analyze = Analyze.NO) */ required string isbn = 1; /* @Field(store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = "standard")) */ required string title = 2; /* @Field(store = Store.YES, analyze = Analyze.NO) @SortableField */ required int32 price = 3; /* @Field(store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = "keyword")) */ required string tag = 4; }
先ほどまで使っていた@Fieldアノテーションのanalyze属性を、Analyze.YESとするようにしています。
さらに、analyzerも指定していますが、Analyzerの使い方はドキュメントの以下の部分を読むとよいでしょう。
今回は、titleにStandardAnalyzer、tagにKeywordAnalyzerを使用しています。
EntityからMarshallerまで。
Entity。
src/main/java/org/littlewings/quarkus/infinispan/IndexedAnalyzedBook.java
package org.littlewings.quarkus.infinispan; public class IndexedAnalyzedBook { private String isbn; private String title; private int price; private String tag; // getter/setterは省略 }
Marshaller。
src/main/java/org/littlewings/quarkus/infinispan/IndexedAnalyzedBookMarshaller.java
package org.littlewings.quarkus.infinispan; import java.io.IOException; import org.infinispan.protostream.MessageMarshaller; public class IndexedAnalyzedBookMarshaller implements MessageMarshaller<IndexedAnalyzedBook> { @Override public IndexedAnalyzedBook readFrom(ProtoStreamReader reader) throws IOException { IndexedAnalyzedBook indexedAnalyzedBook = new IndexedAnalyzedBook(); indexedAnalyzedBook.setIsbn(reader.readString("isbn")); indexedAnalyzedBook.setTitle(reader.readString("title")); indexedAnalyzedBook.setPrice(reader.readInt("price")); indexedAnalyzedBook.setTag(reader.readString("tag")); return indexedAnalyzedBook; } @Override public void writeTo(ProtoStreamWriter writer, IndexedAnalyzedBook indexedAnalyzedBook) throws IOException { writer.writeString("isbn", indexedAnalyzedBook.getIsbn()); writer.writeString("title", indexedAnalyzedBook.getTitle()); writer.writeInt("price", indexedAnalyzedBook.getPrice()); writer.writeString("tag", indexedAnalyzedBook.getTag()); } @Override public Class<? extends IndexedAnalyzedBook> getJavaClass() { return IndexedAnalyzedBook.class; } @Override public String getTypeName() { return "org.littlewings.quarkus.infinispan.IndexedAnalyzedBook"; } }
MarshallerのProducer。
src/main/java/org/littlewings/quarkus/infinispan/MarshallerFactory.java
package org.littlewings.quarkus.infinispan; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import org.infinispan.protostream.MessageMarshaller; @Dependent public class MarshallerFactory { @Produces @ApplicationScoped public MessageMarshaller bookMarshaller() { return new BookMarshaller(); } @Produces @ApplicationScoped public MessageMarshaller indexedBookMarshaller() { return new IndexedBookMarshaller(); } @Produces @ApplicationScoped public MessageMarshaller indexedAnalyzedBookMarshaller() { return new IndexedAnalyzedBookMarshaller(); } }
JAX-RSリソースクラス。
src/main/java/org/littlewings/quarkus/infinispan/IndexedAnalyzedBookResource.java
package org.littlewings.quarkus.infinispan; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import io.quarkus.infinispan.client.runtime.Remote; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.Search; import org.infinispan.query.dsl.Query; import org.infinispan.query.dsl.QueryFactory; import org.jboss.logging.Logger; @Path("indexed-analyzed-book") public class IndexedAnalyzedBookResource { Logger logger = Logger.getLogger(IndexedAnalyzedBookResource.class); @Inject @Remote("indexedAnalyzedBookCache") RemoteCache<String, IndexedAnalyzedBook> indexedAnalyzedBookCache; @PUT @Path("{isbn}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.TEXT_PLAIN) public String register(@PathParam("isbn") String isbn, IndexedAnalyzedBook indexedAnalyzedBook) { indexedAnalyzedBookCache.put(isbn, indexedAnalyzedBook); return "OK!!"; } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public IndexedAnalyzedBook findByIsbn(@PathParam("isbn") String isbn) { return indexedAnalyzedBookCache.get(isbn); } @GET @Path("search-ickle") @Produces(MediaType.APPLICATION_JSON) public List<IndexedAnalyzedBook> searchByIckleQuery(@QueryParam("title") String title, @QueryParam("price") Integer price, @QueryParam("tag") String tag) { QueryFactory qf = Search.getQueryFactory(indexedAnalyzedBookCache); List<String> conditions = new ArrayList<>(); if (title != null) { conditions.add("title: '" + title + "'"); } if (price != null) { conditions.add("price: [" + price + " TO *]"); } if (tag != null) { conditions.add("tag: '" + tag + "'"); } Query query = qf.create( String.format( "from %s where %s order by price desc", IndexedAnalyzedBook.class.getName(), String.join(" and ", conditions) ) ); logger.infof("query = %s", query.getQueryString()); return query.list(); } }
こちらは、Ickle Queryのみです。
Cacheの名前以外に他と違うのは、Ickle Queryで使っているクエリの構文です。
if (title != null) { conditions.add("title: '" + title + "'"); } if (price != null) { conditions.add("price: [" + price + " TO *]"); } if (tag != null) { conditions.add("tag: '" + tag + "'"); }
こちらは、Apache LuceneのQuery Parser形式での記載になっています。
Deviations from the Lucene Query Parser Syntax
また、パラメーターのバインドは行いません(行なえません)。
Infinispan Serverに、Cacheの作成。名前は、「indexedAnalyzedBookCache」です。
$ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=indexedAnalyzedBookCacheConfiguration:add(start=EAGER,mode=SYNC)' {"outcome" => "success"} $ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=indexedAnalyzedBookCacheConfiguration/indexing=INDEXING:add(indexing=LOCAL,indexing-properties={default.directory_provider=local-heap,lucene_version=LUCENE_CURRENT})' {"outcome" => "success"} $ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/distributed-cache=indexedAnalyzedBookCache:add(configuration=indexedAnalyzedBookCacheConfiguration)' {"outcome" => "success"}
アプリケーションを起動して、データの登録。
$ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-analyzed-book/978-4621303252 -d '{"isbn": "978-4621303252", "title": "Effective Java 第3版", "price":4320, "tag": "java"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-analyzed-book/978-4774189093 -d '{"is "978-4774189093", "title": "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", "price":3218, "tag": "java"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-analyzed-book/978-4774182179 -d '{"is "978-4774182179", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", "price": 4104, "tag": "spring"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-analyzed-book/978-4295003915 -d '{"is "978-4295003915", "title": "Elasticsearch実践ガイド", "price": 3024, "tag": "全文検索"}' OK!! $ curl -XPUT -H 'Content-Type: application/json' localhost:8080/indexed-analyzed-book/978-4774161631 -d '{"is "978-4774161631", "title": "[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン", "price": 3888, "tag": "全文検索"}'
確認。今回は、Ickle Queryしかありません。
これまでのIckle Queryと違って、例えばtitleフィールドであればStandardAnalyzerが効いているので、小文字でも検索できたりします。
$ curl -s 'localhost:8080/indexed-analyzed-book/search-ickle?title=java' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" }, { "isbn": "978-4774182179", "price": 4104, "tag": "spring", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ" }, { "isbn": "978-4774189093", "price": 3218, "tag": "java", "title": "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで" } ]
この時のQuery String。
2019-07-07 01:19:58,424 INFO [org.lit.qua.inf.IndexedAnalyzedBookResource] (executor-thread-1) query = from org.littlewings.quarkus.infinispan.IndexedAnalyzedBook where title: 'java' order by price desc
その他。
$ curl -s 'localhost:8080/indexed-analyzed-book/search-ickle?title=java&price=4000&tag=java' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" } ] $ curl -s 'localhost:8080/indexed-analyzed-book/search-ickle?title=java&price=4000' | jq [ { "isbn": "978-4621303252", "price": 4320, "tag": "java", "title": "Effective Java 第3版" }, { "isbn": "978-4774182179", "price": 4104, "tag": "spring", "title": "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ" } ]
Query String。
2019-07-07 01:21:49,469 INFO [org.lit.qua.inf.IndexedAnalyzedBookResource] (executor-thread-1) query = from org.littlewings.quarkus.infinispan.IndexedAnalyzedBook where title: 'java' and price: [4000 TO *] and tag: 'java' order by price desc 2019-07-07 01:22:02,146 INFO [org.lit.qua.inf.IndexedAnalyzedBookResource] (executor-thread-1) query = from org.littlewings.quarkus.infinispan.IndexedAnalyzedBook where title: 'java' and price: [4000 TO *] order by price desc
こんな感じですね。
まとめ
QuarkusのInfinispan Client Extensionで、InfinispanのRemote Query(Query DSL + Ickle Query)を試してみました。
Quarkusでも、InfinispanのQuery DSLやIckle Queryが使えることが、確認できました。
それにしても、Query DSLとかは久しぶりに使ったのでけっこう忘れていましたが…まあなんとかなりました。
Protocol BuffersのIDLを使って、@Fieldなどを指定するのも久しぶりでしたねぇ。