From 73a42f350675f358f8bc22eeec2d743ee9cf142b Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Mon, 6 Feb 2023 19:48:30 +0100 Subject: [PATCH] Option to add docvalue_fields to a search query. #Closes 2316 --- .../client/elc/RequestConverter.java | 24 ++++-- .../elasticsearch/core/query/BaseQuery.java | 23 +++++- .../core/query/BaseQueryBuilder.java | 20 +++++ .../core/query/DocValueField.java | 35 ++++++++ .../data/elasticsearch/core/query/Query.java | 10 ++- .../client/elc/RequestConverterTest.java | 82 +++++++++++++++++++ 6 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java index ec045fa37..5f6d6933d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java @@ -31,6 +31,7 @@ import co.elastic.clients.elasticsearch._types.mapping.RuntimeField; import co.elastic.clients.elasticsearch._types.mapping.RuntimeFieldType; import co.elastic.clients.elasticsearch._types.mapping.TypeMapping; +import co.elastic.clients.elasticsearch._types.query_dsl.FieldAndFormat; import co.elastic.clients.elasticsearch._types.query_dsl.Like; import co.elastic.clients.elasticsearch.cluster.HealthRequest; import co.elastic.clients.elasticsearch.core.*; @@ -1298,6 +1299,12 @@ private void prepareSearchRequest(Query query, @Nullable Class clazz, Ind // noinspection unchecked builder.indicesBoost(boosts); } + + if (!isEmpty(query.getDocValueFields())) { + builder.docvalueFields(query.getDocValueFields().stream() // + .map(docValueField -> FieldAndFormat.of(b -> b.field(docValueField.field()).format(docValueField.format()))) + .toList()); + } } private Rescore getRescore(RescorerQuery rescorerQuery) { @@ -1554,14 +1561,13 @@ public SearchTemplateRequest searchTemplate(SearchTemplateQuery query, IndexCoor return SearchTemplateRequest.of(builder -> { builder // - .allowNoIndices(query.getAllowNoIndices()) // - .explain(query.getExplain()) // - .id(query.getId()) // - .index(Arrays.asList(index.getIndexNames())) // - .preference(query.getPreference()) // - .routing(query.getRoute()) // - .searchType(searchType(query.getSearchType())) - .source(query.getSource()) // + .allowNoIndices(query.getAllowNoIndices()) // + .explain(query.getExplain()) // + .id(query.getId()) // + .index(Arrays.asList(index.getIndexNames())) // + .preference(query.getPreference()) // + .routing(query.getRoute()) // + .searchType(searchType(query.getSearchType())).source(query.getSource()) // ; var expandWildcards = query.getExpandWildcards(); @@ -1577,7 +1583,7 @@ public SearchTemplateRequest searchTemplate(SearchTemplateQuery query, IndexCoor Function, String> keyMapper = Map.Entry::getKey; Function, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper); Map params = query.getParams().entrySet().stream() - .collect(Collectors.toMap(keyMapper, valueMapper)); + .collect(Collectors.toMap(keyMapper, valueMapper)); builder.params(params); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java index 57bc02921..8978717c7 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java @@ -78,9 +78,9 @@ public class BaseQuery implements Query { @Nullable protected PointInTime pointInTime; private boolean queryIsUpdatedByConverter = false; @Nullable private Integer reactiveBatchSize = null; -@Nullable private Boolean allowNoIndices = null; - + @Nullable private Boolean allowNoIndices = null; private EnumSet expandWildcards; + private List docValueFields = new ArrayList<>(); public BaseQuery() {} @@ -114,6 +114,7 @@ public > BaseQuery(BaseQue this.reactiveBatchSize = builder.getReactiveBatchSize(); this.allowNoIndices = builder.getAllowNoIndices(); this.expandWildcards = builder.getExpandWildcards(); + this.docValueFields = builder.getDocValueFields(); } /** @@ -524,4 +525,22 @@ public Boolean getAllowNoIndices() { public EnumSet getExpandWildcards() { return expandWildcards; } + + /** + * @since 5.1 + */ + @Override + public List getDocValueFields() { + return docValueFields; + } + + /** + * @since 5.1 + */ + public void setDocValueFields(List docValueFields) { + + Assert.notNull(docValueFields, "getDocValueFields must not be null"); + + this.docValueFields = docValueFields; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java index d858d3971..e85829667 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java @@ -70,6 +70,7 @@ public abstract class BaseQueryBuilder expandWildcards = EnumSet.noneOf(IndicesOptions.WildcardStates.class); @Nullable Integer reactiveBatchSize; + private final List docValueFields = new ArrayList<>(); @Nullable public Sort getSort() { @@ -218,6 +219,13 @@ public EnumSet getExpandWildcards() { return expandWildcards; } + /** + * @since 5.1 + */ + public List getDocValueFields() { + return docValueFields; + } + public SELF withPageable(Pageable pageable) { this.pageable = pageable; return self(); @@ -423,6 +431,18 @@ public SELF withExpandWildcards(EnumSet expandWil return self(); } + /** + * @since 5.1 + */ + public SELF withDocValueFields(List docValueFields) { + + Assert.notNull(docValueFields, "docValueFields must not be null"); + + this.docValueFields.clear(); + this.docValueFields.addAll(docValueFields); + return self(); + } + public abstract Q build(); private SELF self() { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java new file mode 100644 index 000000000..71ad09e9e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 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.core.query; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Record defining a docvalue_field to be used in a query. + * + * @author Peter-Josef Meisch + * @since 5.1 + */ +public record DocValueField(String field, @Nullable String format) { + public DocValueField { + Assert.notNull(field, "field must not be null"); + } + + public DocValueField(String field) { + this(field, null); + } +} 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 d62513c72..6d98c39b5 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 @@ -472,8 +472,14 @@ default Integer getReactiveBatchSize() { EnumSet getExpandWildcards(); /** - * @since 4.3 - */ + * @return a possible empty list of docvalue_field values to be set on the query. + * @since 5.1 + */ + List getDocValueFields(); + + /** + * @since 4.3 + */ enum SearchType { QUERY_THEN_FETCH, DFS_QUERY_THEN_FETCH } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java new file mode 100644 index 000000000..cb314a988 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 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 static org.assertj.core.api.Assertions.*; + +import co.elastic.clients.json.jackson.JacksonJsonpMapper; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.elasticsearch.core.query.DocValueField; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.StringQuery; +import org.springframework.lang.Nullable; + +import java.util.List; + +/** + * @author Peter-Josef Meisch + */ +class RequestConverterTest { + + private static final SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); + private static final MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); + private JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(); + private RequestConverter requestConverter = new RequestConverter(converter, jsonpMapper); + + @Test // #2316 + @DisplayName("should add docvalue_fields") + void shouldAddDocvalueFields() { + + var docValueFields = List.of( // + new DocValueField("field1"), // + new DocValueField("field2", "format2") // + ); + // doesn't matter what type of query is used, the relevant part for docvalue_fields is in the base builder. + var query = StringQuery.builder(""" + { + "match_all":{} + } + """) // + .withDocValueFields(docValueFields) // + .build(); + + var searchRequest = requestConverter.searchRequest(query, SampleEntity.class, IndexCoordinates.of("foo"), true); + + var fieldAndFormats = searchRequest.docvalueFields(); + assertThat(fieldAndFormats).hasSize(2); + assertThat(fieldAndFormats.get(0).field()).isEqualTo("field1"); + assertThat(fieldAndFormats.get(0).format()).isNull(); + assertThat(fieldAndFormats.get(1).field()).isEqualTo("field2"); + assertThat(fieldAndFormats.get(1).format()).isEqualTo("format2"); + } + + @Document(indexName = "does-not-matter") + static class SampleEntity { + @Nullable + @Id private String id; + @Nullable + @Field(type = FieldType.Text) private String text; + } +}