Elasticsearchクラスターの実行
Elasticsearchはよく知られた全文検索エンジンであり、NoSQLデータストアです。
このガイドでは、RESTサービスをElasticsearchクラスタと接続する方法を紹介します。
Quarkusは、Elasticsearchにアクセスする2つの方法を提供します:
-
低レベルRESTクライアント
-
Elasticsearch Java クライアント
高レベルRESTクライアント用の第3のQuarkusエクステンションが存在しますが、このクライアントはElasticによって非推奨とされており、ライセンス上の問題もあるため、将来のバージョンで削除される予定です。 新しい Elasticsearch Java クライアントエクステンションにアップグレードすることを強くお勧めします。 |
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 11+ がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.9.1
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
-
Elasticsearchがインストールされているか、Dockerがインストールされていること
アーキテクチャ
このガイドで構築されるアプリケーションは非常にシンプルです。ユーザーはフォームを使用してリストに要素を追加することができ、リストが更新されます。
ブラウザとサーバー間の情報はすべてJSON形式になっています。
要素はElasticsearchに格納されます。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:
このコマンドは、RESTEasy Reactive、Jackson、およびElasticsearch低レベルRESTクライアントエクステンションをインポートするMaven構造を生成します。
Elasticsearch low level REST client は、ビルドファイルに追加された quarkus-elasticsearch-rest-client
エクステンションに付属しています。
Elasticsearch Java クライアントを代わりに使用する場合は、 quarkus-elasticsearch-rest-client
エクステンションを quarkus-elasticsearch-java-client
エクステンションに置き換えてください。
ここでは JSON-B などではなく |
既存のプロジェクトにエクステンションを追加する場合は、以下の手順で行ってください。
Elasticsearch 低レベルRESTクライアントについては、以下の依存関係をビルドファイルに追加してください:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elasticsearch-rest-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elasticsearch-rest-client")
Elasticsearch Javaクライアントについては、以下の依存関係をビルドファイルに追加してください:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elasticsearch-java-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elasticsearch-java-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 クライアントを使用していますが、代わりに Java API クライアントを使用したい場合は、代わりに Elasticsearch Java クライアントの使用 のパラグラフの指示に従ってください。
package org.acme.elasticsearch;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.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;
}
}
1 | Elasticsearch の低レベル RestClient をサービスに注入しています。 |
2 | Elasticsearchリクエストを作成します。 |
3 | Elasticsearch に送る前にオブジェクトをシリアライズするために Vert.x JsonObject を使用しています。オブジェクトを JSON にシリアライズするために好きなものを使用することができます。 |
4 | Elasticsearchにリクエスト(ここではインデックス作成のリクエスト)を送信します。 |
5 | Elasticsearchからオブジェクトをデシリアライズするために、再びVert.x JsonObject を使用します。 |
では、次のように org.acme.elasticsearch.FruitResource
クラスを作成します:
package org.acme.elasticsearch;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.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");
}
}
}
実装は非常に簡単で、Jakarta RESTアノテーションを使ってエンドポイントを定義し、 FruitService
を使って新しいフルーツをリストアップ/追加するだけでよいのです。
Elasticsearchの設定
設定する主なプロパティーは、Elasticsearchクラスターに接続するためのURLです。
典型的なクラスタ化されたElasticsearchサービスの場合、サンプル設定は次のようになります:
# configure the Elasticsearch client for a cluster of two nodes
quarkus.elasticsearch.hosts = elasticsearch1:9200,elasticsearch2:9200
このケースでは、localhost上で動作する単一のインスタンスを使用しています:
# configure the Elasticsearch client for a single instance on localhost
quarkus.elasticsearch.hosts = localhost:9200
より高度な設定が必要な場合は、このガイドの最後に、サポートされている設定プロパティーの包括的なリストがあります。
Dev Services
QuarkusはDev Servicesと呼ばれる機能をサポートしており、様々なコンテナを設定なしで起動することができます。Elasticsearchの場合、このサポートはデフォルトのElasticsearch接続にまで及んでいます。実質的にどういうことかというと、 quarkus.elasticsearch.hosts
を設定していない場合、Quarkusはテストや開発モードの実行時に自動的にElasticsearchコンテナを起動し、自動的に接続を設定します。
製品版アプリケーションの実行時には、通常通りElasticsearch接続の設定が必要です。 application.properties
に製品版データベース設定を含め、Dev Servicesを引き続き使用したい場合は、 %prod.
プロファイルを使用してElasticsearch設定を定義することをお勧めします。
興味のある方は、 Hibernate Search with 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 jakarta.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
アプリケーションの実行
それでは、アプリケーションをdevモードで起動してみましょう:
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
Elasticsearch Javaクライアントの使用
ここでは、低レベルのものではなく、Elasticsearch Java Client を使用したバージョンの FruitService
を紹介します:
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.acme.elasticsearch.Fruit;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
@ApplicationScoped
public class FruitService {
@Inject
ElasticsearchClient client; (1)
public void index(Fruit fruit) throws IOException {
IndexRequest<Fruit> request = IndexRequest.of( (2)
b -> b.index("fruits")
.id(fruit.id)
.document(fruit)); (3)
client.index(request); (4)
}
public Fruit get(String id) throws IOException {
GetRequest getRequest = GetRequest.of(
b -> b.index("fruits")
.id(id));
GetResponse<Fruit> getResponse = client.get(getRequest, Fruit.class);
if (getResponse.found()) {
return getResponse.source();
}
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 = SearchRequest.of(
b -> b.index("fruits")
.query(QueryBuilders.match().field(term).query(FieldValue.of(match)).build()._toQuery()));
SearchResponse<Fruit> searchResponse = client.search(searchRequest, Fruit.class);
HitsMetadata<Fruit> hits = searchResponse.hits();
return hits.hits().stream().map(hit -> hit.source()).collect(Collectors.toList());
}
}
1 | サービス内部に ElasticsearchClient を注入します。 |
2 | ビルダーを使って、Elasticsearchのインデックスリクエストを作成します。 |
3 | Java APIクライアントにはシリアライズ層があるため、オブジェクトを直接リクエストに渡します。 |
4 | Elasticsearchにリクエストを送信します。 |
Hibernate Search Elasticsearch
Quarkusは、 quarkus-hibernate-search-orm-elasticsearch
エクステンションによって、ElasticsearchによるHibernate Searchをサポートしています。
Hibernate Search Elasticsearchは、Jakarta PersistenceエンティティをElasticsearchクラスタに同期させ、Hibernate Search APIを使用してElasticsearchクラスタを照会する方法を提供します。
興味のある方は、 Hibernate Search with Elasticsearchガイド を参考にしてください。
クラスターヘルスチェック
quarkus-smallrye-health
エクステンションを使用している場合、両エクステンションは自動的にreadinessヘルスチェックを追加して、クラスタの健全性を検証します。
そのため、アプリケーションの /q/health/ready
エンドポイントにアクセスすると、クラスタのステータスに関する情報を得ることができます。これはクラスタヘルスエンドポイントを使用しており、クラスタのステータスが red の場合、またはクラスタが利用できない場合、チェックはダウンします。
この動作は、 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で低レベルのRESTクライアントやElasticsearchのJavaクライアントからElasticsearchクラスタにアクセスするのは簡単です。簡単な設定、CDI統合、ネイティブサポートが提供されている為です。
設定リファレンス
ビルド時に固定される設定プロパティ - その他の設定プロパティは実行時にオーバーライド可能です。
型 |
デフォルト |
|
---|---|---|
Whether a health check is published in case the smallrye-health extension is present. Environment variable: |
boolean |
|
The list of hosts of the Elasticsearch servers. Environment variable: |
list of host:port |
|
The protocol to use when contacting Elasticsearch servers. Set to "https" to enable SSL/TLS. Environment variable: |
string |
|
The username for basic HTTP authentication. Environment variable: |
string |
|
The password for basic HTTP authentication. Environment variable: |
string |
|
The connection timeout. Environment variable: |
|
|
The socket timeout. Environment variable: |
|
|
The maximum number of connections to all the Elasticsearch servers. Environment variable: |
int |
|
The maximum number of connections per Elasticsearch server. Environment variable: |
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: |
int |
|
Defines if automatic discovery is enabled. Environment variable: |
boolean |
|
Refresh interval of the node list. Environment variable: |
|
期間フォーマットについて
期間のフォーマットは標準の 数値で始まる期間の値を指定することもできます。この場合、値が数値のみで構成されている場合、コンバーターは値を秒として扱います。そうでない場合は、 |