Connecting to an Elasticsearch cluster
Elasticsearch is a well known full text search engine and NoSQL datastore.
In this guide, we will see how you can get your REST services to use an Elasticsearch cluster.
Quarkus provides two ways of accessing Elasticsearch: via the lower level RestClient or via the RestHighLevelClient we will call them
the low level and the high level clients.
Pré-requisitos
Para concluir este guia, você precisa:
-
Cerca de 15 minutos
-
Um IDE
-
JDK 11+ instalado com 'JAVA_HOME' configurado adequadamente
-
Apache Maven 3.8.6
-
Opcionalmente, o Quarkus CLI se você quiser usá-lo
-
Opcionalmente, Mandrel ou GraalVM instalado e configurado apropriadamente se você quiser criar um executável nativo (ou Docker se você usar uma compilação de contêiner nativo)
-
Elasticsearch installed or Docker installed
Arquitetura
O aplicativo criado neste guia é bastante simples: o usuário pode adicionar elementos em uma lista usando um formulário e a lista é atualizada.
Todas as informações entre o navegador e o servidor são formatadas como JSON.
The elements are stored in Elasticsearch.
Criar o projeto Maven
Primeiro, precisamos de um novo projeto. Crie um novo projeto com o seguinte comando:
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.
If you want to use the high level client instead, replace the elasticsearch-rest-client extension by the elasticsearch-rest-high-level-client extension.
|
We use the |
If you don’t want to generate a new project, add the following dependencies to your build file.
For the Elasticsearch low level client, add:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elasticsearch-rest-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elasticsearch-rest-client")
For the Elasticsearch high level client, add:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elasticsearch-rest-high-level-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elasticsearch-rest-high-level-client")
Criando seu primeiro serviço JSON REST
Neste exemplo, criaremos um aplicativo para gerenciar uma lista de frutas.
Primeiro, vamos criar o bean Fruit da seguinte forma:
package org.acme.elasticsearch;
public class Fruit {
public String id;
public String name;
public String color;
}
Nada de especial. Um aspecto importante a ser observado é que a camada de serialização JSON exige um construtor padrão.
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;
}
}
In this example you can note the following:
-
We inject an Elasticsearch low level
RestClientinto our service. -
We create an Elasticsearch request.
-
We use Vert.x
JsonObjectto serialize the object before sending it to Elasticsearch, you can use whatever you want to serialize to JSON. -
We send the request (indexing request here) to Elasticsearch.
-
In order to deserialize the object from Elasticsearch, we again use 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");
}
}
}
A implementação é bastante simples e você só precisa definir seus endpoints usando as anotações JAX-RS e usar o FruitService para listar/adicionar novas frutas.
Configuring Elasticsearch
The main property to configure is the URL to connect to the Elasticsearch cluster.
Um exemplo de configuração deve ser semelhante a este:
# configure the Elasticsearch client for a cluster of two nodes
quarkus.elasticsearch.hosts = elasticsearch1:9200,elasticsearch2:9200
Neste exemplo, estamos usando uma única instância em execução no localhost:
# configure the Elasticsearch client for a single instance on localhost
quarkus.elasticsearch.hosts = localhost:9200
If you need a more advanced configuration, you can find the comprehensive list of supported configuration properties at the end of this guide.
Serviços de desenvolvimento (bancos de dados sem configuração)
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.
Programmatically Configuring Elasticsearch
On top of the parametric configuration, you can also programmatically apply additional configuration to the client by implementing a RestClientBuilder.HttpClientConfigCallback 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.
For example, when accessing an Elasticsearch cluster that is set up for TLS on the HTTP layer, the client needs to trust the certificate that Elasticsearch is using. The following is an example of setting up the client to trust the CA that has signed the certificate that Elasticsearch is using, when that CA certificate is available in a PKCS#12 keystore.
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.
|
Classes marked with |
Running an Elasticsearch cluster
As by default, the Elasticsearch client is configured to access a local Elasticsearch cluster on port 9200 (the default Elasticsearch port), if you have a local running instance on this port, there is nothing more to do before being able to test it!
If you want to use Docker to run an Elasticsearch instance, you can use the following command to launch one:
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
Executando a aplicação
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
Using the High Level REST Client
Quarkus provides support for the Elasticsearch High Level REST Client but keep in mind that it comes with some caveats:
-
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.
-
It is tied to a certain version of the Elasticsearch server: you cannot use a High Level REST Client version 7 to access a server version 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. |
Here is a version of the FruitService using the high level client instead of the low level one:
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;
}
}
In this example you can note the following:
-
We inject an Elasticsearch
RestHighLevelClientinside the service. -
We create an Elasticsearch index request.
-
We use Vert.x
JsonObjectto serialize the object before sending it to Elasticsearch, you can use whatever you want to serialize to JSON. -
We send the request to Elasticsearch.
-
In order to deserialize the object from Elasticsearch, we again use Vert.x
JsonObject.
Hibernate Search Elasticsearch
Quarkus supports Hibernate Search with Elasticsearch via the hibernate-search-orm-elasticsearch extension.
Hibernate Search Elasticsearch allows to synchronize your JPA entities to an Elasticsearch cluster and offers a way to query your Elasticsearch cluster using the Hibernate Search API.
If you’re interested in it, you can read the Hibernate Search with Elasticsearch guide.
Cluster Health Check
If you are using the quarkus-smallrye-health extension, both the extension will automatically add a readiness health check
to validate the health of the cluster.
So when you access the /q/health/ready endpoint of your application you will have information about the cluster status.
It uses the cluster health endpoint, the check will be down if the status of the cluster is red, or the cluster is not available.
This behavior can be disabled by setting the quarkus.elasticsearch.health.enabled property to false in your application.properties.
Criação de um executável nativo
You can use both clients in a native executable.
Você pode criar um executável nativo com o comando usual:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
Running it is as simple as executing ./target/elasticsearch-low-level-client-quickstart-1.0.0-SNAPSHOT-runner.
Em seguida, você pode direcionar seu navegador para http://localhost:8080/fruits.html e usar seu aplicativo.
Conclusion
Accessing an Elasticsearch cluster from a low level or a high level client is easy with Quarkus as it provides easy configuration, CDI integration and native support for it.
Referência de configuração
Propriedade de Configuração Fixa no Momento da Compilação - Todas as outras propriedades de configuração podem ser sobrepostas em tempo de execução.
Tipo |
Padrão |
|
|---|---|---|
Whether a health check is published in case the smallrye-health extension is present. Environment variable: Show more |
booleano |
|
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 |
booleano |
|
Refresh interval of the node list. Environment variable: Show more |
|
|
About the Duration format
The format for durations uses the standard You can also provide duration values starting with a number.
In this case, if the value consists only of a number, the converter treats the value as seconds.
Otherwise, |