Connecting to an Elasticsearch cluster
Elasticsearchはよく知られた全文検索エンジンであり、NoSQLデータストアです。
このガイドでは、RESTサービスをElasticsearchクラスタと接続する方法を紹介します。
Quarkusは、Elasticsearchにアクセスする2つの方法を提供します:
-
低レベルRESTクライアント
-
Elasticsearch Java クライアント
|
以前は「ハイレベルRESTクライアント」用の3つ目のQuarkusエクステンションが存在しましたが、このクライアントはElasticによって非推奨とされ、ライセンス上の問題があるため削除されました。 |
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOMEが適切に設定されていること -
Apache Maven 3.9.16
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
-
Elasticsearchがインストールされているか、Dockerがインストールされていること
アーキテクチャ
このガイドで構築されるアプリケーションは非常にシンプルです: ユーザーはフォームを使用してリストに要素を追加することができ、リストは更新されます。
ブラウザとサーバー間の情報はすべてJSON形式になっています。
要素はElasticsearchに格納されます。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-Dパラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=elasticsearch-quickstart"
このコマンドは、Quarkus REST(旧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 java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import co.elastic.clients.transport.rest5_client.low_level.Request;
import co.elastic.clients.transport.rest5_client.low_level.Response;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
@ApplicationScoped
public class FruitService {
@Inject
Rest5Client 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 void index(List<Fruit> list) throws IOException {
var entityList = new ArrayList<JsonObject>();
for (var fruit : list) {
entityList.add(new JsonObject().put("index", new JsonObject()(5)
.put("_index", "fruits").put("_id", fruit.id)));
entityList.add(JsonObject.mapFrom(fruit));
}
Request request = new Request(
"POST", "fruits/_bulk?pretty");
request.setEntity(new StringEntity(
toNdJsonString(entityList),(6)
ContentType.create("application/x-ndjson")));(7)
restClient.performRequest(request);
}
public void delete(List<String> identityList) throws IOException {
var entityList = new ArrayList<JsonObject>();
for (var id : identityList) {
entityList.add(new JsonObject().put("delete",
new JsonObject().put("_index", "fruits").put("_id", id)));(8)
}
Request request = new Request(
"POST", "fruits/_bulk?pretty");
request.setEntity(new StringEntity(
toNdJsonString(entityList),
ContentType.create("application/x-ndjson")));
restClient.performRequest(request);
}
public Fruit get(String id) throws IOException, ParseException {
Request request = new Request(
"GET",
"/fruits/_doc/" + id);
Response response = restClient.performRequest(request);
String responseBody = EntityUtils.toString(response.getEntity());
JsonObject json = new JsonObject(responseBody); (9)
return json.getJsonObject("_source").mapTo(Fruit.class);
}
public List<Fruit> searchByColor(String color) throws IOException, ParseException {
return search("color", color);
}
public List<Fruit> searchByName(String name) throws IOException, ParseException {
return search("name", name);
}
private List<Fruit> search(String term, String match) throws IOException, ParseException {
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;
}
private static String toNdJsonString(List<JsonObject> objects) {
return objects.stream()
.map(JsonObject::encode)
.collect(Collectors.joining("\n", "", "\n"));
}
}
| 1 | We inject an Elasticsearch low level Rest5Client into our service. |
| 2 | Elasticsearchリクエストを作成します。 |
| 3 | Elasticsearch に送る前にオブジェクトをシリアライズするために Vert.x JsonObject を使用しています。オブジェクトを JSON にシリアライズするために好きなものを使用することができます。 |
| 4 | Elasticsearchにリクエスト(ここではインデックス作成のリクエスト)を送信します。 |
| 5 | オブジェクトのコレクションを index するときは、 index、 create、または update アクションを使用する必要があります。 |
| 6 | toNdJsonString(entityList) を使用して、以下のような出力を生成します。
|
| 7 | 一括リクエストに対して検索バックエンドが期待するコンテントタイプを渡します。 |
| 8 | バルクAPIの削除操作のJSONには、すでに必要な情報がすべて含まれています。したがって、バルクAPIのリクエストボディには、この操作に続くリクエストボディはありません。
|
| 9 | Elasticsearchからオブジェクトをデシリアライズするために、再びVert.x JsonObjectを使用します。 |
では、次のように org.acme.elasticsearch.FruitResource クラスを作成します:
package org.acme.elasticsearch;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.UUID;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.BadRequestException;
@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();
}
@Path("bulk")
@DELETE
public Response delete(List<String> identityList) throws IOException {
fruitService.delete(identityList);
return Response.ok().build();
}
@Path("bulk")
@POST
public Response index(List<Fruit> list) throws IOException {
fruitService.index(list);
return Response.ok().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設定を定義することをお勧めします。
詳細については、 Dev Services for Elasticsearch ガイド をご覧ください。
Elasticsearchのプログラムによる設定
On top of the parametric configuration, you can also programmatically apply additional configuration to the client by implementing a ElasticsearchClientConfigConfigurer and annotating it with ElasticsearchClientConfig. You may provide multiple implementations and configuration provided by each implementation will be applied in a randomly ordered cascading manner.
例えば、HTTPレイヤでTLS用に設定されているElasticsearchクラスタにアクセスする場合、クライアントはElasticsearchが使用している証明書を信頼する必要があります。以下は、Elasticsearchが使用している証明書に署名したCAの証明書がPKCS#12のキーストアで利用可能な場合に、クライアントがそのCAの証明書を信頼するように設定する例です。
import io.quarkus.elasticsearch.restclient.lowlevel.ElasticsearchClientConfig;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.ssl.SSLContexts;
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 ElasticsearchClientConfigConfigurer {
@Override
public void accept(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.setConnectionManager(
PoolingAsyncClientConnectionManagerBuilder
.create().setTlsStrategy(
ClientTlsStrategyBuilder.create()
.setSslContext(sslContext).buildAsync()
).build()
);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
この例の詳細については、 Elasticsearchのドキュメント を参照してください。
|
|
Elasticsearchクラスターの実行
デフォルトでは、Elasticsearchクライアントはポート9200(Elasticsearchのデフォルトポート)でローカルのElasticsearchクラスターにアクセスするように設定されているので、このポートでローカルで実行中のインスタンスがある場合、テストできるようにするためにやるべきことは何もありません!
Dockerを使ってElasticsearchインスタンスを起動したい場合は、以下のコマンドで起動します:
docker run --name elasticsearch -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms512m -Xmx512m"\
-e "cluster.routing.allocation.disk.threshold_enabled=false" -e "xpack.security.enabled=false"\
--rm -p 9200:9200 docker.io/elastic/elasticsearch:9.4.1
アプリケーションの実行
それでは、アプリケーションを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 java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.IndexRequest;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.core.GetRequest;
import co.elastic.clients.elasticsearch.core.GetResponse;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.BulkRequest;
import co.elastic.clients.elasticsearch.core.BulkResponse;
@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 void index(List<Fruit> list) throws IOException {
BulkRequest.Builder br = new BulkRequest.Builder();
for (var fruit : list) {
br.operations(op -> op
.index(idx -> idx.index("fruits").id(fruit.id).document(fruit)));
}
BulkResponse result = client.bulk(br.build());
if (result.errors()) {
throw new RuntimeException("The indexing operation encountered errors.");
}
}
public void delete(List<String> list) throws IOException {
BulkRequest.Builder br = new BulkRequest.Builder();
for (var id : list) {
br.operations(op -> op.delete(idx -> idx.index("fruits").id(id)));
}
BulkResponse result = client.bulk(br.build());
if (result.errors()) {
throw new RuntimeException("The indexing operation encountered errors.");
}
}
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.native.enabled=true
実行は ./target/elasticsearch-low-level-client-quickstart-1.0-SNAPSHOT-runner を実行するだけで簡単です。
その後、ブラウザで http://localhost:8080/fruits.html を開いてアプリケーションを使用します。
まとめ
Quarkusで低レベルのRESTクライアントやElasticsearchのJavaクライアントからElasticsearchクラスタにアクセスするのは簡単です。簡単な設定、CDI統合、ネイティブサポートが提供されている為です。
設定リファレンス
ビルド時に固定される設定プロパティ - 他のすべての設定プロパティは、実行時にオーバーライド可能
Configuration property |
タイプ |
デフォルト |
||
|---|---|---|---|---|
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 API key for 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 |
|
|||
タイプ |
デフォルト |
|||
Whether this Dev Service should start with the application in dev mode or tests. Dev Services are enabled by default
unless connection configuration (e.g. Environment variable: Show more |
boolean |
|||
Optional fixed port the dev service will listen to. If not defined, the port will be chosen randomly. Environment variable: Show more |
int |
|||
The Elasticsearch distribution to use. Defaults to a distribution inferred from the explicitly configured Environment variable: Show more |
|
|||
The Elasticsearch container image to use. Defaults depend on the configured
Environment variable: Show more |
string |
|||
The value for the ES_JAVA_OPTS env variable. Environment variable: Show more |
string |
|
||
Whether the Elasticsearch server managed by Quarkus Dev Services is shared. When shared, Quarkus looks for running containers using label-based service discovery. If a matching container is found, it is used, and so a second one is not started. Otherwise, Dev Services for Elasticsearch starts a new container. The discovery uses the Container sharing is only used in dev mode. Environment variable: Show more |
boolean |
|
||
The value of the This property is used when This property is used when you need multiple shared Elasticsearch servers. Environment variable: Show more |
string |
|
||
Environment variables that are passed to the container. Environment variable: Show more |
Map<String,String> |
|||
Whether to keep Dev Service containers running after a dev mode session or test suite execution to reuse them in the next dev mode session or test suite execution. Within a dev mode session or test suite execution, Quarkus will always reuse Dev Services as long as their configuration (username, password, environment, port bindings, …) did not change. This feature is specifically about keeping containers running when Quarkus is not running to reuse them across runs.
This configuration property is set to Environment variable: Show more |
boolean |
|
|
期間フォーマットについて
期間の値を書くには、標準の 数字で始まる簡略化した書式を使うこともできます:
その他の場合は、簡略化されたフォーマットが解析のために
|