Connecting to an Elasticsearch cluster
Elasticsearchはよく知られた全文検索エンジンであり、NoSQLデータストアです。
このガイドでは、RESTサービスでElasticsearchクラスターを使用する方法を見ていきます。
QuarkusはElasticsearchにアクセスするための2つの方法を提供しています:低レベルの RestClient
経由、または RestHighLevelClient
経由であり、低レベルと高レベルのクライアントと呼びます。
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 11+ がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.8.1+
-
使用したい場合、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
-
Elasticsearchがインストールされているか、Dockerがインストールされていること
アーキテクチャ
このガイドで構築されるアプリケーションは非常にシンプルです:ユーザーはフォームを使用してリストに要素を追加することができ、リストが更新されます。
ブラウザとサーバー間の情報はすべてJSON形式になっています。
要素はElasticsearchに格納されます。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
This command generates a Maven structure importing the RESTEasy Reactive/JAX-RS, Jackson, and the Elasticsearch low level client extensions. After this, the quarkus-elasticsearch-rest-client
extension has been added to your build file.
代わりに高レベルクライアントを使いたい場合は、 elasticsearch-rest-client
のエクステンションを elasticsearch-rest-high-level-client
のエクステンションで置き換えてください。
We use the |
新しいプロジェクトを生成したくない場合は、以下の依存関係をビルドファイルに追加してください。
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シリアライズレイヤーで必須であるということです。
Now create a org.acme.elasticsearch.FruitService
that will be the business layer of our application and store/load the fruits from the Elasticsearch instance. Here we use the low level client, if you want to use the high level client instead follow the instructions in the Using the High Level REST Client paragraph instead.
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
を使用します。
Now, create the org.acme.elasticsearch.FruitResource
class as follows:
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 supports a feature called Dev Services that allows you to start various containers without any config. In the case of Elasticsearch this support extends to the default Elasticsearch connection. What that means practically is that, if you have not configured quarkus.elasticsearch.hosts
, Quarkus will automatically start an Elasticsearch container when running tests or dev mode, and automatically configure the connection.
When running the production version of the application, the Elasticsearch connection needs to be configured as usual, so if you want to include a production database config in your application.properties
and continue to use Dev Services we recommend that you use the %prod.
profile to define your Elasticsearch settings.
For more information you can read the Dev Services for Elasticsearch guide.
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;
}
}
See Elasticsearch documentation for more details on this particular example.
|
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
アプリケーションの実行
Now let’s run our application via Quarkus dev mode:
+
quarkus dev
+
./mvnw quarkus:dev
+
./gradlew --console=plain quarkusDev
You can add new fruits to the list via the following curl command:
curl localhost:8080/fruits -d '{"name": "bananas", "color": "yellow"}' -H "Content-Type: application/json"
And search for fruits by name or color via the flowing curl command:
curl localhost:8080/fruits/search?color=yellow
高レベルRESTクライアントの使用
QuarkusはElasticsearch High Level REST Clientのサポートを提供していますが、いくつかの注意点があることを覚えておいてください。
-
It drags a lot of dependencies - especially Lucene -, which doesn’t fit well with Quarkus philosophy. The Elasticsearch team is aware of this issue, and it might improve sometime in the future.
-
これはElasticsearchサーバーの特定のバージョンに縛られています: 高レベルRESTクライアントのバージョン7を使用してサーバーのバージョン6にアクセスすることはできません。
Due to the license change made by Elastic for the Elasticsearch High Level REST Client, we are keeping in Quarkus the last Open Source version of this particular client, namely 7.10, and it won’t be upgraded to newer versions. Given this client was deprecated by Elastic and replaced by a new Open Source Java client, the Elasticsearch High Level REST Client extension is considered deprecated and will be removed from the Quarkus codebase at some point in the future. Note that contrary to the High Level REST client, we are using the latest version of the Low Level REST client (which is still Open Source), and, while we believe it should work, the situation is less than ideal and might cause some issues. Feel free to override the versions of the clients in your applications depending on your requirements, but be aware of the new licence of the High Level REST Client for versions 7.11+: it is not Open Source and has several usage restrictions. We will eventually provide an extension for the new Open Source Java client, but it will require changes in your applications as it is an entirely new client. |
ここでは、低レベルのクライアントの代わりに高レベルのクライアントを使用したバージョンの 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クラスターにクエリを発行する方法を提供しています。
If you’re interested in it, you can read the Hibernate Search with Elasticsearch guide.
クラスターヘルスチェック
quarkus-smallrye-health
エクステンションを使用している場合、どちらのエクステンションも、クラスターの健全性を検証するための readiness ヘルスチェックを自動的に追加します。
そのため、アプリケーションの /q/health/ready
エンドポイントにアクセスすると、クラスターの状態に関する情報を得ることができます。これはクラスターヘルスエンドポイントを使用しており、クラスターの状態が 赤 であったり、クラスターが利用できなかったりするとチェックが失敗します。
この動作は、 application.properties
の quarkus.elasticsearch.health.enabled
プロパティーを false
に設定することで無効にできます。
ネイティブ実行可能ファイルの構築
ネイティブ実行可能ファイルで両方のクライアントを使用することができます。
You can build a native executable with the usual command:
quarkus build --native
./mvnw package -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: |
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: |
|
期間フォーマットについて
期間のフォーマットは標準の 数値で始まる期間の値を指定することもできます。この場合、値が数値のみで構成されている場合、コンバーターは値を秒として扱います。そうでない場合は、 |