Support multi search template API.

Original Pull Request #2807
Closes #2704
This commit is contained in:
puppylpg 2023-12-30 23:10:36 +08:00 committed by GitHub
parent 260dadd4d6
commit 1554c3c94f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 358 additions and 89 deletions

View File

@ -29,6 +29,7 @@ import co.elastic.clients.transport.Version;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -72,6 +73,7 @@ import org.springframework.util.Assert;
* @author Peter-Josef Meisch
* @author Hamid Rahimi
* @author Illia Ulianov
* @author Haibo Liu
* @since 4.4
*/
public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
@ -437,13 +439,10 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(queries, "queries must not be null");
Assert.notNull(clazz, "clazz must not be null");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
for (Query query : queries) {
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz)));
}
int size = queries.size();
// noinspection unchecked
return doMultiSearch(multiSearchQueryParameters).stream().map(searchHits -> (SearchHits<T>) searchHits)
return multiSearch(queries, Collections.nCopies(size, clazz), Collections.nCopies(size, index))
.stream().map(searchHits -> (SearchHits<T>) searchHits)
.collect(Collectors.toList());
}
@ -454,14 +453,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(classes, "classes must not be null");
Assert.isTrue(queries.size() == classes.size(), "queries and classes must have the same size");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
Iterator<Class<?>> it = classes.iterator();
for (Query query : queries) {
Class<?> clazz = it.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz)));
}
return doMultiSearch(multiSearchQueryParameters);
return multiSearch(queries, classes, classes.stream().map(this::getIndexCoordinatesFor).toList());
}
@Override
@ -473,14 +465,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(index, "index must not be null");
Assert.isTrue(queries.size() == classes.size(), "queries and classes must have the same size");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
Iterator<Class<?>> it = classes.iterator();
for (Query query : queries) {
Class<?> clazz = it.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, index));
}
return doMultiSearch(multiSearchQueryParameters);
return multiSearch(queries, classes, Collections.nCopies(queries.size(), index));
}
@Override
@ -497,16 +482,49 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Iterator<Class<?>> it = classes.iterator();
Iterator<IndexCoordinates> indexesIt = indexes.iterator();
Assert.isTrue(!queries.isEmpty(), "queries should have at least 1 query");
boolean isSearchTemplateQuery = queries.get(0) instanceof SearchTemplateQuery;
for (Query query : queries) {
Assert.isTrue((query instanceof SearchTemplateQuery) == isSearchTemplateQuery,
"SearchTemplateQuery can't be mixed with other types of query in multiple search");
Class<?> clazz = it.next();
IndexCoordinates index = indexesIt.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, index));
}
return doMultiSearch(multiSearchQueryParameters);
return multiSearch(multiSearchQueryParameters, isSearchTemplateQuery);
}
private List<SearchHits<?>> multiSearch(List<MultiSearchQueryParameter> multiSearchQueryParameters,
boolean isSearchTemplateQuery) {
return isSearchTemplateQuery ?
doMultiTemplateSearch(multiSearchQueryParameters.stream()
.map(p -> new MultiSearchTemplateQueryParameter((SearchTemplateQuery) p.query, p.clazz, p.index))
.toList())
: doMultiSearch(multiSearchQueryParameters);
}
private List<SearchHits<?>> doMultiTemplateSearch(List<MultiSearchTemplateQueryParameter> mSearchTemplateQueryParameters) {
MsearchTemplateRequest request = requestConverter.searchMsearchTemplateRequest(mSearchTemplateQueryParameters,
routingResolver.getRouting());
MsearchTemplateResponse<EntityAsMap> response = execute(client -> client.msearchTemplate(request, EntityAsMap.class));
List<MultiSearchResponseItem<EntityAsMap>> responseItems = response.responses();
Assert.isTrue(mSearchTemplateQueryParameters.size() == responseItems.size(),
"number of response items does not match number of requests");
int size = mSearchTemplateQueryParameters.size();
List<Class<?>> classes = mSearchTemplateQueryParameters
.stream().map(MultiSearchTemplateQueryParameter::clazz).collect(Collectors.toList());
List<IndexCoordinates> indices = mSearchTemplateQueryParameters
.stream().map(MultiSearchTemplateQueryParameter::index).collect(Collectors.toList());
return getSearchHitsFromMsearchResponse(size, classes, indices, responseItems);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private List<SearchHits<?>> doMultiSearch(List<MultiSearchQueryParameter> multiSearchQueryParameters) {
MsearchRequest request = requestConverter.searchMsearchRequest(multiSearchQueryParameters,
@ -518,22 +536,37 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.isTrue(multiSearchQueryParameters.size() == responseItems.size(),
"number of response items does not match number of requests");
List<SearchHits<?>> searchHitsList = new ArrayList<>(multiSearchQueryParameters.size());
int size = multiSearchQueryParameters.size();
List<Class<?>> classes = multiSearchQueryParameters
.stream().map(MultiSearchQueryParameter::clazz).collect(Collectors.toList());
List<IndexCoordinates> indices = multiSearchQueryParameters
.stream().map(MultiSearchQueryParameter::index).collect(Collectors.toList());
Iterator<MultiSearchQueryParameter> queryIterator = multiSearchQueryParameters.iterator();
return getSearchHitsFromMsearchResponse(size, classes, indices, responseItems);
}
/**
* {@link MsearchResponse} and {@link MsearchTemplateResponse} share the same {@link MultiSearchResponseItem}
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private List<SearchHits<?>> getSearchHitsFromMsearchResponse(int size, List<Class<?>> classes,
List<IndexCoordinates> indices, List<MultiSearchResponseItem<EntityAsMap>> responseItems) {
List<SearchHits<?>> searchHitsList = new ArrayList<>(size);
Iterator<Class<?>> clazzIter = classes.iterator();
Iterator<IndexCoordinates> indexIter = indices.iterator();
Iterator<MultiSearchResponseItem<EntityAsMap>> responseIterator = responseItems.iterator();
while (queryIterator.hasNext()) {
MultiSearchQueryParameter queryParameter = queryIterator.next();
while (clazzIter.hasNext() && indexIter.hasNext()) {
MultiSearchResponseItem<EntityAsMap> responseItem = responseIterator.next();
if (responseItem.isResult()) {
Class clazz = queryParameter.clazz;
Class clazz = clazzIter.next();
IndexCoordinates index = indexIter.next();
ReadDocumentCallback<?> documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz,
queryParameter.index);
index);
SearchDocumentResponseCallback<SearchHits<?>> callback = new ReadSearchDocumentResponseCallback<>(clazz,
queryParameter.index);
index);
SearchHits<?> searchHits = callback.doWith(
SearchDocumentResponseBuilder.from(responseItem.result(), getEntityCreator(documentCallback), jsonpMapper));
@ -541,8 +574,8 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
searchHitsList.add(searchHits);
} else {
if (LOGGER.isWarnEnabled()) {
LOGGER
.warn(String.format("multisearch responsecontains failure: {}", responseItem.failure().error().reason()));
LOGGER.warn(String.format("multisearch response contains failure: %s",
responseItem.failure().error().reason()));
}
}
}
@ -556,6 +589,12 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
record MultiSearchQueryParameter(Query query, Class<?> clazz, IndexCoordinates index) {
}
/**
* value class combining the information needed for a single query in a template multisearch request.
*/
record MultiSearchTemplateQueryParameter(SearchTemplateQuery query, Class<?> clazz, IndexCoordinates index) {
}
@Override
public String openPointInTime(IndexCoordinates index, Duration keepAlive, Boolean ignoreUnavailable) {

View File

@ -44,6 +44,7 @@ import co.elastic.clients.elasticsearch.core.bulk.IndexOperation;
import co.elastic.clients.elasticsearch.core.bulk.UpdateOperation;
import co.elastic.clients.elasticsearch.core.mget.MultiGetOperation;
import co.elastic.clients.elasticsearch.core.msearch.MultisearchBody;
import co.elastic.clients.elasticsearch.core.msearch.MultisearchHeader;
import co.elastic.clients.elasticsearch.core.search.Highlight;
import co.elastic.clients.elasticsearch.core.search.Rescore;
import co.elastic.clients.elasticsearch.core.search.SourceConfig;
@ -54,6 +55,7 @@ import co.elastic.clients.elasticsearch.indices.update_aliases.Action;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpDeserializer;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.util.ObjectBuilder;
import jakarta.json.stream.JsonParser;
import java.io.ByteArrayInputStream;
@ -66,6 +68,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -397,10 +400,7 @@ class RequestConverter {
.order(putTemplateRequest.getOrder());
if (putTemplateRequest.getSettings() != null) {
Function<Map.Entry<String, Object>, String> keyMapper = Map.Entry::getKey;
Function<Map.Entry<String, Object>, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper);
Map<String, JsonData> settings = putTemplateRequest.getSettings().entrySet().stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
Map<String, JsonData> settings = getTemplateParams(putTemplateRequest.getSettings().entrySet());
builder.settings(settings);
}
@ -1146,6 +1146,36 @@ class RequestConverter {
return builder.build();
}
public MsearchTemplateRequest searchMsearchTemplateRequest(
List<ElasticsearchTemplate.MultiSearchTemplateQueryParameter> multiSearchTemplateQueryParameters,
@Nullable String routing) {
// basically the same stuff as in template search
return MsearchTemplateRequest.of(mtrb -> {
multiSearchTemplateQueryParameters.forEach(param -> {
var query = param.query();
mtrb.searchTemplates(stb -> stb
.header(msearchHeaderBuilder(query, param.index(), routing))
.body(bb -> {
bb //
.explain(query.getExplain()) //
.id(query.getId()) //
.source(query.getSource()) //
;
if (!CollectionUtils.isEmpty(query.getParams())) {
Map<String, JsonData> params = getTemplateParams(query.getParams().entrySet());
bb.params(params);
}
return bb;
})
);
});
return mtrb;
});
}
public MsearchRequest searchMsearchRequest(
List<ElasticsearchTemplate.MultiSearchQueryParameter> multiSearchQueryParameters, @Nullable String routing) {
@ -1157,28 +1187,7 @@ class RequestConverter {
var query = param.query();
mrb.searches(sb -> sb //
.header(h -> {
var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null
: searchType(query.getSearchType());
h //
.index(Arrays.asList(param.index().getIndexNames())) //
.searchType(searchType) //
.requestCache(query.getRequestCache()) //
;
if (StringUtils.hasText(query.getRoute())) {
h.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
h.routing(routing);
}
if (query.getPreference() != null) {
h.preference(query.getPreference());
}
return h;
}) //
.header(msearchHeaderBuilder(query, param.index(), routing)) //
.body(bb -> {
bb //
.query(getQuery(query, param.clazz()))//
@ -1284,6 +1293,35 @@ class RequestConverter {
});
}
/**
* {@link MsearchRequest} and {@link MsearchTemplateRequest} share the same {@link MultisearchHeader}
*/
private Function<MultisearchHeader.Builder, ObjectBuilder<MultisearchHeader>> msearchHeaderBuilder(Query query,
IndexCoordinates index, @Nullable String routing) {
return h -> {
var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null
: searchType(query.getSearchType());
h //
.index(Arrays.asList(index.getIndexNames())) //
.searchType(searchType) //
.requestCache(query.getRequestCache()) //
;
if (StringUtils.hasText(query.getRoute())) {
h.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
h.routing(routing);
}
if (query.getPreference() != null) {
h.preference(query.getPreference());
}
return h;
};
}
private <T> void prepareSearchRequest(Query query, @Nullable String routing, @Nullable Class<T> clazz,
IndexCoordinates indexCoordinates, SearchRequest.Builder builder, boolean forCount, boolean forBatchedSearch) {
@ -1770,7 +1808,8 @@ class RequestConverter {
.id(query.getId()) //
.index(Arrays.asList(index.getIndexNames())) //
.preference(query.getPreference()) //
.searchType(searchType(query.getSearchType())).source(query.getSource()) //
.searchType(searchType(query.getSearchType())) //
.source(query.getSource()) //
;
if (query.getRoute() != null) {
@ -1789,10 +1828,7 @@ class RequestConverter {
}
if (!CollectionUtils.isEmpty(query.getParams())) {
Function<Map.Entry<String, Object>, String> keyMapper = Map.Entry::getKey;
Function<Map.Entry<String, Object>, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper);
Map<String, JsonData> params = query.getParams().entrySet().stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
Map<String, JsonData> params = getTemplateParams(query.getParams().entrySet());
builder.params(params);
}
@ -1800,6 +1836,14 @@ class RequestConverter {
});
}
@NotNull
private Map<String, JsonData> getTemplateParams(Set<Map.Entry<String, Object>> query) {
Function<Map.Entry<String, Object>, String> keyMapper = Map.Entry::getKey;
Function<Map.Entry<String, Object>, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper);
return query.stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
}
// endregion
public PutScriptRequest scriptPut(Script script) {

View File

@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.util.List;
import java.util.Map;
import org.json.JSONException;
@ -42,11 +43,12 @@ import org.springframework.lang.Nullable;
* Integration tests search template API.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
@SpringIntegrationTest
public abstract class SearchTemplateIntegrationTests {
private static final String SCRIPT = """
private static final String SEARCH_FIRSTNAME = """
{
"query": {
"bool": {
@ -63,21 +65,57 @@ public abstract class SearchTemplateIntegrationTests {
"size": 100
}
""";
private Script script = Script.builder() //
.withId("testScript") //
private static final String SEARCH_LASTNAME = """
{
"query": {
"bool": {
"must": [
{
"match": {
"lastName": "{{lastName}}"
}
}
]
}
},
"from": 0,
"size": 100
}
""";
private static final Script SCRIPT_SEARCH_FIRSTNAME = Script.builder() //
.withId("searchFirstName") //
.withLanguage("mustache") //
.withSource(SCRIPT) //
.withSource(SEARCH_FIRSTNAME) //
.build();
private static final Script SCRIPT_SEARCH_LASTNAME = Script.builder() //
.withId("searchLastName") //
.withLanguage("mustache") //
.withSource(SEARCH_LASTNAME) //
.build();
@Autowired ElasticsearchOperations operations;
@Autowired IndexNameProvider indexNameProvider;
@Nullable IndexOperations indexOperations;
IndexOperations personIndexOperations, studentIndexOperations;
@BeforeEach
void setUp() {
indexNameProvider.increment();
indexOperations = operations.indexOps(Person.class);
indexOperations.createWithMapping();
personIndexOperations = operations.indexOps(Person.class);
personIndexOperations.createWithMapping();
studentIndexOperations = operations.indexOps(Student.class);
studentIndexOperations.createWithMapping();
operations.save( //
new Person("1", "John", "Smith"), //
new Person("2", "Willy", "Smith"), //
new Person("3", "John", "Myers"));
operations.save(
new Student("1", "Joey", "Dunlop"), //
new Student("2", "Michael", "Dunlop"));
}
@Test
@ -89,41 +127,35 @@ public abstract class SearchTemplateIntegrationTests {
@Test // #1891
@DisplayName("should store, retrieve and delete template script")
void shouldStoreAndRetrieveAndDeleteTemplateScript() throws JSONException {
// we do all in this test because scripts aren't stored in an index but in the cluster and we need to clenaup.
var success = operations.putScript(script);
var success = operations.putScript(SCRIPT_SEARCH_FIRSTNAME);
assertThat(success).isTrue();
var savedScript = operations.getScript(script.id());
var savedScript = operations.getScript(SCRIPT_SEARCH_FIRSTNAME.id());
assertThat(savedScript).isNotNull();
assertThat(savedScript.id()).isEqualTo(script.id());
assertThat(savedScript.language()).isEqualTo(script.language());
assertEquals(savedScript.source(), script.source(), false);
assertThat(savedScript.id()).isEqualTo(SCRIPT_SEARCH_FIRSTNAME.id());
assertThat(savedScript.language()).isEqualTo(SCRIPT_SEARCH_FIRSTNAME.language());
assertEquals(savedScript.source(), SCRIPT_SEARCH_FIRSTNAME.source(), false);
success = operations.deleteScript(script.id());
success = operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id());
assertThat(success).isTrue();
savedScript = operations.getScript(script.id());
savedScript = operations.getScript(SCRIPT_SEARCH_FIRSTNAME.id());
assertThat(savedScript).isNull();
assertThatThrownBy(() -> operations.deleteScript(script.id())) //
assertThatThrownBy(() -> operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())) //
.isInstanceOf(ResourceNotFoundException.class);
}
@Test // #1891
@DisplayName("should search with template")
void shouldSearchWithTemplate() {
var success = operations.putScript(script);
var success = operations.putScript(SCRIPT_SEARCH_FIRSTNAME);
assertThat(success).isTrue();
operations.save( //
new Person("1", "John", "Smith"), //
new Person("2", "Willy", "Smith"), //
new Person("3", "John", "Myers"));
var query = SearchTemplateQuery.builder() //
.withId(script.id()) //
.withId(SCRIPT_SEARCH_FIRSTNAME.id()) //
.withParams(Map.of("firstName", "John")) //
.build();
@ -131,15 +163,169 @@ public abstract class SearchTemplateIntegrationTests {
assertThat(searchHits.getTotalHits()).isEqualTo(2);
success = operations.deleteScript(script.id());
success = operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id());
assertThat(success).isTrue();
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
@Test // #2704
@DisplayName("should search with template multisearch")
void shouldSearchWithTemplateMultiSearch() {
var success = operations.putScript(SCRIPT_SEARCH_FIRSTNAME);
assertThat(success).isTrue();
var q1 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_FIRSTNAME.id()) //
.withParams(Map.of("firstName", "John")) //
.build();
var q2 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_FIRSTNAME.id()) //
.withParams(Map.of("firstName", "Willy")) //
.build();
var multiSearchHits = operations.multiSearch(List.of(q1, q2), Person.class);
assertThat(multiSearchHits.size()).isEqualTo(2);
assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2);
assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(1);
assertThat(multiSearchHits.get(0).getSearchHits())
.extracting(SearchHit::getContent)
.extracting(Person::lastName)
.contains("Smith", "Myers");
assertThat(multiSearchHits.get(1).getSearchHits())
.extracting(SearchHit::getContent)
.extracting(Person::lastName)
.containsExactly("Smith");
success = operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id());
assertThat(success).isTrue();
}
@Test // #2704
@DisplayName("should search with template multisearch including different scripts")
void shouldSearchWithTemplateMultiSearchIncludingDifferentScripts() {
assertThat(operations.putScript(SCRIPT_SEARCH_FIRSTNAME)).isTrue();
assertThat(operations.putScript(SCRIPT_SEARCH_LASTNAME)).isTrue();
var q1 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_FIRSTNAME.id()) //
.withParams(Map.of("firstName", "John")) //
.build();
var q2 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_LASTNAME.id()) //
.withParams(Map.of("lastName", "smith")) //
.build();
var multiSearchHits = operations.multiSearch(List.of(q1, q2), Person.class);
assertThat(multiSearchHits.size()).isEqualTo(2);
assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2);
assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(2);
assertThat(multiSearchHits.get(0).getSearchHits())
.extracting(SearchHit::getContent)
.extracting(Person::lastName)
.contains("Smith", "Myers");
assertThat(multiSearchHits.get(1).getSearchHits())
.extracting(SearchHit::getContent)
.extracting(Person::firstName)
.contains("John", "Willy");
assertThat(operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())).isTrue();
assertThat(operations.deleteScript(SCRIPT_SEARCH_LASTNAME.id())).isTrue();
}
@Test // #2704
@DisplayName("should search with template multisearch with multiple classes")
void shouldSearchWithTemplateMultiSearchWithMultipleClasses() {
assertThat(operations.putScript(SCRIPT_SEARCH_FIRSTNAME)).isTrue();
assertThat(operations.putScript(SCRIPT_SEARCH_LASTNAME)).isTrue();
var q1 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_FIRSTNAME.id()) //
.withParams(Map.of("firstName", "John")) //
.build();
var q2 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_FIRSTNAME.id()) //
.withParams(Map.of("firstName", "Joey")) //
.build();
// search with multiple classes
var multiSearchHits = operations.multiSearch(List.of(q1, q2), List.of(Person.class, Student.class));
assertThat(multiSearchHits.size()).isEqualTo(2);
assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2);
assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(1);
assertThat(multiSearchHits.get(0).getSearchHits())
// type casting is needed here
.extracting(hits -> (Person) hits.getContent())
.extracting(Person::lastName)
.contains("Smith", "Myers");
assertThat(multiSearchHits.get(1).getSearchHits())
// type casting is needed here
.extracting(hits -> (Student) hits.getContent())
.extracting(Student::lastName)
.containsExactly("Dunlop");
assertThat(operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())).isTrue();
assertThat(operations.deleteScript(SCRIPT_SEARCH_LASTNAME.id())).isTrue();
}
@Test // #2704
@DisplayName("should search with template multisearch with multiple index coordinates")
void shouldSearchWithTemplateMultiSearchWithMultipleIndexCoordinates() {
assertThat(operations.putScript(SCRIPT_SEARCH_FIRSTNAME)).isTrue();
assertThat(operations.putScript(SCRIPT_SEARCH_LASTNAME)).isTrue();
var q1 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_FIRSTNAME.id()) //
.withParams(Map.of("firstName", "John")) //
.build();
var q2 = SearchTemplateQuery.builder() //
.withId(SCRIPT_SEARCH_LASTNAME.id()) //
.withParams(Map.of("lastName", "Dunlop")) //
.build();
// search with multiple index coordinates
var multiSearchHits = operations.multiSearch(
List.of(q1, q2),
List.of(Person.class, Student.class),
List.of(IndexCoordinates.of(indexNameProvider.indexName() + "-person"),
IndexCoordinates.of(indexNameProvider.indexName() + "-student")));
assertThat(multiSearchHits.size()).isEqualTo(2);
assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2);
assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(2);
assertThat(multiSearchHits.get(0).getSearchHits())
// type casting is needed here
.extracting(hits -> (Person) hits.getContent())
.extracting(Person::lastName)
.contains("Smith", "Myers");
assertThat(multiSearchHits.get(1).getSearchHits())
// type casting is needed here
.extracting(hits -> (Student) hits.getContent())
.extracting(Student::firstName)
.contains("Joey", "Michael");
assertThat(operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())).isTrue();
assertThat(operations.deleteScript(SCRIPT_SEARCH_LASTNAME.id())).isTrue();
}
@Document(indexName = "#{@indexNameProvider.indexName()}-person")
record Person( //
@Nullable @Id String id, //
@Field(type = FieldType.Text) String firstName, //
@Field(type = FieldType.Text) String lastName //
) {
}
@Document(indexName = "#{@indexNameProvider.indexName()}-student")
record Student( //
@Nullable @Id String id, //
@Field(type = FieldType.Text) String firstName, //
@Field(type = FieldType.Text) String lastName //
) {
}
}