From c679d63747a2656396d2aaf54e43d6277a5e5324 Mon Sep 17 00:00:00 2001 From: Akulov S V Date: Thu, 8 Aug 2024 23:30:58 +0300 Subject: [PATCH] Elasticsearch docs update Added code snippet about Bulk API usage via low level RestClient expanded Bulk API usage description rewritten low level Rest Client example added Java API example added awaitility added test cases for bulk operations --- docs/src/main/asciidoc/elasticsearch.adoc | 157 ++++++++++++++++-- .../elasticsearch-java-client/pom.xml | 10 ++ .../quarkus/it/elasticsearch/java/Fruit.java | 16 ++ .../it/elasticsearch/java/FruitResource.java | 20 +++ .../it/elasticsearch/java/FruitService.java | 44 ++++- .../it/elasticsearch/FruitResourceTest.java | 111 ++++++++----- .../elasticsearch-rest-client/pom.xml | 10 ++ .../io/quarkus/it/elasticsearch/Fruit.java | 17 ++ .../it/elasticsearch/FruitResource.java | 20 +++ .../it/elasticsearch/FruitService.java | 45 +++++ .../it/elasticsearch/FruitResourceTest.java | 78 ++++++--- 11 files changed, 438 insertions(+), 90 deletions(-) diff --git a/docs/src/main/asciidoc/elasticsearch.adoc b/docs/src/main/asciidoc/elasticsearch.adoc index 2fb01d9eba966..feffac5f93b66 100644 --- a/docs/src/main/asciidoc/elasticsearch.adoc +++ b/docs/src/main/asciidoc/elasticsearch.adoc @@ -126,10 +126,13 @@ 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 org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; @@ -151,13 +154,49 @@ public class FruitService { restClient.performRequest(request); //<4> } + public void index(List list) throws IOException { + + var entityList = new ArrayList(); + + 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 identityList) throws IOException { + + var entityList = new ArrayList(); + + 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 { 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> + JsonObject json = new JsonObject(responseBody); //<9> return json.getJsonObject("_source").mapTo(Fruit.class); } @@ -191,13 +230,42 @@ public class FruitService { } return results; } + + private static String toNdJsonString(List objects) { + return objects.stream() + .map(JsonObject::encode) + .collect(Collectors.joining("\n", "", "\n")); + } } ---- <1> We inject an Elasticsearch low level `RestClient` into our service. <2> We create an Elasticsearch request. <3> We use Vert.x `JsonObject` to serialize the object before sending it to Elasticsearch, you can use whatever you want to serialize your objects to JSON. <4> We send the request (indexing request here) to Elasticsearch. -<5> In order to deserialize the object from Elasticsearch, we again use Vert.x `JsonObject`. +<5> As we `index` collection of objects we should use `index`, `create` or `update` action. +<6> We use `toNdJsonString(entityList)` call to produce output like below ++ +[source, json] +---- +{"index", {"_index" : "fruits", "_id", "1"}} +{"id": "1", "name": "apple", "color": "red"} +... ... ... ... +{"create", {"_index" : "fruits", "_id", "N"}} +{"id": "N", "name": "dragonfruit", "color": "pink"} + +---- +<7> Pass the content type that is expected by the search backend for bulk requests. +<8> The bulk API's delete operation JSON already contains all the required information; hence, there is no request body following this operation in the Bulk API request body. ++ +[source, json] +---- +{"delete", {"_index" : "fruits", "_id", "1"}} +{"delete", {"_index" : "fruits", "_id", "2"}} +... ... ... ... +{"delete", {"_index" : "fruits", "_id", "N"}} + +---- +<9> In order to deserialize the object from Elasticsearch, we again use Vert.x JsonObject. Now, create the `org.acme.elasticsearch.FruitResource` class as follows: @@ -205,18 +273,20 @@ Now, create the `org.acme.elasticsearch.FruitResource` class as follows: ---- package org.acme.elasticsearch; -import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Response; import java.io.IOException; import java.net.URI; import java.util.List; import java.util.UUID; -import org.jboss.resteasy.reactive.RestQuery; +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 { @@ -233,6 +303,20 @@ public class FruitResource { return Response.created(URI.create("/fruits/" + fruit.id)).build(); } + @Path("bulk") + @DELETE + public Response delete(List identityList) throws IOException { + fruitService.delete(identityList); + return Response.ok().build(); + } + + @Path("bulk") + @POST + public Response index(List list) throws IOException { + fruitService.index(list); + return Response.ok().build(); + } + @GET @Path("/{id}") public Fruit get(String id) throws IOException { @@ -384,18 +468,26 @@ Here is a version of the `FruitService` using the Elasticsearch Java Client inst [source,java] ---- +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.*; +import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.HitsMetadata; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.acme.elasticsearch.Fruit; - -import java.io.IOException; -import java.util.List; -import java.util.stream.Collectors; +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 { @@ -410,6 +502,37 @@ public class FruitService { client.index(request); // <4> } + public void index(List 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 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") diff --git a/integration-tests/elasticsearch-java-client/pom.xml b/integration-tests/elasticsearch-java-client/pom.xml index c370065d77d14..9b998438c9687 100644 --- a/integration-tests/elasticsearch-java-client/pom.xml +++ b/integration-tests/elasticsearch-java-client/pom.xml @@ -37,6 +37,16 @@ rest-assured test + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + diff --git a/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/Fruit.java b/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/Fruit.java index f29ed26e50895..5ffd102b861aa 100644 --- a/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/Fruit.java +++ b/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/Fruit.java @@ -1,7 +1,23 @@ package io.quarkus.it.elasticsearch.java; +import java.util.Objects; + public class Fruit { public String id; public String name; public String color; + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Fruit fruit)) + return false; + return Objects.equals(id, fruit.id) && Objects.equals(name, fruit.name) && Objects.equals(color, fruit.color); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, color); + } } diff --git a/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitResource.java b/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitResource.java index 4d27587777d05..fea59d6f93eff 100644 --- a/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitResource.java +++ b/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitResource.java @@ -7,14 +7,20 @@ import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @Path("/fruits") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) public class FruitResource { @Inject FruitService fruitService; @@ -54,4 +60,18 @@ public List searchUnsafe(@QueryParam("json") String json) throws IOExcept return fruitService.searchWithJson(json); } + @Path("bulk") + @DELETE + public Response delete(List identityList) throws IOException { + fruitService.delete(identityList); + return Response.ok().build(); + } + + @Path("bulk") + @POST + public Response index(List list) throws IOException { + fruitService.index(list); + return Response.ok().build(); + } + } diff --git a/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitService.java b/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitService.java index 2c9427e982ece..b4eea5603a577 100644 --- a/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitService.java +++ b/integration-tests/elasticsearch-java-client/src/main/java/io/quarkus/it/elasticsearch/java/FruitService.java @@ -11,7 +11,14 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.FieldValue; import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; -import co.elastic.clients.elasticsearch.core.*; +import co.elastic.clients.elasticsearch.core.BulkRequest; +import co.elastic.clients.elasticsearch.core.BulkResponse; +import co.elastic.clients.elasticsearch.core.GetRequest; +import co.elastic.clients.elasticsearch.core.GetResponse; +import co.elastic.clients.elasticsearch.core.IndexRequest; +import co.elastic.clients.elasticsearch.core.SearchRequest; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.HitsMetadata; @ApplicationScoped @@ -53,7 +60,7 @@ private List search(String term, String match) throws IOException { SearchResponse searchResponse = client.search(searchRequest, Fruit.class); HitsMetadata hits = searchResponse.hits(); - return hits.hits().stream().map(hit -> hit.source()).collect(Collectors.toList()); + return hits.hits().stream().map(Hit::source).collect(Collectors.toList()); } public List searchWithJson(String json) throws IOException { @@ -63,6 +70,37 @@ public List searchWithJson(String json) throws IOException { } SearchResponse searchResponse = client.search(searchRequest, Fruit.class); HitsMetadata hits = searchResponse.hits(); - return hits.hits().stream().map(hit -> hit.source()).collect(Collectors.toList()); + return hits.hits().stream().map(Hit::source).collect(Collectors.toList()); + } + + public void index(List 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 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."); + } } } diff --git a/integration-tests/elasticsearch-java-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java b/integration-tests/elasticsearch-java-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java index e4e0f80ff5217..741b2e7d86baf 100644 --- a/integration-tests/elasticsearch-java-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java +++ b/integration-tests/elasticsearch-java-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java @@ -2,13 +2,13 @@ import static io.restassured.RestAssured.get; import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.List; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -23,12 +23,13 @@ public class FruitResourceTest { }; @Test - public void testEndpoint() throws InterruptedException { + public void testEndpoint() { // create a Fruit Fruit fruit = new Fruit(); fruit.id = "1"; fruit.name = "Apple"; fruit.color = "Green"; + given() .contentType("application/json") .body(fruit) @@ -36,47 +37,67 @@ public void testEndpoint() throws InterruptedException { .then() .statusCode(201); - // get the Fruit - Fruit result = get("/fruits/1").as(Fruit.class); - assertNotNull(result); - assertEquals("1", result.id); - assertEquals("Apple", result.name); - assertEquals("Green", result.color); - - // wait a few ms for the indexing to happened - Thread.sleep(1000); - - // search the Fruit - List results = get("/fruits/search?color=Green").as(LIST_OF_FRUIT_TYPE_REF); - assertNotNull(results); - assertFalse(results.isEmpty()); - assertEquals("1", results.get(0).id); - assertEquals("Apple", results.get(0).name); - assertEquals("Green", results.get(0).color); - - results = get("/fruits/search?name=Apple").as(LIST_OF_FRUIT_TYPE_REF); - assertNotNull(results); - assertFalse(results.isEmpty()); - assertEquals("1", results.get(0).id); - assertEquals("Apple", results.get(0).name); - assertEquals("Green", results.get(0).color); - - results = RestAssured.given().queryParam("json", - "{\n" + - "\"query\": {\n" + - " \"prefix\": {\n" + - " \"name\": {\n" + - " \"value\": \"app\"\n" + - " }\n" + - " }\n" + - "}\n" + - "}\n") - .get("/fruits/search/unsafe").as(LIST_OF_FRUIT_TYPE_REF); - assertNotNull(results); - assertFalse(results.isEmpty()); - assertEquals("1", results.get(0).id); - assertEquals("Apple", results.get(0).name); - assertEquals("Green", results.get(0).color); + await().atMost(2, TimeUnit.SECONDS).pollDelay(500, TimeUnit.MILLISECONDS).untilAsserted(() -> { + // get the Fruit + Fruit result = get("/fruits/1").as(Fruit.class); + assertThat(result).isNotNull().isEqualTo(fruit); + + // search the Fruit + List results = get("/fruits/search?color=Green").as(LIST_OF_FRUIT_TYPE_REF); + assertThat(results).hasSize(1).contains(fruit); + + results = get("/fruits/search?name=Apple").as(LIST_OF_FRUIT_TYPE_REF); + assertThat(results).hasSize(1).contains(fruit); + + results = RestAssured.given().queryParam("json", + "{\n" + + "\"query\": {\n" + + " \"prefix\": {\n" + + " \"name\": {\n" + + " \"value\": \"app\"\n" + + " }\n" + + " }\n" + + "}\n" + + "}\n") + .get("/fruits/search/unsafe").as(LIST_OF_FRUIT_TYPE_REF); + assertThat(results).hasSize(1).contains(fruit); + }); + + //create new fruit index via bulk operation + Fruit pomegranate = new Fruit(); + pomegranate.id = "2"; + pomegranate.name = "Pomegranate"; + pomegranate.color = "Red"; + + List fruits = List.of(pomegranate); + + given() + .contentType("application/json") + .body(fruits) + .when().post("/fruits/bulk") + .then() + .statusCode(200); + + await().atMost(2, TimeUnit.SECONDS).pollDelay(500, TimeUnit.MILLISECONDS).untilAsserted(() -> { + Fruit result = get("/fruits/2").as(Fruit.class); + assertThat(result).isNotNull().isEqualTo(pomegranate); + + }); + + given() + .contentType("application/json") + .body(List.of(pomegranate.id)) + .when().delete("/fruits/bulk") + .then() + .statusCode(200); + + await().atMost(2, TimeUnit.SECONDS).pollDelay(500, TimeUnit.MILLISECONDS).untilAsserted(() -> { + // search the removed Fruit should yield no results + // using a simple get-doc-by-id won't work since we will get an 500 error response: + List results = get("/fruits/search?color=Red").as(LIST_OF_FRUIT_TYPE_REF); + assertThat(results).isEmpty(); + }); + } @Test diff --git a/integration-tests/elasticsearch-rest-client/pom.xml b/integration-tests/elasticsearch-rest-client/pom.xml index 64720ca0c3079..a22dc4e7ff939 100644 --- a/integration-tests/elasticsearch-rest-client/pom.xml +++ b/integration-tests/elasticsearch-rest-client/pom.xml @@ -37,6 +37,16 @@ rest-assured test + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + diff --git a/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/Fruit.java b/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/Fruit.java index ff28481a534bf..1690971933151 100644 --- a/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/Fruit.java +++ b/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/Fruit.java @@ -1,7 +1,24 @@ package io.quarkus.it.elasticsearch; +import java.util.Objects; + public class Fruit { public String id; public String name; public String color; + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Fruit)) + return false; + Fruit fruit = (Fruit) o; + return Objects.equals(id, fruit.id) && Objects.equals(name, fruit.name) && Objects.equals(color, fruit.color); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, color); + } } diff --git a/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitResource.java b/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitResource.java index 4cec914a9ae37..43ab5935f21b2 100644 --- a/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitResource.java +++ b/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitResource.java @@ -7,14 +7,20 @@ import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @Path("/fruits") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) public class FruitResource { @Inject FruitService fruitService; @@ -46,4 +52,18 @@ public List search(@QueryParam("name") String name, @QueryParam("color") } } + @Path("bulk") + @DELETE + public Response delete(List identityList) throws IOException { + fruitService.delete(identityList); + return Response.ok().build(); + } + + @Path("bulk") + @POST + public Response index(List list) throws IOException { + fruitService.index(list); + return Response.ok().build(); + } + } diff --git a/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitService.java b/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitService.java index aba58dad86b04..7ce74da36334a 100644 --- a/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitService.java +++ b/integration-tests/elasticsearch-rest-client/src/main/java/io/quarkus/it/elasticsearch/FruitService.java @@ -3,10 +3,13 @@ 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 org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; @@ -68,4 +71,46 @@ private List search(String term, String match) throws IOException { } return results; } + + public void index(List list) throws IOException { + + var entityList = new ArrayList(); + + for (var fruit : list) { + + entityList.add(new JsonObject().put("index", new JsonObject() + .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), + ContentType.create("application/x-ndjson"))); + restClient.performRequest(request); + } + + public void delete(List identityList) throws IOException { + + var entityList = new ArrayList(); + + for (var id : identityList) { + entityList.add(new JsonObject().put("delete", + new JsonObject().put("_index", "fruits").put("_id", id))); + } + + Request request = new Request( + "POST", "fruits/_bulk?pretty"); + request.setEntity(new StringEntity( + toNdJsonString(entityList), + ContentType.create("application/x-ndjson"))); + restClient.performRequest(request); + } + + private static String toNdJsonString(List objects) { + return objects.stream() + .map(JsonObject::encode) + .collect(Collectors.joining("\n", "", "\n")); + } } diff --git a/integration-tests/elasticsearch-rest-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java b/integration-tests/elasticsearch-rest-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java index e4745164ae800..7b4d86b1483e2 100644 --- a/integration-tests/elasticsearch-rest-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java +++ b/integration-tests/elasticsearch-rest-client/src/test/java/io/quarkus/it/elasticsearch/FruitResourceTest.java @@ -2,10 +2,12 @@ import static io.restassured.RestAssured.get; import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import java.util.List; +import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @@ -17,12 +19,13 @@ public class FruitResourceTest { }; @Test - public void testEndpoint() throws InterruptedException { + public void testEndpoint() { // create a Fruit Fruit fruit = new Fruit(); fruit.id = "1"; fruit.name = "Apple"; fruit.color = "Green"; + given() .contentType("application/json") .body(fruit) @@ -30,28 +33,53 @@ public void testEndpoint() throws InterruptedException { .then() .statusCode(201); - // get the Fruit - Fruit result = get("/fruits/1").as(Fruit.class); - Assertions.assertNotNull(result); - Assertions.assertEquals("1", result.id); - Assertions.assertEquals("Apple", result.name); - Assertions.assertEquals("Green", result.color); - - // wait a few ms for the indexing to happened - Thread.sleep(1000); - - // search the Fruit - List results = get("/fruits/search?color=Green").as(LIST_OF_FRUIT_TYPE_REF); - Assertions.assertNotNull(results); - Assertions.assertFalse(results.isEmpty()); - Assertions.assertEquals("1", results.get(0).id); - Assertions.assertEquals("Apple", results.get(0).name); - Assertions.assertEquals("Green", results.get(0).color); - results = get("/fruits/search?name=Apple").as(LIST_OF_FRUIT_TYPE_REF); - Assertions.assertNotNull(results); - Assertions.assertFalse(results.isEmpty()); - Assertions.assertEquals("1", results.get(0).id); - Assertions.assertEquals("Apple", results.get(0).name); - Assertions.assertEquals("Green", results.get(0).color); + await().atMost(2, TimeUnit.SECONDS).pollDelay(500, TimeUnit.MILLISECONDS).untilAsserted(() -> { + // get the Fruit + Fruit result = get("/fruits/1").as(Fruit.class); + + assertThat(result).isNotNull().isEqualTo(fruit); + + // search the Fruit + List results = get("/fruits/search?color=Green").as(LIST_OF_FRUIT_TYPE_REF); + assertThat(results).hasSize(1).contains(fruit); + + results = get("/fruits/search?name=Apple").as(LIST_OF_FRUIT_TYPE_REF); + assertThat(results).hasSize(1).contains(fruit); + }); + + //create new fruit index via bulk operation + Fruit pomegranate = new Fruit(); + pomegranate.id = "2"; + pomegranate.name = "Pomegranate"; + pomegranate.color = "Red"; + + List fruits = List.of(pomegranate); + + given() + .contentType("application/json") + .body(fruits) + .when().post("/fruits/bulk") + .then() + .statusCode(200); + + await().atMost(2, TimeUnit.SECONDS).pollDelay(500, TimeUnit.MILLISECONDS).untilAsserted(() -> { + Fruit result = get("/fruits/2").as(Fruit.class); + assertThat(result).isNotNull().isEqualTo(pomegranate); + }); + + given() + .contentType("application/json") + .body(List.of(pomegranate.id)) + .when().delete("/fruits/bulk") + .then() + .statusCode(200); + + await().atMost(2, TimeUnit.SECONDS).pollDelay(500, TimeUnit.MILLISECONDS).untilAsserted(() -> { + // search the removed Fruit should yield no results + // using a simple get-doc-by-id won't work since we will get an 500 error response: + List results = get("/fruits/search?color=Red").as(LIST_OF_FRUIT_TYPE_REF); + assertThat(results).isEmpty(); + }); + } }