diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java index 53e8cefa7..02690a829 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java @@ -66,7 +66,8 @@ private DocumentAdapters() { * @param jsonpMapper to map JsonData objects * @return the created {@link SearchDocument} */ - public static SearchDocument from(Hit hit, JsonpMapper jsonpMapper) { + @Deprecated + public static SearchDocument fromLegacy(Hit hit, JsonpMapper jsonpMapper) { Assert.notNull(hit, "hit must not be null"); @@ -144,6 +145,84 @@ public static SearchDocument from(Hit hit, JsonpMapper jsonpMapper) { documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing()); } + /** + * Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client. + * + * @param hit the hit object + * @param jsonpMapper to map JsonData objects + * @return the created {@link SearchDocument} + */ + public static SearchDocument from(Hit hit, JsonpMapper jsonpMapper) { + + Assert.notNull(hit, "hit must not be null"); + + Map> highlightFields = hit.highlight(); + + Map innerHits = hit.innerHits().entrySet().parallelStream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> SearchDocumentResponseBuilder.from( + entry.getValue().hits(), null, null, null, 0, null, null, + searchDocument -> null, jsonpMapper + ), + (a, b) -> b, + LinkedHashMap::new + )); + + NestedMetaData nestedMetaData = from(hit.nested()); + Explanation explanation = from(hit.explanation()); + + Map matchedQueries = hit.matchedQueries(); + + EntityAsMap hitFieldsAsMap = new EntityAsMap(); + if (!hit.fields().isEmpty()) { + Map fieldMap = new LinkedHashMap<>(); + hit.fields().forEach((key, jsonData) -> { + var value = jsonData.to(Object.class); + fieldMap.put(key, value); + }); + hitFieldsAsMap.putAll(fieldMap); + } + + Map> documentFields = hitFieldsAsMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue() instanceof List + ? (List) entry.getValue() + : Collections.singletonList(entry.getValue()), + (a, b) -> b, + LinkedHashMap::new + )); + + Document document; + Object source = hit.source(); + if (source == null) { + document = Document.from(hitFieldsAsMap); + } else if (source instanceof EntityAsMap entityAsMap) { + document = Document.from(entityAsMap); + } else if (source instanceof JsonData jsonData) { + document = Document.from(jsonData.to(EntityAsMap.class)); + } else { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName())); + } + document = Document.create(); + } + document.setIndex(hit.index()); + document.setId(hit.id()); + if (hit.version() != null) { + document.setVersion(hit.version()); + } + document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); + document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0); + + float score = hit.score() != null ? hit.score().floatValue() : Float.NaN; + return new SearchDocumentAdapter( + document, score, hit.sort().stream().map(TypeUtils::toObject).toArray(), + documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing() + ); + } + public static SearchDocument from(CompletionSuggestOption completionSuggestOption) { Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source()) diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersBenchmark.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersBenchmark.java new file mode 100644 index 000000000..8f223a013 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersBenchmark.java @@ -0,0 +1,141 @@ +/* + * Copyright 2021-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.client.elc; + +import co.elastic.clients.elasticsearch.core.search.*; +import co.elastic.clients.json.JsonData; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +class DocumentAdaptersBenchmark { + + private final JsonpMapper jsonpMapper = new JacksonJsonpMapper(); + + private List> mockHits; + + @BeforeEach + public void setup() { + mockHits = generateMockHits(100); + } + + public Object fromLegacy() { + return mockHits.stream() + .map(hit -> DocumentAdapters.fromLegacy(hit, jsonpMapper)) + .collect(Collectors.toList()); + } + + public Object from() { + return mockHits.stream() + .map(hit -> DocumentAdapters.from(hit, jsonpMapper)) + .collect(Collectors.toList()); + } + + @Test + public void runner() { + benchmark("fromLegacy", this::fromLegacy); + benchmark("from", this::from); + } + + private void benchmark(String name, Supplier func) { + int warmups = 5; + int runs = 20; + + // Warm-up + for (int i = 0; i < warmups; i++) { + func.get(); + } + + long total = 0; + for (int i = 0; i < runs; i++) { + long start = System.nanoTime(); + func.get(); + long end = System.nanoTime(); + total += (end - start); + } + + double avgMs = total / (runs * 1_000_000.0); + System.out.printf("%s average time: %.6f ms%n", name, avgMs); + } + + public static List> generateMockHits(int count) { + List> results = new ArrayList<>(); + Random random = new Random(); + + for (int i = 0; i < count; i++) { + EntityAsMap source = new EntityAsMap(); + source.put("field1", 10000 + i); + source.put("field2", "value2_" + UUID.randomUUID().toString().substring(0, 8)); + source.put("field3", UUID.randomUUID().toString() + ".jpeg"); + source.put("field4", "value4_" + random.nextInt(1000)); + source.put("field5", "value5_" + random.nextInt(10)); + + List> subCategories = new ArrayList<>(); + for (int j = 0; j < 50; j++) { + Map subCat = new LinkedHashMap<>(); + subCat.put("key", "sub_category_" + j); + subCat.put("value", random.nextInt(5)); + subCategories.add(subCat); + } + + EntityAsMap innerSource = new EntityAsMap(); + innerSource.put("field1", "inner_source_" + i); + innerSource.put("field2", random.nextInt(5)); + innerSource.put("field3", subCategories); + + NestedIdentity nested = new NestedIdentity.Builder().field("nested_field").offset(random.nextInt(10)).build(); + Hit innerHit = new Hit.Builder() + .index("index") + .id(String.valueOf(10000 + i)) + .nested(nested) + .score(random.nextDouble()) + .source(JsonData.of(innerSource)) + .build(); + + List> innerHits = List.of(innerHit); + + TotalHits total = new TotalHits.Builder() + .value(1) + .relation(TotalHitsRelation.Eq) + .build(); + + HitsMetadata hits = new HitsMetadata.Builder() + .total(total) + .maxScore(random.nextDouble()) + .hits(innerHits) + .build(); + + InnerHitsResult innerHitsResult = new InnerHitsResult.Builder() + .hits(hits) + .build(); + + Hit hit = new Hit.Builder() + .index("index") + .id(String.valueOf(10000 + i)) + .score(random.nextDouble()) + .source(source) + .innerHits(Map.of("inner_hit", innerHitsResult)) + .build(); + results.add(hit); + } + return results; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java index 68dc1db7c..0f038904b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java @@ -15,16 +15,10 @@ */ package org.springframework.data.elasticsearch.client.elc; -import co.elastic.clients.elasticsearch.core.search.Hit; +import co.elastic.clients.elasticsearch.core.search.*; import co.elastic.clients.json.JsonData; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.jackson.JacksonJsonpMapper; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - import org.assertj.core.api.SoftAssertions; import org.assertj.core.data.Offset; import org.junit.jupiter.api.DisplayName; @@ -32,6 +26,8 @@ import org.springframework.data.elasticsearch.core.document.Explanation; import org.springframework.data.elasticsearch.core.document.SearchDocument; +import java.util.*; + /** * @author Peter-Josef Meisch * @since 4.4