Elasticsearchクラスターの実行
Elasticsearchはよく知られた全文検索エンジンであり、NoSQLデータストアです。
このガイドでは、RESTサービスでElasticsearchクラスターを使用する方法を見ていきます。
QuarkusはElasticsearchにアクセスするための2つの方法を提供しています:低レベルの RestClient 経由、または RestHighLevelClient 経由であり、低レベルと高レベルのクライアントと呼びます。
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 11+ がインストールされ、
JAVA_HOMEが適切に設定されていること -
Apache Maven 3.8.6
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
-
Elasticsearchがインストールされているか、Dockerがインストールされていること
アーキテクチャ
このガイドで開発するアプリケーションは非常にシンプルです: ユーザーはフォームを使用してリストに要素を追加することができ、リストが更新されます。
ブラウザとサーバー間の情報はすべてJSON形式になっています。
要素はElasticsearchに格納されます。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
このコマンドは、RESTEasy/JAX-RS、Jackson、およびElasticsearchの低レベルクライアント拡張をインポートするMaven構造を生成します。この後、 quarkus-elasticsearch-rest-client のエクステンションがビルドファイルに追加されています。
代わりに高レベルクライアントを使いたい場合は、 elasticsearch-rest-client のエクステンションを elasticsearch-rest-high-level-client のエクステンションで置き換えてください。
|
ここでは JSON-B などではなく |
新しいプロジェクトを生成したくない場合は、以下の依存関係をビルドファイルに追加してください。
Elasticsearchの低レベルクライアントの場合は、以下を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elasticsearch-rest-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elasticsearch-rest-client")
Elasticsearchの高レベルクライアントの場合は、以下を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elasticsearch-rest-high-level-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elasticsearch-rest-high-level-client")
初めてのJSON RESTサービスの作成
この例では、果物のリストを管理するアプリケーションを作成します。
まず、以下のように Fruit Bean を作成してみましょう。
package org.acme.elasticsearch;
public class Fruit {
public String id;
public String name;
public String color;
}
派手なことは何もありません。注意すべき重要なことは、デフォルトのコンストラクタを持つことはJSONシリアライズレイヤーで必須であるということです。
ここで、アプリケーションのビジネスレイヤーとなる org.acme.elasticsearch.FruitService を作成し、Elasticsearch インスタンスからフルーツを保存/ロードします。ここでは低レベルのクライアントを使用していますが、代わりに高レベルのクライアントを使用したい場合は、 高レベルのRESTクライアントの使用 の項の指示に従ってください。
package org.acme.elasticsearch;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestClient;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
@ApplicationScoped
public class FruitService {
@Inject
RestClient restClient; (1)
public void index(Fruit fruit) throws IOException {
Request request = new Request(
"PUT",
"/fruits/_doc/" + fruit.id); (2)
request.setJsonEntity(JsonObject.mapFrom(fruit).toString()); (3)
restClient.performRequest(request); (4)
}
public Fruit get(String id) throws IOException {
Request request = new Request(
"GET",
"/fruits/_doc/" + id);
Response response = restClient.performRequest(request);
String responseBody = EntityUtils.toString(response.getEntity());
JsonObject json = new JsonObject(responseBody); (5)
return json.getJsonObject("_source").mapTo(Fruit.class);
}
public List<Fruit> searchByColor(String color) throws IOException {
return search("color", color);
}
public List<Fruit> searchByName(String name) throws IOException {
return search("name", name);
}
private List<Fruit> search(String term, String match) throws IOException {
Request request = new Request(
"GET",
"/fruits/_search");
//construct a JSON query like {"query": {"match": {"<term>": "<match"}}
JsonObject termJson = new JsonObject().put(term, match);
JsonObject matchJson = new JsonObject().put("match", termJson);
JsonObject queryJson = new JsonObject().put("query", matchJson);
request.setJsonEntity(queryJson.encode());
Response response = restClient.performRequest(request);
String responseBody = EntityUtils.toString(response.getEntity());
JsonObject json = new JsonObject(responseBody);
JsonArray hits = json.getJsonObject("hits").getJsonArray("hits");
List<Fruit> results = new ArrayList<>(hits.size());
for (int i = 0; i < hits.size(); i++) {
JsonObject hit = hits.getJsonObject(i);
Fruit fruit = hit.getJsonObject("_source").mapTo(Fruit.class);
results.add(fruit);
}
return results;
}
}
この例では、次のことに注意してください:
-
Elasticsearch の低レベル
RestClientをサービスに注入しています。 -
Elasticsearchリクエストを作成します。
-
Elasticsearchに送信する前にオブジェクトをシリアライズするためにVert.x
JsonObjectを使用していますが、JSONにシリアライズしたものは何でも使えます。 -
Elasticsearchにリクエスト(ここではインデックス作成のリクエスト)を送信します。
-
Elasticsearchからオブジェクトをデシリアライズするために、再びVert.x
JsonObjectを使用します。
では、次のように org.acme.elasticsearch.FruitResource クラスを作成します:
package org.acme.elasticsearch;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.UUID;
import org.jboss.resteasy.reactive.RestQuery;
@Path("/fruits")
public class FruitResource {
@Inject
FruitService fruitService;
@POST
public Response index(Fruit fruit) throws IOException {
if (fruit.id == null) {
fruit.id = UUID.randomUUID().toString();
}
fruitService.index(fruit);
return Response.created(URI.create("/fruits/" + fruit.id)).build();
}
@GET
@Path("/{id}")
public Fruit get(String id) throws IOException {
return fruitService.get(id);
}
@GET
@Path("/search")
public List<Fruit> search(@RestQuery String name, @RestQuery String color) throws IOException {
if (name != null) {
return fruitService.searchByName(name);
} else if (color != null) {
return fruitService.searchByColor(color);
} else {
throw new BadRequestException("Should provide name or color query parameter");
}
}
}
実装はとても簡単で、JAX-RSのアノテーションを使ってエンドポイントを定義し、 FruitService を使って新しいフルーツをリストアップ/追加するだけです。
Elasticsearchの設定
設定する主なプロパティーは、Elasticsearchクラスターに接続するためのURLです。
設定のサンプルは以下のようになります。
# configure the Elasticsearch client for a cluster of two nodes
quarkus.elasticsearch.hosts = elasticsearch1:9200,elasticsearch2:9200
この例では、ローカルホスト上で実行されている単一のインスタンスを使用しています。
# configure the Elasticsearch client for a single instance on localhost
quarkus.elasticsearch.hosts = localhost:9200
より高度な設定が必要な場合は、このガイドの最後に、サポートされている設定プロパティーの包括的なリストがあります。
開発サービス(コンフィグレーション・フリー・データベース)
Quarkusは、Dev Servicesという機能をサポートしており、設定なしでさまざまなコンテナを起動することができます。Elasticsearchの場合、このサポートはデフォルトのElasticsearch接続にも及んでいます。具体的には、 quarkus.elasticsearch.hosts を設定していない場合、テストや開発モードの実行時にQuarkusが自動的にElasticsearchコンテナを起動し、接続を自動的に設定するということです。
製品版アプリケーションの実行時には、通常通りElasticsearch接続の設定が必要です。 application.properties に製品版データベース設定を含め、Dev Servicesを引き続き使用したい場合は、 %prod. プロファイルを使用してElasticsearch設定を定義することをお勧めします。
詳細については、 Dev Services for Elasticsearch ガイド をご覧ください。
Elasticsearchのプログラムによる設定
パラメーターによる設定に加えて、 RestClientBuilder.HttpClientConfigCallback を実装して ElasticsearchClientConfig とアノテーションを付けることで、追加の設定をプログラムでクライアントに適用することもできます。複数の実装を追加することができ、各実装で提供された設定はランダムに順序付けられたカスケード方式で適用されます。
例えば、HTTPレイヤでTLS用に設定されているElasticsearchクラスタにアクセスする場合、クライアントはElasticsearchが使用している証明書を信頼する必要があります。以下は、Elasticsearchが使用している証明書に署名したCAの証明書がPKCS#12のキーストアで利用可能な場合に、クライアントがそのCAの証明書を信頼するように設定する例です。
import io.quarkus.elasticsearch.restclient.lowlevel.ElasticsearchClientConfig;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.elasticsearch.client.RestClientBuilder;
import javax.enterprise.context.Dependent;
import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
@ElasticsearchClientConfig
public class SSLContextConfigurator implements RestClientBuilder.HttpClientConfigCallback {
@Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
try {
String keyStorePass = "password-for-keystore";
Path trustStorePath = Paths.get("/path/to/truststore.p12");
KeyStore truststore = KeyStore.getInstance("pkcs12");
try (InputStream is = Files.newInputStream(trustStorePath)) {
truststore.load(is, keyStorePass.toCharArray());
}
SSLContextBuilder sslBuilder = SSLContexts.custom()
.loadTrustMaterial(truststore, null);
SSLContext sslContext = sslBuilder.build();
httpClientBuilder.setSSLContext(sslContext);
} catch (Exception e) {
throw new RuntimeException(e);
}
return httpClientBuilder;
}
}
この例の詳細については、 Elasticsearchのドキュメント を参照してください。
|
|
Elasticsearchクラスターの実行
デフォルトでは、Elasticsearchクライアントはポート9200(Elasticsearchのデフォルトポート)でローカルのElasticsearchクラスターにアクセスするように設定されているので、このポートでローカルで実行中のインスタンスがある場合、テストできるようにするためにやるべきことは何もありません!
Dockerを使ってElasticsearchインスタンスを起動したい場合は、以下のコマンドで起動します。
docker run --name elasticsearch -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms512m -Xmx512m"\
--rm -p 9200:9200 docker.io/elastic/elasticsearch:7.16.3
アプリケーションの実行
それでは、Quarkusの開発モードでアプリケーションを実行してみましょう:
+
quarkus dev
+
./mvnw quarkus:dev
+
./gradlew --console=plain quarkusDev
以下の curl コマンドで、新しいフルーツをリストに追加することができます:
curl localhost:8080/fruits -d '{"name": "bananas", "color": "yellow"}' -H "Content-Type: application/json"
また、以下の curl コマンドで、名前や色でフルーツを検索することができます:
curl localhost:8080/fruits/search?color=yellow
高レベルRESTクライアントの使用
QuarkusはElasticsearch High Level REST Clientのサポートを提供していますが、いくつかの注意点があることを覚えておいてください。
-
これは多くの依存関係(特に Lucene)を引きずっており、Quarkus の哲学にはあまり合っていません。Elasticsearch チームはこの問題を認識しており、将来的には改善されるかもしれません。
-
これはElasticsearchサーバーの特定のバージョンに縛られています: 高レベルRESTクライアントのバージョン7を使用してサーバーのバージョン6にアクセスすることはできません。
|
Elastic社がElasticsearch 高レベルRESTクライアントのライセンスを変更したため、Quarkusではこの特定のクライアントの最後のオープンソースバージョンである7.10を維持しており、新しいバージョンへのアップグレードはありません。 このクライアントがElasticによって非推奨とされ、新しいオープンソースのJavaクライアントに置き換えられたことから、Elasticsearch 高レベルRESTクライアントのエクステンションは非推奨とされ、将来的にQuarkusのコードベースから削除される予定です。 高レベルRESTクライアントとは逆に、低レベルRESTクライアントの最新バージョン(まだオープンソース)を使用しており、動作すると信じていますが、状況は理想的とは言えず、いくつかの問題が発生する可能性があることに注意してください。アプリケーションの要件に応じてクライアントのバージョンを自由に上書きできますが、バージョン7.11+の 高レベルRESTクライアントの新しいライセンス には注意してください:オープンソースではなく、いくつかの使用制限があります。 最終的には、新しいオープンソースのJavaクライアント用のエクステンションを提供する予定ですが、全く新しいクライアントであるため、アプリケーションの変更が必要になります。 |
ここでは、低レベルのクライアントの代わりに高レベルのクライアントを使用したバージョンの FruitService を示します。
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import io.vertx.core.json.JsonObject;
@ApplicationScoped
public class FruitService {
@Inject
RestHighLevelClient restHighLevelClient; (1)
public void index(Fruit fruit) throws IOException {
IndexRequest request = new IndexRequest("fruits"); (2)
request.id(fruit.id);
request.source(JsonObject.mapFrom(fruit).toString(), XContentType.JSON); (3)
restHighLevelClient.index(request, RequestOptions.DEFAULT); (4)
}
public Fruit get(String id) throws IOException {
GetRequest getRequest = new GetRequest("fruits", id);
GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
if (getResponse.isExists()) {
String sourceAsString = getResponse.getSourceAsString();
JsonObject json = new JsonObject(sourceAsString); (5)
return json.mapTo(Fruit.class);
}
return null;
}
public List<Fruit> searchByColor(String color) throws IOException {
return search("color", color);
}
public List<Fruit> searchByName(String name) throws IOException {
return search("name", name);
}
private List<Fruit> search(String term, String match) throws IOException {
SearchRequest searchRequest = new SearchRequest("fruits");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery(term, match));
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
List<Fruit> results = new ArrayList<>(hits.getHits().length);
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
JsonObject json = new JsonObject(sourceAsString);
results.add(json.mapTo(Fruit.class));
}
return results;
}
}
この例では、次のことに注意してください:
-
サービス内部にElasticsearch
RestHighLevelClientを注入しています。 -
Elasticsearchのインデックスリクエストを作成します。
-
Elasticsearchに送信する前にオブジェクトをシリアライズするためにVert.x
JsonObjectを使用していますが、JSONにシリアライズしたものは何でも使えます。 -
Elasticsearchにリクエストを送信します。
-
Elasticsearchからオブジェクトをデシリアライズするために、再びVert.x
JsonObjectを使用します。
Hibernate Search Elasticsearch
Quarkusは、 hibernate-search-orm-elasticsearch エクステンションを介してElasticsearchでHibernate Searchをサポートしています。
Hibernate Search Elasticsearchでは、JPAエンティティーをElasticsearchクラスターに同期させることができ、Hibernate Search APIを使ってElasticsearchクラスターにクエリを発行する方法を提供しています。
興味のある方は、 Hibernate Search with Elasticsearchのガイド をお読みください。
クラスターヘルスチェック
quarkus-smallrye-health エクステンションを使用している場合、どちらのエクステンションも、クラスターの健全性を検証するための readiness ヘルスチェックを自動的に追加します。
そのため、アプリケーションの /q/health/ready エンドポイントにアクセスすると、クラスターの状態に関する情報を得ることができます。これはクラスターヘルスエンドポイントを使用しており、クラスターの状態が 赤 であったり、クラスターが利用できなかったりするとチェックが失敗します。
この動作は、 application.properties の quarkus.elasticsearch.health.enabled プロパティーを false に設定することで無効にできます。
ネイティブ実行可能ファイルの構築
ネイティブ実行可能ファイルで両方のクライアントを使用することができます。
以下のコマンドでネイティブ実行可能ファイルをビルドすることができます。
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
実行は ./target/elasticsearch-low-level-client-quickstart-1.0-SNAPSHOT-runner を実行するだけで簡単です。
その後、ブラウザで http://localhost:8080/fruits.html を開いてアプリケーションを使用します。
まとめ
Quarkusでは、簡単な設定、CDIの統合、ネイティブサポートが提供されているため、低レベルまたは高レベルのクライアントからElasticsearchクラスターにアクセスすることが簡単にできます。
設定リファレンス
ビルド時に固定される設定プロパティ - 他のすべての設定プロパティは実行時にオーバーライド可能
タイプ |
デフォルト |
|
|---|---|---|
Whether a health check is published in case the smallrye-health extension is present. Environment variable: Show more |
boolean |
|
The list of hosts of the Elasticsearch servers. Environment variable: Show more |
list of host:port |
|
The protocol to use when contacting Elasticsearch servers. Set to "https" to enable SSL/TLS. Environment variable: Show more |
string |
|
The username for basic HTTP authentication. Environment variable: Show more |
string |
|
The password for basic HTTP authentication. Environment variable: Show more |
string |
|
The connection timeout. Environment variable: Show more |
|
|
The socket timeout. Environment variable: Show more |
|
|
The maximum number of connections to all the Elasticsearch servers. Environment variable: Show more |
int |
|
The maximum number of connections per Elasticsearch server. Environment variable: Show more |
int |
|
The number of IO thread. By default, this is the number of locally detected processors. Thread counts higher than the number of processors should not be necessary because the I/O threads rely on non-blocking operations, but you may want to use a thread count lower than the number of processors. Environment variable: Show more |
int |
|
Defines if automatic discovery is enabled. Environment variable: Show more |
boolean |
|
Refresh interval of the node list. Environment variable: Show more |
|
|
期間フォーマットについて
期間のフォーマットは標準の 数値で始まる期間の値を指定することもできます。この場合、値が数値のみで構成されている場合、コンバーターは値を秒として扱います。そうでない場合は、 |