From ea563ab7ee872209820ec696a9f5d7c022fe2ff0 Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Sat, 9 Oct 2021 18:43:11 +0200 Subject: [PATCH] Support different routing for each id in multiget. --- .../core/DocumentOperations.java | 7 +- .../core/ElasticsearchRestTemplate.java | 1 - .../core/ElasticsearchTemplate.java | 1 - .../core/ReactiveDocumentOperations.java | 4 + .../core/ReactiveElasticsearchTemplate.java | 1 - .../elasticsearch/core/RequestFactory.java | 10 +-- .../{AbstractQuery.java => BaseQuery.java} | 51 ++++++++++-- .../core/query/CriteriaQuery.java | 2 +- .../core/query/MoreLikeThisQuery.java | 2 +- .../core/query/NativeSearchQuery.java | 2 +- .../data/elasticsearch/core/query/Query.java | 71 +++++++++++++++- .../elasticsearch/core/query/StringQuery.java | 2 +- .../SimpleElasticsearchRepository.java | 4 +- .../core/SourceFilterIntegrationTests.java | 11 +-- .../ElasticsearchOperationsRoutingTests.java | 82 +++++++++++++++++-- 15 files changed, 211 insertions(+), 40 deletions(-) rename src/main/java/org/springframework/data/elasticsearch/core/query/{AbstractQuery.java => BaseQuery.java} (85%) diff --git a/src/main/java/org/springframework/data/elasticsearch/core/DocumentOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/DocumentOperations.java index c5ddbd597..6cfe2e2e0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/DocumentOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/DocumentOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.elasticsearch.core; +import java.util.Collection; import java.util.List; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -121,6 +122,8 @@ public interface DocumentOperations { * @param query the query defining the ids of the objects to get * @param clazz the type of the object to be returned * @return list of {@link MultiGetItem}s + * @see Query#multiGetQuery(Collection) + * @see Query#multiGetQueryWithRouting(List) * @since 4.1 */ List> multiGet(Query query, Class clazz); @@ -132,6 +135,8 @@ public interface DocumentOperations { * @param clazz the type of the object to be returned * @param index the index(es) from which the objects are read. * @return list of {@link MultiGetItem}s + * @see Query#multiGetQuery(Collection) + * @see Query#multiGetQueryWithRouting(List) */ List> multiGet(Query query, Class clazz, IndexCoordinates index); @@ -283,7 +288,7 @@ default void bulkUpdate(List queries, IndexCoordinates index) { /** * Delete all records matching the query. - * + * * @param query query defining the objects * @param clazz The entity class, must be annotated with * {@link org.springframework.data.elasticsearch.annotations.Document} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java index 7b86aaca6..1d73a2b72 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java @@ -182,7 +182,6 @@ public T get(String id, Class clazz, IndexCoordinates index) { public List> multiGet(Query query, Class clazz, IndexCoordinates index) { Assert.notNull(index, "index must not be null"); - Assert.notEmpty(query.getIds(), "No Id defined for Query"); MultiGetRequest request = requestFactory.multiGetRequest(query, clazz, index); MultiGetResponse result = execute(client -> client.mget(request, RequestOptions.DEFAULT)); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java index a9070a2dd..c58d3f4c6 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -205,7 +205,6 @@ public T get(String id, Class clazz, IndexCoordinates index) { public List> multiGet(Query query, Class clazz, IndexCoordinates index) { Assert.notNull(index, "index must not be null"); - Assert.notEmpty(query.getIds(), "No Ids defined for Query"); MultiGetRequestBuilder builder = requestFactory.multiGetRequestBuilder(client, query, clazz, index); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java index 566fee06f..20fa0bcfb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java @@ -148,6 +148,8 @@ default Flux saveAll(Iterable entities, IndexCoordinates index) { * @param query the query defining the ids of the objects to get * @param clazz the type of the object to be returned, used to determine the index * @return flux with list of {@link MultiGetItem}s that contain the entities + * @see Query#multiGetQuery(Collection) + * @see Query#multiGetQueryWithRouting(List) * @since 4.1 */ Flux> multiGet(Query query, Class clazz); @@ -159,6 +161,8 @@ default Flux saveAll(Iterable entities, IndexCoordinates index) { * @param clazz the type of the object to be returned * @param index the index(es) from which the objects are read. * @return flux with list of {@link MultiGetItem}s that contain the entities + * @see Query#multiGetQuery(Collection) + * @see Query#multiGetQueryWithRouting(List) * @since 4.0 */ Flux> multiGet(Query query, Class clazz, IndexCoordinates index); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java index 2750adb51..131711483 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java @@ -338,7 +338,6 @@ public Flux> multiGet(Query query, Class clazz, IndexCoor Assert.notNull(index, "Index must not be null"); Assert.notNull(clazz, "Class must not be null"); Assert.notNull(query, "Query must not be null"); - Assert.notEmpty(query.getIds(), "No Id define for Query"); DocumentCallback callback = new ReadDocumentCallback<>(converter, clazz, index); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java index dd69178d2..d906b47dd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java @@ -677,13 +677,13 @@ private List getMultiRequestItems(Query searchQuery, Class FetchSourceContext fetchSourceContext = getFetchSourceContext(searchQuery); - if (!isEmpty(searchQuery.getIds())) { + if (!isEmpty(searchQuery.getIdsWithRouting())) { String indexName = index.getIndexName(); - for (String id : searchQuery.getIds()) { - MultiGetRequest.Item item = new MultiGetRequest.Item(indexName, id); - if (searchQuery.getRoute() != null) { - item = item.routing(searchQuery.getRoute()); + for (Query.IdWithRouting idWithRouting : searchQuery.getIdsWithRouting()) { + MultiGetRequest.Item item = new MultiGetRequest.Item(indexName, idWithRouting.getId()); + if (idWithRouting.getRouting() != null) { + item = item.routing(idWithRouting.getRouting()); } // note: multiGet does not have fields, need to set sourceContext to filter diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java similarity index 85% rename from src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java rename to src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java index 7281da289..20282b268 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java @@ -16,12 +16,15 @@ package org.springframework.data.elasticsearch.core.query; import static java.util.Collections.*; +import static org.springframework.util.CollectionUtils.*; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -29,7 +32,7 @@ import org.springframework.util.Assert; /** - * AbstractQuery + * BaseQuery * * @author Rizwan Idrees * @author Mohsin Husen @@ -40,7 +43,7 @@ * @author Peter-Josef Meisch * @author Peer Mueller */ -abstract class AbstractQuery implements Query { +public class BaseQuery implements Query { protected Pageable pageable = DEFAULT_PAGE; @Nullable protected Sort sort; @@ -63,6 +66,7 @@ abstract class AbstractQuery implements Query { @Nullable private List searchAfter; protected List rescorerQueries = new ArrayList<>(); @Nullable protected Boolean requestCache; + private List idsWithRouting = Collections.emptyList(); @Override @Nullable @@ -81,7 +85,7 @@ public final T setPageable(Pageable pageable) { Assert.notNull(pageable, "Pageable must not be null!"); this.pageable = pageable; - return (T) this.addSort(pageable.getSort()); + return this.addSort(pageable.getSort()); } @Override @@ -116,7 +120,7 @@ public SourceFilter getSourceFilter() { @Override @SuppressWarnings("unchecked") - public final T addSort(Sort sort) { + public final T addSort(@Nullable Sort sort) { if (sort == null) { return (T) this; } @@ -139,14 +143,46 @@ public void setMinScore(float minScore) { this.minScore = minScore; } - @Nullable + /** + * Set Ids for a multi-get request with on this query. + * + * @param ids list of id values + */ + public void setIds(@Nullable Collection ids) { + this.ids = ids; + } + @Override + @Nullable public Collection getIds() { return ids; } - public void setIds(Collection ids) { - this.ids = ids; + @Override + public List getIdsWithRouting() { + + if (!isEmpty(idsWithRouting)) { + return Collections.unmodifiableList(idsWithRouting); + } + + if (!isEmpty(ids)) { + return ids.stream().map(id -> new IdWithRouting(id, route)).collect(Collectors.toList()); + } + + return Collections.emptyList(); + } + + /** + * Set Ids with routing values for a multi-get request set on this query. + * + * @param idsWithRouting list of id values, must not be {@literal null} + * @since 4.3 + */ + public void setIdsWithRouting(List idsWithRouting) { + + Assert.notNull(idsWithRouting, "idsWithRouting must not be null"); + + this.idsWithRouting = idsWithRouting; } @Nullable @@ -337,4 +373,5 @@ public void setRequestCache(@Nullable Boolean value) { public Boolean getRequestCache() { return this.requestCache; } + } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java index dbba1e847..82949568f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java @@ -26,7 +26,7 @@ * @author Mark Paluch * @author Peter-Josef Meisch */ -public class CriteriaQuery extends AbstractQuery { +public class CriteriaQuery extends BaseQuery { private Criteria criteria; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java index 8fc2d7227..7d823521d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java @@ -16,7 +16,7 @@ package org.springframework.data.elasticsearch.core.query; import static java.util.Collections.*; -import static org.springframework.data.elasticsearch.core.query.AbstractQuery.*; +import static org.springframework.data.elasticsearch.core.query.BaseQuery.*; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java index df5b73078..fe1ecb662 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java @@ -42,7 +42,7 @@ * @author Martin Choraine * @author Peter-Josef Meisch */ -public class NativeSearchQuery extends AbstractQuery { +public class NativeSearchQuery extends BaseQuery { @Nullable private final QueryBuilder query; @Nullable private QueryBuilder filter; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java index a1302d000..47277ce6b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java @@ -26,6 +26,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Query @@ -76,7 +77,7 @@ static Query findAll() { * @param sort * @return */ - T addSort(Sort sort); + T addSort(@Nullable Sort sort); /** * @return null if not set @@ -137,13 +138,48 @@ static Query findAll() { boolean getTrackScores(); /** - * Get Ids - * - * @return + * @return Get ids set on this query. */ @Nullable Collection getIds(); + /** + * @return Ids with routing values used in a multi-get request. + * @see #multiGetQueryWithRouting(List) + * @since 4.3 + */ + List getIdsWithRouting(); + + /** + * Utility method to get a query for a multiget request + * + * @param idsWithRouting Ids with routing values used in a multi-get request. + * @return Query instance + */ + static Query multiGetQueryWithRouting(List idsWithRouting) { + + Assert.notNull(idsWithRouting, "idsWithRouting must not be null"); + + BaseQuery query = new BaseQuery(); + query.setIdsWithRouting(idsWithRouting); + return query; + } + + /** + * Utility method to get a query for a multiget request + * + * @param ids Ids used in a multi-get request. + * @return Query instance + */ + static Query multiGetQuery(Collection ids) { + + Assert.notNull(ids, "ids must not be null"); + + BaseQuery query = new BaseQuery(); + query.setIds(ids); + return query; + } + /** * Get route * @@ -362,4 +398,31 @@ default List getRescorerQueries() { enum SearchType { QUERY_THEN_FETCH, DFS_QUERY_THEN_FETCH } + + /** + * Value class combining an id with a routing value. Used in multi-get requests. + * + * @since 4.3 + */ + final class IdWithRouting { + private final String id; + @Nullable private final String routing; + + public IdWithRouting(String id, @Nullable String routing) { + + Assert.notNull(id, "id must not be null"); + + this.id = id; + this.routing = routing; + } + + public String getId() { + return id; + } + + @Nullable + public String getRouting() { + return routing; + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java index 3ea7215ac..fe768e03b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java @@ -24,7 +24,7 @@ * @author Rizwan Idrees * @author Mohsin Husen */ -public class StringQuery extends AbstractQuery { +public class StringQuery extends BaseQuery { private String source; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java index 7c12b9b53..cedffeba5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.repository.support; import static org.elasticsearch.index.query.QueryBuilders.*; +import static org.springframework.util.CollectionUtils.*; import java.util.ArrayList; import java.util.Collections; @@ -50,7 +51,6 @@ import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; /** * Elasticsearch specific repository implementation. Likely to be used as target within @@ -149,7 +149,7 @@ public Iterable findAllById(Iterable ids) { List result = new ArrayList<>(); Query idQuery = getIdQuery(ids); - if (CollectionUtils.isEmpty(idQuery.getIds())) { + if (isEmpty(idQuery.getIds())) { return result; } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java index 762ce9deb..518ca5c5c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java @@ -30,7 +30,6 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; -import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.SourceFilter; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; @@ -72,10 +71,8 @@ void tearDown() { void shouldOnlyReturnRequestedFieldsOnGMultiGet() { // multiget has no fields, need sourcefilter here - Query query = new NativeSearchQueryBuilder() // - .withIds(Collections.singleton("42")) // - .withSourceFilter(new FetchSourceFilterBuilder().withIncludes("field2").build()) // - .build(); // + Query query = Query.multiGetQuery(Collections.singleton("42")); + query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("field2").build()); // List> entities = operations.multiGet(query, Entity.class); @@ -116,7 +113,7 @@ public String[] getExcludes() { @DisplayName("should not return excluded fields from SourceFilter on multiget") void shouldNotReturnExcludedFieldsFromSourceFilterOnMultiGet() { - Query query = new NativeSearchQueryBuilder().withIds(Collections.singleton("42")).build(); + Query query = Query.multiGetQuery(Collections.singleton("42")); query.addSourceFilter(new SourceFilter() { @Override public String[] getIncludes() { @@ -168,7 +165,7 @@ public String[] getExcludes() { @DisplayName("should only return included fields from SourceFilter on multiget") void shouldOnlyReturnIncludedFieldsFromSourceFilterOnMultiGet() { - Query query = new NativeSearchQueryBuilder().withIds(Collections.singleton("42")).build(); + Query query = Query.multiGetQuery(Collections.singleton("42")); query.addSourceFilter(new SourceFilter() { @Override public String[] getIncludes() { diff --git a/src/test/java/org/springframework/data/elasticsearch/core/routing/ElasticsearchOperationsRoutingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/routing/ElasticsearchOperationsRoutingTests.java index ed096ca0b..74f4b8c4c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/routing/ElasticsearchOperationsRoutingTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/routing/ElasticsearchOperationsRoutingTests.java @@ -17,9 +17,13 @@ import static org.assertj.core.api.Assertions.*; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; -import org.elasticsearch.cluster.routing.Murmur3HashFunction; +import org.apache.lucene.util.StringHelper; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -31,8 +35,10 @@ import org.springframework.data.elasticsearch.annotations.Setting; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.IndexOperations; +import org.springframework.data.elasticsearch.core.MultiGetItem; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; @@ -48,6 +54,7 @@ public class ElasticsearchOperationsRoutingTests { private static final String INDEX = "routing-test"; + private static final String ID_0 = "id0"; private static final String ID_1 = "id1"; private static final String ID_2 = "id2"; private static final String ID_3 = "id3"; @@ -57,16 +64,22 @@ public class ElasticsearchOperationsRoutingTests { @BeforeAll static void beforeAll() { - // check that the used id values go to different shards of the index which is configured to have 5 shards. + // check that the used id values go to different shards of the index which is configured to have 7 shards. // Elasticsearch uses the following function: - Function calcShard = routing -> Math.floorMod(Murmur3HashFunction.hash(routing), 5); + Function calcShard = routing -> Math.floorMod(Murmur3HashFunction.hash(routing), 7); + Integer shard0 = calcShard.apply(ID_0); Integer shard1 = calcShard.apply(ID_1); Integer shard2 = calcShard.apply(ID_2); Integer shard3 = calcShard.apply(ID_3); + assertThat(shard0).isNotEqualTo(shard1); + assertThat(shard0).isNotEqualTo(shard2); + assertThat(shard0).isNotEqualTo(shard3); + assertThat(shard1).isNotEqualTo(shard2); assertThat(shard1).isNotEqualTo(shard3); + assertThat(shard2).isNotEqualTo(shard3); } @@ -84,7 +97,6 @@ void shouldStoreDataWithDifferentRoutingAndBeAbleToGetIt() { RoutingEntity entity = new RoutingEntity(ID_1, ID_2); operations.save(entity); - indexOps.refresh(); RoutingEntity savedEntity = operations.withRouting(RoutingResolver.just(ID_2)).get(entity.id, RoutingEntity.class); @@ -97,7 +109,6 @@ void shouldStoreDataWithDifferentRoutingAndBeAbleToDeleteIt() { RoutingEntity entity = new RoutingEntity(ID_1, ID_2); operations.save(entity); - indexOps.refresh(); String deletedId = operations.withRouting(RoutingResolver.just(ID_2)).delete(entity.id, IndexCoordinates.of(INDEX)); @@ -106,11 +117,11 @@ void shouldStoreDataWithDifferentRoutingAndBeAbleToDeleteIt() { @Test // #1218 @DisplayName("should store data with different routing and get the routing in the search result") + void shouldStoreDataWithDifferentRoutingAndGetTheRoutingInTheSearchResult() { RoutingEntity entity = new RoutingEntity(ID_1, ID_2); operations.save(entity); - indexOps.refresh(); SearchHits searchHits = operations.search(Query.findAll(), RoutingEntity.class); @@ -118,8 +129,38 @@ void shouldStoreDataWithDifferentRoutingAndGetTheRoutingInTheSearchResult() { assertThat(searchHits.getSearchHit(0).getRouting()).isEqualTo(ID_2); } + @Test // #1954 + @DisplayName("should use routing values in multiget") + void shouldUseRoutingValuesInMultiget() { + + Consumer save = (String id) -> operations.save(new RoutingEntity(id, id)); + save.accept(ID_1); + save.accept(ID_2); + save.accept(ID_3); + + Query query = Query.multiGetQueryWithRouting( // + Arrays.asList( // + new Query.IdWithRouting(ID_1, ID_1), // + new Query.IdWithRouting(ID_2, ID_2), // + new Query.IdWithRouting(ID_3, ID_3) // + ) // + ); // + + // make sure that the correct routing values are used + ((BaseQuery) query).setRoute(ID_0); + + List> multiGetItems = operations.multiGet(query, RoutingEntity.class); + + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(multiGetItems).hasSize(3); + softly.assertThat(multiGetItems.get(0).hasItem()).isTrue(); + softly.assertThat(multiGetItems.get(1).hasItem()).isTrue(); + softly.assertThat(multiGetItems.get(2).hasItem()).isTrue(); + softly.assertAll(); + } + @Document(indexName = INDEX) - @Setting(shards = 5) + @Setting(shards = 7) @Routing("routing") static class RoutingEntity { @Nullable @Id private String id; @@ -169,4 +210,31 @@ public int hashCode() { return result; } } + + /** + * Copied from org.elasticsearch.cluster.routing.Murmur3HashFunction from Elasticsearch 7.9.3 + */ + static class Murmur3HashFunction { + + private Murmur3HashFunction() { + // no instance + } + + public static int hash(String routing) { + final byte[] bytesToHash = new byte[routing.length() * 2]; + for (int i = 0; i < routing.length(); ++i) { + final char c = routing.charAt(i); + final byte b1 = (byte) c, b2 = (byte) (c >>> 8); + assert ((b1 & 0xFF) | ((b2 & 0xFF) << 8)) == c; // no information loss + bytesToHash[i * 2] = b1; + bytesToHash[i * 2 + 1] = b2; + } + return hash(bytesToHash, 0, bytesToHash.length); + } + + public static int hash(byte[] bytes, int offset, int length) { + return StringHelper.murmurhash3_x86_32(bytes, offset, length, 0); + } + + } }