1. Overview

Quarkus is a modern framework that makes it easy and enjoyable to build high-performance applications. In this tutorial, we’ll explore how to integrate Quarkus with Elasticsearch, a well-known full-text search engine and NoSQL datastore.

2. Dependencies and Configuration

Once we have an Elasticsearch instance running on localhost, let’s add the dependencies to our Quarkus application:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-elasticsearch-rest-client</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-elasticsearch-java-client</artifactId>
    <version>${quarkus.version}</version>
</dependency>

We’ve added the quarkus-elasticsearch-rest-client dependency which brings us a low-level Elasticsearch REST client. Besides that, we’ve attached the quarkus-elasticsearch-java-client dependency which gives us the ability to use the Elasticsearch Java client. In our application, we can choose the option that best suits our needs. 

Next step, let’s add the Elasticsearch host to our application.properties file:

quarkus.elasticsearch.hosts=localhost:9200

Now we’re ready to start using Elasticsearch in our Quarkus application. All the necessary beans will be automatically created behind the scenes by ElasticsearchRestClientProducer and ElasticsearchJavaClientProducer.

3. Elasticsearch Low-Level REST Client

We can use the Elasticsearch low-level REST client to integrate our application with Elasticsearch. This gives us full control over the serialization and deserialization processes and allows us to build queries for Elasticsearch using JSON.

Let’s create a model that we want to index in our application:

public class StoreItem {
    private String id;
    private String name;
    private Long price;
    //getters and setters
}

In our model, we have a field for the document ID. Additionally, we’ve added a few more fields to facilitate searching.

Now, let’s add the method to index our StoreItem:

private void indexUsingRestClient() throws IOException, InterruptedException {
    iosPhone = new StoreItem();
    iosPhone.setId(UUID.randomUUID().toString());
    iosPhone.setPrice(1000L);
    iosPhone.setName("IOS smartphone");

    Request restRequest = new Request(
      "PUT",
      "/store-items/_doc/" + iosPhone.getId());
    restRequest.setJsonEntity(JsonObject.mapFrom(iosPhone).toString());
    restClient.performRequest(restRequest);
}

Here we’ve created a StoreItem with a random ID and specific name. Then we executed a PUT request to the /store-items/_doc/{id} path to index our document. Now we’ll create a method to verify how our documents are indexed and search them by various fields:

@Test
void givenRestClient_whenSearchInStoreItemsByName_thenExpectedDocumentsFound() throws Exception {
    indexUsingRestClient();

    Request request = new Request(
      "GET",
      "/store-items/_search");

    JsonObject termJson = new JsonObject().put("name", "IOS smartphone");
    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<StoreItem> results = new ArrayList<>(hits.size());

    for (int i = 0; i < hits.size(); i++) {
        JsonObject hit = hits.getJsonObject(i);
        StoreItem fruit = hit.getJsonObject("_source").mapTo(StoreItem.class);
        results.add(fruit);
    }

    assertThat(results)
      .hasSize(1)
      .containsExactlyInAnyOrder(iosPhone);
}

We indexed our StoreItem using the indexUsingRestClient() method. Then, we built the JSON query and executed the search request. We deserialized each search hit and verified if it contained our StoreItem.

With this, we’ve implemented basic integration with Elasticsearch in our Quarkus application using the low-level REST client. As we can see, we need to handle all the serialization and deserialization processes ourselves.

4. Elasticsearch Java Client

The Elasticsearch Java Client is a higher-level client. We can use its DSL syntax to create Elasticsearch queries more elegantly.

Let’s create a method to index our StoreItem using this client:

private void indexUsingElasticsearchClient() throws IOException, InterruptedException {
    androidPhone = new StoreItem();
    androidPhone.setId(UUID.randomUUID().toString());
    androidPhone.setPrice(500L);
    androidPhone.setName("Android smartphone");

    IndexRequest<StoreItem> request = IndexRequest.of(
      b -> b.index("store-items")
        .id(androidPhone.getId())
        .document(androidPhone));

    elasticsearchClient.index(request);
}

We’ve built another StoreItem and created the IndexRequest. Then we called the index() method of the Elasticsearch Java Client to execute the request.

Now let’s search our saved items:

@Test
void givenElasticsearchClient_whenSearchInStoreItemsByName_thenExpectedDocumentsFound() throws Exception {
    indexUsingElasticsearchClient();
    Query query = QueryBuilders.match()
      .field("name")
      .query(FieldValue.of("Android smartphone"))
      .build()
      ._toQuery();

    SearchRequest request = SearchRequest.of(
      b -> b.index("store-items")
        .query(query)
    );
    SearchResponse<StoreItem> searchResponse = elasticsearchClient
      .search(request, StoreItem.class);

    HitsMetadata<StoreItem> hits = searchResponse.hits();
    List<StoreItem> results = hits.hits().stream()
      .map(Hit::source)
      .collect(Collectors.toList());

    assertThat(results)
      .hasSize(1)
      .containsExactlyInAnyOrder(androidPhone);
}

We’ve indexed the new document using our indexUsingElasticsearchClient()  method. Then, we built the SearchRequest, executed it with the Elasticsearch Java Client, and collected all the hits into a list of StoreItem instances. We used the DSL syntax to create the query, so we didn’t have to worry about serialization and deserialization. 

5. Conclusion

As we can see, Quarkus offers excellent capabilities for integrating our application with Elasticsearch. To get started, all we need to do is add the extension dependency and provide a few lines of configuration. If we want more control, we can always define a client bean ourselves. Additionally, in this article, we explored how to use both the low-level REST client and the higher-level Java client for Elasticsearch in Quarkus.

As usual, the full source code can be found over on GitHub.