The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

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プロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。

コマンドラインインタフェース
quarkus create app org.acme:elasticsearch-quickstart \
    --stream=2.16 \
    --extension='resteasy-reactive-jackson,elasticsearch-rest-client' \
    --no-code
cd elasticsearch-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストール方法や使用方法については、 Quarkus CLIガイド を参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.16.12.Final:create \
    -DplatformVersion=2.16 \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=elasticsearch-quickstart \
    -Dextensions='resteasy-reactive-jackson,elasticsearch-rest-client' \
    -DnoCode
cd elasticsearch-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

このコマンドは、RESTEasy/JAX-RS、Jackson、およびElasticsearchの低レベルクライアント拡張をインポートするMaven構造を生成します。この後、 quarkus-elasticsearch-rest-client のエクステンションがビルドファイルに追加されています。

代わりに高レベルクライアントを使いたい場合は、 elasticsearch-rest-client のエクステンションを elasticsearch-rest-high-level-client のエクステンションで置き換えてください。

ここでは JSON-B などではなく resteasy-jackson エクステンションを使用しています。これは Vert.x JsonObject ヘルパーを使用して Elasticsearch との間でオブジェクトをシリアライズ/デシリアライズし、Jackson を使用するからです。

新しいプロジェクトを生成したくない場合は、以下の依存関係をビルドファイルに追加してください。

Elasticsearchの低レベルクライアントの場合は、以下を追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-elasticsearch-rest-client</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-elasticsearch-rest-client")

Elasticsearchの高レベルクライアントの場合は、以下を追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-elasticsearch-rest-high-level-client</artifactId>
</dependency>
build.gradle
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;
    }
}

この例では、次のことに注意してください:

  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 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のドキュメント を参照してください。

@ElasticsearchClientConfig と書かれたクラスは、デフォルトでアプリケーション・スコープ付きのCDI Beanになります。異なるスコープを希望する場合は、クラスレベルでスコープを上書きすることができます。

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

+

Maven
./mvnw quarkus:dev

+

Gradle
./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;
    }
}

この例では、次のことに注意してください:

  1. サービス内部にElasticsearch RestHighLevelClient を注入しています。

  2. Elasticsearchのインデックスリクエストを作成します。

  3. Elasticsearchに送信する前にオブジェクトをシリアライズするためにVert.x JsonObject を使用していますが、JSONにシリアライズしたものは何でも使えます。

  4. Elasticsearchにリクエストを送信します。

  5. 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.propertiesquarkus.elasticsearch.health.enabled プロパティーを false に設定することで無効にできます。

ネイティブ実行可能ファイルの構築

ネイティブ実行可能ファイルで両方のクライアントを使用することができます。

以下のコマンドでネイティブ実行可能ファイルをビルドすることができます。

コマンドラインインタフェース
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

実行は ./target/elasticsearch-low-level-client-quickstart-1.0-SNAPSHOT-runner を実行するだけで簡単です。

その後、ブラウザで http://localhost:8080/fruits.html を開いてアプリケーションを使用します。

まとめ

Quarkusでは、簡単な設定、CDIの統合、ネイティブサポートが提供されているため、低レベルまたは高レベルのクライアントからElasticsearchクラスターにアクセスすることが簡単にできます。

設定リファレンス

ビルド時に固定される設定プロパティ - 他のすべての設定プロパティは実行時にオーバーライド可能

Configuration property

タイプ

デフォルト

Whether a health check is published in case the smallrye-health extension is present.

Environment variable: QUARKUS_ELASTICSEARCH_HEALTH_ENABLED

Show more

boolean

true

The list of hosts of the Elasticsearch servers.

Environment variable: QUARKUS_ELASTICSEARCH_HOSTS

Show more

list of host:port

localhost:9200

The protocol to use when contacting Elasticsearch servers. Set to "https" to enable SSL/TLS.

Environment variable: QUARKUS_ELASTICSEARCH_PROTOCOL

Show more

string

http

The username for basic HTTP authentication.

Environment variable: QUARKUS_ELASTICSEARCH_USERNAME

Show more

string

The password for basic HTTP authentication.

Environment variable: QUARKUS_ELASTICSEARCH_PASSWORD

Show more

string

The connection timeout.

Environment variable: QUARKUS_ELASTICSEARCH_CONNECTION_TIMEOUT

Show more

Duration

1S

The socket timeout.

Environment variable: QUARKUS_ELASTICSEARCH_SOCKET_TIMEOUT

Show more

Duration

30S

The maximum number of connections to all the Elasticsearch servers.

Environment variable: QUARKUS_ELASTICSEARCH_MAX_CONNECTIONS

Show more

int

20

The maximum number of connections per Elasticsearch server.

Environment variable: QUARKUS_ELASTICSEARCH_MAX_CONNECTIONS_PER_ROUTE

Show more

int

10

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: QUARKUS_ELASTICSEARCH_IO_THREAD_COUNTS

Show more

int

Defines if automatic discovery is enabled.

Environment variable: QUARKUS_ELASTICSEARCH_DISCOVERY_ENABLED

Show more

boolean

false

Refresh interval of the node list.

Environment variable: QUARKUS_ELASTICSEARCH_DISCOVERY_REFRESH_INTERVAL

Show more

Duration

5M

期間フォーマットについて

期間のフォーマットは標準の java.time.Duration フォーマットを使用します。詳細は Duration#parse() javadoc を参照してください。

数値で始まる期間の値を指定することもできます。この場合、値が数値のみで構成されている場合、コンバーターは値を秒として扱います。そうでない場合は、 PT が暗黙的に値の前に付加され、標準の java.time.Duration 形式が得られます。