From 3aac111ea3586a8be63a163a352a6f11aae01181 Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Fri, 11 Aug 2023 16:07:39 +0200 Subject: [PATCH] Scripted and runtime fields improvements. Closes #2035 --- ...elasticsearch-migration-guide-5.1-5.2.adoc | 7 + ...arch-misc-scripted-and-runtime-fields.adoc | 223 ++++++++++ .../reference/elasticsearch-misc.adoc | 6 +- .../annotations/ScriptedField.java | 1 + .../client/elc/RequestConverter.java | 8 +- .../elasticsearch/core/query/BaseQuery.java | 11 +- .../core/query/BaseQueryBuilder.java | 1 - .../core/query/FetchSourceFilter.java | 20 + .../data/elasticsearch/core/query/Query.java | 1 - .../core/{ => query}/RuntimeField.java | 2 +- .../elasticsearch/core/query/ScriptData.java | 87 +++- .../core/{ => query}/ScriptType.java | 2 +- .../core/query/ScriptedField.java | 9 + .../elasticsearch/core/query/UpdateQuery.java | 1 - .../AbstractElasticsearchRepositoryQuery.java | 25 +- ...tReactiveElasticsearchRepositoryQuery.java | 29 +- .../query/ElasticsearchParameter.java | 55 +++ .../query/ElasticsearchParameters.java | 57 +-- ...sticsearchParametersParameterAccessor.java | 9 +- .../query/ElasticsearchPartQuery.java | 8 +- .../query/ElasticsearchQueryMethod.java | 55 ++- .../query/ElasticsearchStringQuery.java | 4 +- ...sticsearchParametersParameterAccessor.java | 2 +- .../ReactiveElasticsearchQueryMethod.java | 5 +- .../ReactivePartTreeElasticsearchQuery.java | 4 +- .../ElasticsearchELCIntegrationTests.java | 1 + .../core/ElasticsearchIntegrationTests.java | 2 +- .../core/RuntimeFieldsIntegrationTests.java | 200 --------- .../core/{ => query}/RuntimeFieldTest.java | 2 +- ...edAndRuntimeFieldsELCIntegrationTests.java | 27 ++ ...iptedAndRuntimeFieldsIntegrationTests.java | 416 ++++++++++++++++++ ...dAndRuntimeFieldsELCIntegrationTests.java} | 10 +- ...iptedAndRuntimeFieldsIntegrationTests.java | 413 +++++++++++++++++ 33 files changed, 1404 insertions(+), 299 deletions(-) create mode 100644 src/main/asciidoc/reference/elasticsearch-misc-scripted-and-runtime-fields.adoc rename src/main/java/org/springframework/data/elasticsearch/core/{ => query}/RuntimeField.java (96%) rename src/main/java/org/springframework/data/elasticsearch/core/{ => query}/ScriptType.java (92%) create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameter.java delete mode 100644 src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsIntegrationTests.java rename src/test/java/org/springframework/data/elasticsearch/core/{ => query}/RuntimeFieldTest.java (96%) create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsELCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java rename src/test/java/org/springframework/data/elasticsearch/core/{RuntimeFieldsELCIntegrationTests.java => query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java} (70%) create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java diff --git a/src/main/asciidoc/reference/elasticsearch-migration-guide-5.1-5.2.adoc b/src/main/asciidoc/reference/elasticsearch-migration-guide-5.1-5.2.adoc index d5534be2a6..d0329ea7b1 100644 --- a/src/main/asciidoc/reference/elasticsearch-migration-guide-5.1-5.2.adoc +++ b/src/main/asciidoc/reference/elasticsearch-migration-guide-5.1-5.2.adoc @@ -6,6 +6,7 @@ This section describes breaking changes from version 5.1.x to 5.2.x and how remo [[elasticsearch-migration-guide-5.1-5.2.breaking-changes]] == Breaking Changes +=== Bulk failures In the `org.springframework.data.elasticsearch.BulkFailureException` class, the return type of the `getFailedDocuments` is changed from `Map` to `Map`, which allows to get additional details about failure reasons. @@ -14,6 +15,12 @@ The definition of the `FailureDetails` class (inner to `BulkFailureException`): public record FailureDetails(Integer status, String errorMessage) { } +=== scripted and runtime fields + +The classes `org.springframework.data.elasticsearch.core.RuntimeField` and `org.springframework.data.elasticsearch.core.query.ScriptType` have been moved to the subpackage `org.springframework.data.elasticsearch.core.query`. + +The `type` parameter of the `ScriptData` constructir is not nullable any longer. + [[elasticsearch-migration-guide-5.1-5.2.deprecations]] == Deprecations diff --git a/src/main/asciidoc/reference/elasticsearch-misc-scripted-and-runtime-fields.adoc b/src/main/asciidoc/reference/elasticsearch-misc-scripted-and-runtime-fields.adoc new file mode 100644 index 0000000000..04c233c22a --- /dev/null +++ b/src/main/asciidoc/reference/elasticsearch-misc-scripted-and-runtime-fields.adoc @@ -0,0 +1,223 @@ +[[elasticsearch.misc.scripted-and-runtime-fields]] += Scripted and runtime fields + +Spring Data Elasticsearch supports scripted fields and runtime fields. +Please refer to the Elasticsearch documentation about scripting (https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html) and runtime fields (https://www.elastic.co/guide/en/elasticsearch/reference/8.9/runtime.html) for detailed information about this. +In the context of Spring Data Elasticsearch you can use + +* scripted fields that are used to return fields that are calculated on the result documents and added to the returned document. +* runtime fields that are calculated on the stored documents and can be used in a query and/or be returned in the search result. + +The following code snippets will show what you can do (this show imperative code, but the reactive implementation works similar). + +== The person entity + +The enity that is used in these examples is a `Person` entity. +This entity has a `birthDate` and an `age` property. +Whereas the birthdate is fix, the age depends on the time when a query is issued and needs to be calculated dynamically. + +==== +[source,java] +---- +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.DateFormat; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.ScriptedField; +import org.springframework.lang.Nullable; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.springframework.data.elasticsearch.annotations.FieldType.*; + +import java.lang.Integer; + +@Document(indexName = "persons") +public record Person( + @Id + @Nullable + String id, + @Field(type = Text) + String lastName, + @Field(type = Text) + String firstName, + @Field(type = Keyword) + String gender, + @Field(type = Date, format = DateFormat.basic_date) + LocalDate birthDate, + @Nullable + @ScriptedField Integer age <.> +) { + public Person(String id,String lastName, String firstName, String gender, String birthDate) { + this(id, <.> + lastName, + firstName, + LocalDate.parse(birthDate, DateTimeFormatter.ISO_LOCAL_DATE), + gender, + null); + } +} + +---- + +<.> the `age` property will be calculated and filled in search results. +<.> a convenience constructor to set up the test data +==== + +Note that the `age` property is annotated with `@ScriptedField`. +This inhibits the writing of a corresponding entry in the index mapping and marks the property as a target to put a calculated field from a search response. + +== The repository interface + +The repository used in this example: + +==== +[source,java] +---- +public interface PersonRepository extends ElasticsearchRepository { + + SearchHits findAllBy(ScriptedField scriptedField); + + SearchHits findByGenderAndAgeLessThanEqual(String gender, Integer age, RuntimeField runtimeField); +} + +---- +==== + +== The service class + +The service class has a repository injected and an `ElasticsearchOperations` instance to show several ways of poplauting and using the `age` property. +We show the code split up in different pieces to put the explanations in + +==== +[source,java] +---- +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; +import org.springframework.data.elasticsearch.core.query.RuntimeField; +import org.springframework.data.elasticsearch.core.query.ScriptData; +import org.springframework.data.elasticsearch.core.query.ScriptType; +import org.springframework.data.elasticsearch.core.query.ScriptedField; +import org.springframework.data.elasticsearch.core.query.StringQuery; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class PersonService { + private final ElasticsearchOperations operations; + private final PersonRepository repository; + + public PersonService(ElasticsearchOperations operations, SaRPersonRepository repository) { + this.operations = operations; + this.repository = repository; + } + + public void save() { <.> + List persons = List.of( + new Person("1", "Smith", "Mary", "f", "1987-05-03"), + new Person("2", "Smith", "Joshua", "m", "1982-11-17"), + new Person("3", "Smith", "Joanna", "f", "2018-03-27"), + new Person("4", "Smith", "Alex", "m", "2020-08-01"), + new Person("5", "McNeill", "Fiona", "f", "1989-04-07"), + new Person("6", "McNeill", "Michael", "m", "1984-10-20"), + new Person("7", "McNeill", "Geraldine", "f", "2020-03-02"), + new Person("8", "McNeill", "Patrick", "m", "2022-07-04")); + + repository.saveAll(persons); + } +---- + +<.> a utility method to store some data in Elasticsearch. +==== + +=== Scripted fields + +The next piece show how to use a scripted field to calculate and return the age of the persons. +Scripted fields can only add something to the returned data, the age cannot be used in the query (see runtime fields for that).) + +==== +[source,java] +---- + public SearchHits findAllWithAge() { + + var scriptedField = ScriptedField.of("age", <.> + ScriptData.of(b -> b + .withType(ScriptType.INLINE) + .withScript(""" + Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); + Instant startDate = doc['birth-date'].value.toInstant(); + return (ChronoUnit.DAYS.between(startDate, currentDate) / 365); + """))); + + // version 1: use a direct query + var query = new StringQuery(""" + { "match_all": {} } + """); + query.addScriptedField(scriptedField); <.> + query.addSourceFilter(FetchSourceFilter.of(b -> b.withIncludes("*"))); <.> + + var result1 = operations.search(query, Person.class); <.> + + // version 2: use the repository + var result2 = repository.findAllBy(scriptedField); <.> + + return result1; + } +---- + +<.> define the `ScriptedField` that calculates the age of a person. +<.> when using a `Query`, add the scripted field to the query. +<.> when adding a scripted field to a `Query`, an additional source filter is needed to also retrieve the _normal_ fields from the document source. +<.> get the data where the `Person` entities now have the values set in their `age` property. +<.> when using the repository, all that needs to be done is adding the scripted field as method parameter. +==== + +=== Runtime fields + +When using runtime fields, the calculated value can be used in the query itself. +In the following code this is used to run a query for a given gender and maximum age of persons: + +==== +[source,java] +---- + public SearchHits findWithGenderAndMaxAge(String gender, Integer maxAge) { + + var runtimeField = new RuntimeField("age", "long", """ <.> + Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); + Instant startDate = doc['birth-date'].value.toInstant(); + emit (ChronoUnit.DAYS.between(startDate, currentDate) / 365); + """); + + // variant 1 : use a direct query + var query = CriteriaQuery.builder(Criteria + .where("gender").is(gender) + .and("age").lessThanEqual(maxAge)) + .withRuntimeFields(List.of(runtimeField)) <.> + .withFields("age") <.> + .withSourceFilter(FetchSourceFilter.of(b -> b.withIncludes("*"))) <.> + .build(); + + var result1 = operations.search(query, Person.class); <.> + + // variant 2: use the repository <.> + var result2 = repository.findByGenderAndAgeLessThanEqual(gender, maxAge, runtimeField); + + return result1; + } +} +---- + +<.> define the runtime field that caclulates the // see https://asciidoctor.org/docs/user-manual/#builtin-attributes for builtin attributes. +<.> when using `Query`, add the runtime field. +<.> when adding a scripted field to a `Query`, an additional field parameter is needed to have the calculated value returned. +<.> when adding a scripted field to a `Query`, an additional source filter is needed to also retrieve the _normal_ fields from the document source. +<.> get the data filtered with the query and where the returned entites have the age property set. +<.> when using the repository, all that needs to be done is adding the runtime field as method parameter. +==== + +In addition to define a runtime fields on a query, they can also be defined in the index by setting the `runtimeFIeldPath` property of the `@Mapping` annotation to point to a JSON file that contains the runtime field definitions. diff --git a/src/main/asciidoc/reference/elasticsearch-misc.adoc b/src/main/asciidoc/reference/elasticsearch-misc.adoc index c865ba5336..ac14dd1046 100644 --- a/src/main/asciidoc/reference/elasticsearch-misc.adoc +++ b/src/main/asciidoc/reference/elasticsearch-misc.adoc @@ -365,7 +365,7 @@ operations.putScript( <.> To use a search template in a search query, Spring Data Elasticsearch provides the `SearchTemplateQuery`, an implementation of the `org.springframework.data.elasticsearch.core.query.Query` interface. -In the following code, we will add a call using a search template query to a custom repository implementation (see +In the following code, we will add a call using a search template query to a custom repository implementation (see <>) as an example how this can be integrated into a repository call. @@ -399,7 +399,7 @@ public class PersonCustomRepositoryImpl implements PersonCustomRepository { var query = SearchTemplateQuery.builder() <.> .withId("person-firstname") <.> .withParams( - Map.of( <.> + Map.of( <.> "firstName", firstName, "from", pageable.getOffset(), "size", pageable.getPageSize() @@ -450,3 +450,5 @@ var query = Query.findAll().addSort(Sort.by(order)); About the filter query: It is not possible to use a `CriteriaQuery` here, as this query would be converted into a Elasticsearch nested query which does not work in the filter context. So only `StringQuery` or `NativeQuery` can be used here. When using one of these, like the term query above, the Elasticsearch field names must be used, so take care, when these are redefined with the `@Field(name="...")` definition. For the definition of the order path and the nested paths, the Java entity property names should be used. + +include::elasticsearch-misc-scripted-and-runtime-fields.adoc[leveloffset=+1] diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java index f1eb3e06a3..cc596c54f3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java @@ -3,6 +3,7 @@ import java.lang.annotation.*; /** + * Marks a property to be populated with the result of a scripted field retrieved from an Elasticsearch response. * @author Ryan Murfitt */ @Retention(RetentionPolicy.RUNTIME) 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 8dbaf3b0ae..e1903eb18d 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 @@ -67,7 +67,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.RefreshPolicy; -import org.springframework.data.elasticsearch.core.ScriptType; +import org.springframework.data.elasticsearch.core.query.ScriptType; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.index.*; @@ -1333,10 +1333,8 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu } if (!isEmpty(query.getFields())) { - builder.fields(fb -> { - query.getFields().forEach(fb::field); - return fb; - }); + var fieldAndFormats = query.getFields().stream().map(field -> FieldAndFormat.of(b -> b.field(field))).toList(); + builder.fields(fieldAndFormats); } if (!isEmpty(query.getStoredFields())) { 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 bb25e13037..9f9462370d 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 @@ -29,7 +29,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.elasticsearch.core.RuntimeField; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -548,6 +547,16 @@ public void setDocValueFields(List docValueFields) { this.docValueFields = docValueFields; } + /** + * @since 5.2 + */ + public void addScriptedField(ScriptedField scriptedField) { + + Assert.notNull(scriptedField, "scriptedField must not be null"); + + this.scriptedFields.add(scriptedField); + } + @Override public List getScriptedFields() { return scriptedFields; 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 8c3f4f46f9..18f17acf1e 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 @@ -25,7 +25,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.elasticsearch.core.RuntimeField; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java b/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java index 18b79a1f72..0182c7459e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java @@ -15,7 +15,10 @@ */ package org.springframework.data.elasticsearch.core.query; +import java.util.function.Function; + import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * SourceFilter implementation for providing includes and excludes. @@ -28,6 +31,23 @@ public class FetchSourceFilter implements SourceFilter { @Nullable private final String[] includes; @Nullable private final String[] excludes; + /** + * @since 5.2 + */ + public static SourceFilter of(@Nullable final String[] includes, @Nullable final String[] excludes) { + return new FetchSourceFilter(includes, excludes); + } + + /** + * @since 5.2 + */ + public static SourceFilter of(Function builderFunction) { + + Assert.notNull(builderFunction, "builderFunction must not be null"); + + return builderFunction.apply(new FetchSourceFilterBuilder()).build(); + } + public FetchSourceFilter(@Nullable final String[] includes, @Nullable final String[] excludes) { this.includes = includes; this.excludes = excludes; 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 39ac660765..009594f4ef 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 @@ -25,7 +25,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.elasticsearch.core.RuntimeField; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/RuntimeField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/RuntimeField.java similarity index 96% rename from src/main/java/org/springframework/data/elasticsearch/core/RuntimeField.java rename to src/main/java/org/springframework/data/elasticsearch/core/query/RuntimeField.java index a2302e7c5e..902a2120f8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RuntimeField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/RuntimeField.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.elasticsearch.core; +package org.springframework.data.elasticsearch.core.query; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java index 045265b69a..8b54cad637 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java @@ -16,9 +16,10 @@ package org.springframework.data.elasticsearch.core.query; import java.util.Map; +import java.util.function.Function; -import org.springframework.data.elasticsearch.core.ScriptType; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * value class combining script information. @@ -26,6 +27,88 @@ * @author Peter-Josef Meisch * @since 4.4 */ -public record ScriptData(@Nullable ScriptType type, @Nullable String language, @Nullable String script, +public record ScriptData(ScriptType type, @Nullable String language, @Nullable String script, @Nullable String scriptName, @Nullable Map params) { + + public ScriptData(ScriptType type, @Nullable String language, @Nullable String script, @Nullable String scriptName, + @Nullable Map params) { + + Assert.notNull(type, "type must not be null"); + + this.type = type; + this.language = language; + this.script = script; + this.scriptName = scriptName; + this.params = params; + } + + /** + * @since 5.2 + */ + public static ScriptData of(ScriptType type, @Nullable String language, @Nullable String script, + @Nullable String scriptName, @Nullable Map params) { + return new ScriptData(type, language, script, scriptName, params); + } + + public static ScriptData of(Function builderFunction) { + + Assert.notNull(builderFunction, "f must not be null"); + + return builderFunction.apply(new Builder()).build(); + } + + /** + * @since 5.2 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * @since 5.2 + */ + public static final class Builder { + @Nullable private ScriptType type; + @Nullable private String language; + @Nullable private String script; + @Nullable private String scriptName; + @Nullable private Map params; + + private Builder() {} + + public Builder withType(ScriptType type) { + + Assert.notNull(type, "type must not be null"); + + this.type = type; + return this; + } + + public Builder withLanguage(@Nullable String language) { + this.language = language; + return this; + } + + public Builder withScript(@Nullable String script) { + this.script = script; + return this; + } + + public Builder withScriptName(@Nullable String scriptName) { + this.scriptName = scriptName; + return this; + } + + public Builder withParams(@Nullable Map params) { + this.params = params; + return this; + } + + public ScriptData build() { + + Assert.notNull(type, "type must be set"); + + return new ScriptData(type, language, script, scriptName, params); + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ScriptType.java b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptType.java similarity index 92% rename from src/main/java/org/springframework/data/elasticsearch/core/ScriptType.java rename to src/main/java/org/springframework/data/elasticsearch/core/query/ScriptType.java index f10f72d5bc..f2b54c923f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ScriptType.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptType.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.elasticsearch.core; +package org.springframework.data.elasticsearch.core.query; /** * Define script types for update queries. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java index ffa31762a9..2101fcfc6e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java @@ -18,6 +18,8 @@ import org.springframework.util.Assert; /** + * Class defining a scripted field to be used in a {@link Query}. Must be set by using the builder for a query. + * * @author Peter-Josef Meisch * @since 4.4 */ @@ -26,6 +28,13 @@ public class ScriptedField { private final String fieldName; private final ScriptData scriptData; + /** + * @since 5.2 + */ + public static ScriptedField of(String fieldName, ScriptData scriptData) { + return new ScriptedField(fieldName, scriptData); + } + public ScriptedField(String fieldName, ScriptData scriptData) { Assert.notNull(fieldName, "fieldName must not be null"); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/UpdateQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/UpdateQuery.java index 777a4c7931..32b4c14685 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/UpdateQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/UpdateQuery.java @@ -19,7 +19,6 @@ import java.util.Map; import org.springframework.data.elasticsearch.core.RefreshPolicy; -import org.springframework.data.elasticsearch.core.ScriptType; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.lang.Nullable; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java index 9df5c70625..b5814169c1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java @@ -25,6 +25,7 @@ import org.springframework.data.elasticsearch.core.TotalHitsRelation; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; 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.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; @@ -75,7 +76,7 @@ public QueryMethod getQueryMethod() { @Override public Object execute(Object[] parameters) { - ParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); + ElasticsearchParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); Class clazz = resultProcessor.getReturnedType().getDomainType(); @@ -135,29 +136,19 @@ public Object execute(Object[] parameters) { public Query createQuery(Object[] parameters) { - ParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); + ElasticsearchParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); - Class returnedType = resultProcessor.getReturnedType().getDomainType(); - - Query query = createQuery(parameterAccessor); + var query = createQuery(parameterAccessor); Assert.notNull(query, "unsupported query"); - if (queryMethod.hasAnnotatedHighlight()) { - query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery()); - } - - var sourceFilter = queryMethod.getSourceFilter(parameterAccessor, - elasticsearchOperations.getElasticsearchConverter()); - if (sourceFilter != null) { - query.addSourceFilter(sourceFilter); - } + queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter()); return query; } - private ParametersParameterAccessor getParameterAccessor(Object[] parameters) { - return new ParametersParameterAccessor(queryMethod.getParameters(), parameters); + private ElasticsearchParametersParameterAccessor getParameterAccessor(Object[] parameters) { + return new ElasticsearchParametersParameterAccessor(queryMethod, parameters); } @Nullable @@ -185,5 +176,5 @@ private Object countOrGetDocumentsForDelete(Query query, ParametersParameterAcce return result; } - protected abstract Query createQuery(ParametersParameterAccessor accessor); + protected abstract BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java index 2a079ae39c..cbb984e96b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java @@ -26,6 +26,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.ByQueryResponse; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingConverter; @@ -35,6 +36,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.util.Assert; /** * AbstractElasticsearchRepositoryQuery @@ -79,7 +81,7 @@ private Object executeDeferred(Object[] parameters) { return Mono.defer(() -> (Mono) execute(parameterAccessor)); } - private Object execute(ElasticsearchParameterAccessor parameterAccessor) { + private Object execute(ElasticsearchParametersParameterAccessor parameterAccessor) { ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); var returnedType = processor.getReturnedType(); @@ -90,17 +92,10 @@ private Object execute(ElasticsearchParameterAccessor parameterAccessor) { typeToRead = queryMethod.unwrappedReturnType; } - Query query = createQuery(parameterAccessor); + var query = createQuery(parameterAccessor); + Assert.notNull(query, "unsupported query"); - if (queryMethod.hasAnnotatedHighlight()) { - query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery()); - } - - var sourceFilter = queryMethod.getSourceFilter(parameterAccessor, - elasticsearchOperations.getElasticsearchConverter()); - if (sourceFilter != null) { - query.addSourceFilter(sourceFilter); - } + queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter()); String indexName = queryMethod.getEntityInformation().getIndexName(); IndexCoordinates index = IndexCoordinates.of(indexName); @@ -111,18 +106,18 @@ private Object execute(ElasticsearchParameterAccessor parameterAccessor) { return execution.execute(query, domainType, typeToRead, index); } - private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor, - Converter resultProcessing) { - return new ResultProcessingExecution(getExecutionToWrap(accessor, elasticsearchOperations), resultProcessing); - } - /** * Creates a {@link Query} instance using the given {@link ParameterAccessor} * * @param accessor must not be {@literal null}. * @return */ - protected abstract Query createQuery(ElasticsearchParameterAccessor accessor); + protected abstract BaseQuery createQuery(ElasticsearchParameterAccessor accessor); + + private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor, + Converter resultProcessing) { + return new ResultProcessingExecution(getExecutionToWrap(accessor, elasticsearchOperations), resultProcessing); + } private ReactiveElasticsearchQueryExecution getExecutionToWrap(ElasticsearchParameterAccessor accessor, ReactiveElasticsearchOperations operations) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameter.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameter.java new file mode 100644 index 0000000000..45f4efb972 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-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.repository.query; + +import org.springframework.core.MethodParameter; +import org.springframework.data.elasticsearch.core.query.RuntimeField; +import org.springframework.data.elasticsearch.core.query.ScriptedField; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.util.TypeInformation; + +/** + * Custom {@link Parameter} implementation adding specific types to the special ones. Refactored from being defined in + * {@link ElasticsearchParameters}. + * + * @author Christoph Strobl + * @author Peter-Josef Meisch + * @since 5.2 + */ +class ElasticsearchParameter extends Parameter { + + /** + * Creates a new {@link ElasticsearchParameter}. + * + * @param parameter must not be {@literal null}. + */ + ElasticsearchParameter(MethodParameter parameter, TypeInformation domainType) { + super(parameter, domainType); + } + + @Override + public boolean isSpecialParameter() { + return super.isSpecialParameter() || isScriptedFieldParameter() || isRuntimeFieldParameter(); + } + + public Boolean isScriptedFieldParameter() { + return ScriptedField.class.isAssignableFrom(getType()); + } + + public Boolean isRuntimeFieldParameter() { + return RuntimeField.class.isAssignableFrom(getType()); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java index 78503880a4..d1a76daaf6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java @@ -16,13 +16,12 @@ package org.springframework.data.elasticsearch.repository.query; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import org.springframework.core.MethodParameter; -import org.springframework.data.elasticsearch.repository.query.ElasticsearchParameters.ElasticsearchParameter; -import org.springframework.data.geo.Distance; -import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; +import org.springframework.data.util.TypeInformation; /** * @author Christoph Strobl @@ -31,17 +30,35 @@ */ public class ElasticsearchParameters extends Parameters { - public ElasticsearchParameters(Method method) { - super(method); + private final List scriptedFields = new ArrayList<>(); + private final List runtimeFields = new ArrayList<>(); + + public ElasticsearchParameters(Method method, TypeInformation domainType) { + + super(method, parameter -> new ElasticsearchParameter(parameter, domainType)); + + int parameterCount = method.getParameterCount(); + for (int i = 0; i < parameterCount; i++) { + MethodParameter methodParameter = new MethodParameter(method, i); + var parameter = parameterFactory(methodParameter, domainType); + + if (parameter.isScriptedFieldParameter()) { + scriptedFields.add(parameter); + } + + if (parameter.isRuntimeFieldParameter()) { + runtimeFields.add(parameter); + } + } + } - private ElasticsearchParameters(List parameters) { - super(parameters); + private ElasticsearchParameter parameterFactory(MethodParameter methodParameter, TypeInformation domainType) { + return new ElasticsearchParameter(methodParameter, domainType); } - @Override - protected ElasticsearchParameter createParameter(MethodParameter parameter) { - return new ElasticsearchParameter(parameter); + private ElasticsearchParameters(List parameters) { + super(parameters); } @Override @@ -49,21 +66,11 @@ protected ElasticsearchParameters createFrom(List parame return new ElasticsearchParameters(parameters); } - /** - * Custom {@link Parameter} implementation adding parameters of type {@link Distance} to the special ones. - * - * @author Christoph Strobl - */ - class ElasticsearchParameter extends Parameter { - - /** - * Creates a new {@link ElasticsearchParameter}. - * - * @param parameter must not be {@literal null}. - */ - ElasticsearchParameter(MethodParameter parameter) { - super(parameter); - } + List getScriptedFields() { + return scriptedFields; + } + List getRuntimeFields() { + return runtimeFields; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java index d7107c51a0..67267467a7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java @@ -15,9 +15,6 @@ */ package org.springframework.data.elasticsearch.repository.query; -import java.util.Arrays; -import java.util.List; - import org.springframework.data.repository.query.ParametersParameterAccessor; /** @@ -27,7 +24,7 @@ class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor implements ElasticsearchParameterAccessor { - private final List values; + private final Object[] values; /** * Creates a new {@link ElasticsearchParametersParameterAccessor}. @@ -38,11 +35,11 @@ class ElasticsearchParametersParameterAccessor extends ParametersParameterAccess ElasticsearchParametersParameterAccessor(ElasticsearchQueryMethod method, Object... values) { super(method.getParameters(), values); - this.values = Arrays.asList(values); + this.values = values; } @Override public Object[] getValues() { - return values.toArray(); + return values; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java index 72cb292dea..3463b2b5d0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java @@ -19,9 +19,10 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.RuntimeField; +import org.springframework.data.elasticsearch.core.query.ScriptedField; import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.parser.PartTree; /** @@ -60,12 +61,11 @@ protected boolean isExistsQuery() { return tree.isExistsProjection(); } - protected Query createQuery(ParametersParameterAccessor accessor) { + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) { BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery(); - if (tree.isLimiting()) { - // noinspection ConstantConditions + if (tree.getMaxResults() != null) { query.setMaxResults(tree.getMaxResults()); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java index 4ab1889465..6af2b618e9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java @@ -34,14 +34,19 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; import org.springframework.data.elasticsearch.core.query.HighlightQuery; +import org.springframework.data.elasticsearch.core.query.RuntimeField; +import org.springframework.data.elasticsearch.core.query.ScriptedField; import org.springframework.data.elasticsearch.core.query.SourceFilter; import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.repository.util.ReactiveWrapperConverters; @@ -70,7 +75,7 @@ public class ElasticsearchQueryMethod extends QueryMethod { // base class uses them in order to use our variables protected final Method method; protected final Class unwrappedReturnType; - private Boolean unwrappedReturnTypeFromSearchHit = null; + @Nullable private Boolean unwrappedReturnTypeFromSearchHit = null; private final MappingContext, ElasticsearchPersistentProperty> mappingContext; @Nullable private ElasticsearchEntityMetadata metadata; @@ -97,6 +102,11 @@ public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMeta verifyCountQueryTypes(); } + @Override + protected Parameters createParameters(Method method, TypeInformation domainType) { + return new ElasticsearchParameters(method, domainType); + } + protected void verifyCountQueryTypes() { if (hasCountQueryAnnotation()) { @@ -363,6 +373,49 @@ private Class potentiallyUnwrapReturnTypeFor(RepositoryMetadat } } } + + void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor, + ElasticsearchConverter elasticsearchConverter) { + + if (hasAnnotatedHighlight()) { + query.setHighlightQuery(getAnnotatedHighlightQuery()); + } + + var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter); + if (sourceFilter != null) { + query.addSourceFilter(sourceFilter); + } + + if (parameterAccessor.getParameters() instanceof ElasticsearchParameters methodParameters) { + var values = parameterAccessor.getValues(); + + methodParameters.getScriptedFields().forEach(elasticsearchParameter -> { + var index = elasticsearchParameter.getIndex(); + + if (index >= 0 && index < values.length) { + query.addScriptedField((ScriptedField) values[index]); + } + }); + + methodParameters.getRuntimeFields().forEach(elasticsearchParameter -> { + var index = elasticsearchParameter.getIndex(); + + if (index >= 0 && index < values.length) { + var runtimeField = (RuntimeField) values[index]; + query.addRuntimeField(runtimeField); + query.addFields(runtimeField.getName()); + } + + }); + + var needToAddSourceFilter = sourceFilter == null + && !(methodParameters.getRuntimeFields().isEmpty() + && methodParameters.getScriptedFields().isEmpty()); + if (needToAddSourceFilter) { + query.addSourceFilter(FetchSourceFilter.of(b -> b.withIncludes("*"))); + } + } + } // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java index 581e963621..de1417a516 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java @@ -16,10 +16,10 @@ package org.springframework.data.elasticsearch.repository.query; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; -import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.util.Assert; /** @@ -57,7 +57,7 @@ protected boolean isExistsQuery() { return false; } - protected Query createQuery(ParametersParameterAccessor parameterAccessor) { + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService()) .replacePlaceholders(this.queryString, parameterAccessor); diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java index d43073dc19..bba53bee38 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java @@ -81,7 +81,7 @@ protected T getValue(int index) { @Override public Object[] getValues() { - Object[] result = new Object[getValues().length]; + Object[] result = new Object[super.getValues().length]; for (int i = 0; i < result.length; i++) { result[i] = getValue(i); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java index 1c03d0b148..60ccbcd986 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java @@ -30,7 +30,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; -import org.springframework.data.elasticsearch.repository.query.ElasticsearchParameters.ElasticsearchParameter; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; @@ -103,8 +102,8 @@ protected void verifyCountQueryTypes() { } @Override - protected ElasticsearchParameters createParameters(Method method) { - return new ElasticsearchParameters(method); + protected ElasticsearchParameters createParameters(Method method, TypeInformation domainType) { + return new ElasticsearchParameters(method, domainType); } /** diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java index 0e77d72711..8563c50caf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java @@ -16,8 +16,8 @@ package org.springframework.data.elasticsearch.repository.query; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; -import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.parser.PartTree; @@ -40,7 +40,7 @@ public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod query } @Override - protected Query createQuery(ElasticsearchParameterAccessor accessor) { + protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) { CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery(); if (tree.isLimiting()) { diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java index a373afcd65..6ed2bd9bcb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java @@ -45,6 +45,7 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.RescorerQuery; import org.springframework.data.elasticsearch.core.query.ScriptData; +import org.springframework.data.elasticsearch.core.query.ScriptType; import org.springframework.data.elasticsearch.core.query.ScriptedField; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.utils.IndexNameProvider; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java index ab184ada24..bfed451f76 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java @@ -1630,7 +1630,7 @@ void shouldDoUpdateByQueryForExistingDocument() { final Query query = operations.matchAllQuery(); final UpdateQuery updateQuery = UpdateQuery.builder(query) - .withScriptType(org.springframework.data.elasticsearch.core.ScriptType.INLINE) + .withScriptType(ScriptType.INLINE) .withScript("ctx._source['message'] = params['newMessage']").withLang("painless") .withParams(Collections.singletonMap("newMessage", messageAfterUpdate)).withAbortOnVersionConflict(true) .build(); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsIntegrationTests.java deleted file mode 100644 index 3dafead198..0000000000 --- a/src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsIntegrationTests.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2021-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; - -import static org.assertj.core.api.Assertions.*; - -import java.time.LocalDate; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.ReadOnlyProperty; -import org.springframework.data.elasticsearch.annotations.DateFormat; -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.annotations.Mapping; -import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.data.elasticsearch.core.query.Criteria; -import org.springframework.data.elasticsearch.core.query.CriteriaQuery; -import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; -import org.springframework.data.elasticsearch.core.query.Query; -import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; -import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; - -/** - * @author Peter-Josef Meisch - * @author cdalxndr - */ -@SpringIntegrationTest -public abstract class RuntimeFieldsIntegrationTests { - - @Autowired private ElasticsearchOperations operations; - @Autowired protected IndexNameProvider indexNameProvider; - - @BeforeEach - void setUp() { - - indexNameProvider.increment(); - operations.indexOps(SomethingToBuy.class).createWithMapping(); - operations.indexOps(Person.class).createWithMapping(); - } - - @Test - @Order(java.lang.Integer.MAX_VALUE) - void cleanup() { - operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); - } - - @Test // #1971 - @DisplayName("should use runtime-field from query in search") - void shouldUseRuntimeFieldFromQueryInSearch() { - - insert("1", "item 1", 13.5); - insert("2", "item 2", 15); - Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5)); - RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)"); - query.addRuntimeField(runtimeField); - - SearchHits searchHits = operations.search(query, SomethingToBuy.class); - - assertThat(searchHits.getTotalHits()).isEqualTo(1); - assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("2"); - } - - @Test // #2267 - @DisplayName("should use runtime-field without script") - void shouldUseRuntimeFieldWithoutScript() { - - insert("1", "11", 10); - Query query = new CriteriaQuery(new Criteria("description").matches(11.0)); - RuntimeField runtimeField = new RuntimeField("description", "double"); - query.addRuntimeField(runtimeField); - - SearchHits searchHits = operations.search(query, SomethingToBuy.class); - - assertThat(searchHits.getTotalHits()).isEqualTo(1); - assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("1"); - } - - @Test // #2431 - @DisplayName("should return value from runtime field defined in mapping") - void shouldReturnValueFromRuntimeFieldDefinedInMapping() { - - var person = new Person(); - var years = 10; - person.setBirthDate(LocalDate.now().minusDays(years * 365 + 100)); - operations.save(person); - var query = Query.findAll(); - query.addFields("age"); - query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build()); - - var searchHits = operations.search(query, Person.class); - - assertThat(searchHits.getTotalHits()).isEqualTo(1); - assertThat(searchHits.getSearchHit(0).getContent().getAge()).isEqualTo(years); - } - - private void insert(String id, String description, double price) { - SomethingToBuy entity = new SomethingToBuy(); - entity.setId(id); - entity.setDescription(description); - entity.setPrice(price); - operations.save(entity); - } - - @Document(indexName = "#{@indexNameProvider.indexName()}-something") - private static class SomethingToBuy { - private @Id @Nullable String id; - - @Nullable - @Field(type = FieldType.Text) private String description; - - @Nullable - @Field(type = FieldType.Double) private Double price; - - @Nullable - public String getId() { - return id; - } - - public void setId(@Nullable String id) { - this.id = id; - } - - @Nullable - public String getDescription() { - return description; - } - - public void setDescription(@Nullable String description) { - this.description = description; - } - - @Nullable - public Double getPrice() { - return price; - } - - public void setPrice(@Nullable Double price) { - this.price = price; - } - } - - @Document(indexName = "#{@indexNameProvider.indexName()}-person") - @Mapping(runtimeFieldsPath = "/runtime-fields-person.json") - public class Person { - @Nullable private String id; - - @Field(type = FieldType.Date, format = DateFormat.basic_date) - @Nullable private LocalDate birthDate; - - @ReadOnlyProperty // do not write to prevent ES from automapping - @Nullable private Integer age; - - @Nullable - public String getId() { - return id; - } - - public void setId(@Nullable String id) { - this.id = id; - } - - @Nullable - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(@Nullable LocalDate birthDate) { - this.birthDate = birthDate; - } - - @Nullable - public Integer getAge() { - return age; - } - - public void setAge(@Nullable Integer age) { - this.age = age; - } - } -} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldTest.java b/src/test/java/org/springframework/data/elasticsearch/core/query/RuntimeFieldTest.java similarity index 96% rename from src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldTest.java rename to src/test/java/org/springframework/data/elasticsearch/core/query/RuntimeFieldTest.java index cbe0f560a8..1f05517967 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/RuntimeFieldTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.elasticsearch.core; +package org.springframework.data.elasticsearch.core.query; import static org.assertj.core.api.Assertions.*; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsELCIntegrationTests.java new file mode 100644 index 0000000000..4394eb8395 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsELCIntegrationTests.java @@ -0,0 +1,27 @@ +package org.springframework.data.elasticsearch.core.query.scriptedandruntimefields; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @since 5.2 + */ +@ContextConfiguration(classes = ReactiveScriptedAndRuntimeFieldsELCIntegrationTests.Config.class) +public class ReactiveScriptedAndRuntimeFieldsELCIntegrationTests + extends ReactiveScriptedAndRuntimeFieldsIntegrationTests { + + @Configuration + @Import({ ReactiveElasticsearchTemplateConfiguration.class }) + @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("reactive-scripted-runtime"); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java new file mode 100644 index 0000000000..6676d086ad --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java @@ -0,0 +1,416 @@ +/* + * 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.scriptedandruntimefields; + +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Flux; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.ReadOnlyProperty; +import org.springframework.data.elasticsearch.annotations.DateFormat; +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.annotations.Mapping; +import org.springframework.data.elasticsearch.annotations.ScriptedField; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.RuntimeField; +import org.springframework.data.elasticsearch.core.query.ScriptData; +import org.springframework.data.elasticsearch.core.query.ScriptType; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.lang.Nullable; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +public abstract class ReactiveScriptedAndRuntimeFieldsIntegrationTests { + + @Autowired private ReactiveElasticsearchOperations operations; + @Autowired protected IndexNameProvider indexNameProvider; + @Autowired private ReactiveSARRepository repository; + + @BeforeEach + void setUp() { + + indexNameProvider.increment(); + operations.indexOps(SomethingToBuy.class).createWithMapping().block(); + operations.indexOps(Person.class).createWithMapping().block(); + operations.indexOps(SAREntity.class).createWithMapping().block(); + } + + @Test + @Order(Integer.MAX_VALUE) + void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete().block(); + } + + @Test // #1971 + @DisplayName("should use runtime-field from query in search") + void shouldUseRuntimeFieldFromQueryInSearch() { + + insert("1", "item 1", 13.5); + insert("2", "item 2", 15); + Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5)); + RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)"); + query.addRuntimeField(runtimeField); + + List> searchHits = operations.search(query, SomethingToBuy.class).collectList().block(); + + assertThat(searchHits.size()).isEqualTo(1); + var searchHit = searchHits.get(0); + assertThat(searchHit.getId()).isEqualTo("2"); + var foundEntity = searchHit.getContent(); + assertThat(foundEntity.getDescription()).isEqualTo("item 2"); + } + + @Test // #2267 + @DisplayName("should use runtime-field without script") + void shouldUseRuntimeFieldWithoutScript() { + + insert("1", "11", 10); + Query query = new CriteriaQuery(new Criteria("description").matches(11.0)); + RuntimeField runtimeField = new RuntimeField("description", "double"); + query.addRuntimeField(runtimeField); + + List> searchHits = operations.search(query, SomethingToBuy.class).collectList().block(); + + assertThat(searchHits.size()).isEqualTo(1); + var searchHit = searchHits.get(0); + assertThat(searchHit.getId()).isEqualTo("1"); + assertThat(searchHit.getContent().getDescription()).isEqualTo("11"); + } + + @Test // #2431 + @DisplayName("should return value from runtime field defined in mapping") + void shouldReturnValueFromRuntimeFieldDefinedInMapping() { + + var person = new Person(); + var years = 10; + var birthDate = LocalDate.now().minusDays(years * 365 + 100); + person.setBirthDate(birthDate); + operations.save(person).block(); + var query = Query.findAll(); + query.addFields("age"); + query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build()); + + var searchHits = operations.search(query, Person.class).collectList().block(); + + assertThat(searchHits.size()).isEqualTo(1); + var foundPerson = searchHits.get(0).getContent(); + assertThat(foundPerson.getAge()).isEqualTo(years); + assertThat(foundPerson.getBirthDate()).isEqualTo(birthDate); + } + + @Test // #2035 + @DisplayName("should use repository method with ScriptedField parameters") + void shouldUseRepositoryMethodWithScriptedFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity).block(); + + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1", + 2); + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2", + 3); + + var searchHits = repository.findByValue(3, scriptedField1, scriptedField2).collectList().block(); + + assertThat(searchHits.size()).isEqualTo(1); + var foundEntity = searchHits.get(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(6); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(9); + } + + @NotNull + private static org.springframework.data.elasticsearch.core.query.ScriptedField getScriptedField(String fieldName, + int factor) { + return org.springframework.data.elasticsearch.core.query.ScriptedField.of( + fieldName, + ScriptData.of(b -> b + .withType(ScriptType.INLINE) + .withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0") + .withParams(Map.of("factor", factor)))); + } + + @Test // #2035 + @DisplayName("should use repository string query method with ScriptedField parameters") + void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity).block(); + + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1", + 2); + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2", + 3); + + var searchHits = repository.findWithScriptedFields(3, scriptedField1, scriptedField2).collectList().block(); + + assertThat(searchHits.size()).isEqualTo(1); + var foundEntity = searchHits.get(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(6); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(9); + } + + @Test // #2035 + @DisplayName("should use repository method with RuntimeField parameters") + void shouldUseRepositoryMethodWithRuntimeFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity).block(); + + var runtimeField1 = getRuntimeField("scriptedValue1", 3); + var runtimeField2 = getRuntimeField("scriptedValue2", 4); + + var searchHits = repository.findByValue(3, runtimeField1, runtimeField2).collectList().block(); + + assertThat(searchHits.size()).isEqualTo(1); + var foundEntity = searchHits.get(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(9); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(12); + } + + @NotNull + private static RuntimeField getRuntimeField(String fieldName, int factor) { + return new RuntimeField( + fieldName, + "long", + String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor)); + } + + @Test // #2035 + @DisplayName("should use repository string query method with RuntimeField parameters") + void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity).block(); + + var runtimeField1 = getRuntimeField("scriptedValue1", 3); + var runtimeField2 = getRuntimeField("scriptedValue2", 4); + + var searchHits = repository.findWithRuntimeFields(3, runtimeField1, runtimeField2).collectList().block(); + + assertThat(searchHits.size()).isEqualTo(1); + var foundEntity = searchHits.get(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(9); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(12); + } + + private void insert(String id, String description, double price) { + SomethingToBuy entity = new SomethingToBuy(); + entity.setId(id); + entity.setDescription(description); + entity.setPrice(price); + operations.save(entity).block(); + } + + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-something-to-by") + private static class SomethingToBuy { + private @Id @Nullable String id; + + @Nullable + @Field(type = FieldType.Text) private String description; + + @Nullable + @Field(type = FieldType.Double) private Double price; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getDescription() { + return description; + } + + public void setDescription(@Nullable String description) { + this.description = description; + } + + @Nullable + public Double getPrice() { + return price; + } + + public void setPrice(@Nullable Double price) { + this.price = price; + } + } + + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-person") + @Mapping(runtimeFieldsPath = "/runtime-fields-person.json") + public static class Person { + @Nullable private String id; + + @Field(type = FieldType.Date, format = DateFormat.basic_date) + @Nullable private LocalDate birthDate; + + @ReadOnlyProperty // do not write to prevent ES from automapping + @Nullable private Integer age; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(@Nullable LocalDate birthDate) { + this.birthDate = birthDate; + } + + @Nullable + public Integer getAge() { + return age; + } + + public void setAge(@Nullable Integer age) { + this.age = age; + } + } + + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-sar") + public static class SAREntity { + @Nullable private String id; + @Field(type = FieldType.Integer) + @Nullable Integer value; + @ScriptedField + @Nullable Integer scriptedValue1; + @ScriptedField + @Nullable Integer scriptedValue2; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public Integer getValue() { + return value; + } + + public void setValue(@Nullable Integer value) { + this.value = value; + } + + @Nullable + public Integer getScriptedValue1() { + return scriptedValue1; + } + + public void setScriptedValue1(@Nullable Integer scriptedValue1) { + this.scriptedValue1 = scriptedValue1; + } + + @Nullable + public Integer getScriptedValue2() { + return scriptedValue2; + } + + public void setScriptedValue2(@Nullable Integer scriptedValue2) { + this.scriptedValue2 = scriptedValue2; + } + } + + @SuppressWarnings("SpringDataRepositoryMethodReturnTypeInspection") + public interface ReactiveSARRepository extends ReactiveElasticsearchRepository { + Flux> findByValue(Integer value, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2); + + @org.springframework.data.elasticsearch.annotations.Query(""" + { + "term": { + "value": { + "value": "?0" + } + } + } + """) + Flux> findWithScriptedFields(Integer value, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2); + + Flux> findByValue(Integer value, RuntimeField runtimeField1, RuntimeField runtimeField2); + + @org.springframework.data.elasticsearch.annotations.Query(""" + { + "term": { + "value": { + "value": "?0" + } + } + } + """) + Flux> findWithRuntimeFields(Integer value, RuntimeField runtimeField1, + RuntimeField runtimeField2); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java similarity index 70% rename from src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsELCIntegrationTests.java rename to src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java index 99d458aea8..b4ebd42416 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.elasticsearch.core; +package org.springframework.data.elasticsearch.core.query.scriptedandruntimefields; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.test.context.ContextConfiguration; @@ -26,15 +27,16 @@ * @author Peter-Josef Meisch * @since 4.4 */ -@ContextConfiguration(classes = { RuntimeFieldsELCIntegrationTests.Config.class }) -public class RuntimeFieldsELCIntegrationTests extends RuntimeFieldsIntegrationTests { +@ContextConfiguration(classes = { ScriptedAndRuntimeFieldsELCIntegrationTests.Config.class }) +public class ScriptedAndRuntimeFieldsELCIntegrationTests extends ScriptedAndRuntimeFieldsIntegrationTests { @Configuration @Import({ ElasticsearchTemplateConfiguration.class }) + @EnableElasticsearchRepositories(considerNestedRepositories = true) static class Config { @Bean IndexNameProvider indexNameProvider() { - return new IndexNameProvider("runtime-fields-rest-template"); + return new IndexNameProvider("scripted-runtime"); } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java new file mode 100644 index 0000000000..dbd9a23ac2 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java @@ -0,0 +1,413 @@ +/* + * Copyright 2021-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.scriptedandruntimefields; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; +import java.util.Map; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.ReadOnlyProperty; +import org.springframework.data.elasticsearch.annotations.DateFormat; +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.annotations.Mapping; +import org.springframework.data.elasticsearch.annotations.ScriptedField; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.RuntimeField; +import org.springframework.data.elasticsearch.core.query.ScriptData; +import org.springframework.data.elasticsearch.core.query.ScriptType; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.lang.Nullable; + +/** + * @author Peter-Josef Meisch + * @author cdalxndr + */ +@SpringIntegrationTest +public abstract class ScriptedAndRuntimeFieldsIntegrationTests { + + @Autowired private ElasticsearchOperations operations; + @Autowired protected IndexNameProvider indexNameProvider; + @Autowired private SARRepository repository; + + @BeforeEach + void setUp() { + + indexNameProvider.increment(); + operations.indexOps(SomethingToBuy.class).createWithMapping(); + operations.indexOps(Person.class).createWithMapping(); + operations.indexOps(SAREntity.class).createWithMapping(); + } + + @Test + @Order(java.lang.Integer.MAX_VALUE) + void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete(); + } + + @Test // #1971 + @DisplayName("should use runtime-field from query in search") + void shouldUseRuntimeFieldFromQueryInSearch() { + + insert("1", "item 1", 13.5); + insert("2", "item 2", 15); + Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5)); + RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)"); + query.addRuntimeField(runtimeField); + + SearchHits searchHits = operations.search(query, SomethingToBuy.class); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var searchHit = searchHits.getSearchHit(0); + assertThat(searchHit.getId()).isEqualTo("2"); + var foundEntity = searchHit.getContent(); + assertThat(foundEntity.getDescription()).isEqualTo("item 2"); + } + + @Test // #2267 + @DisplayName("should use runtime-field without script") + void shouldUseRuntimeFieldWithoutScript() { + + insert("1", "11", 10); + Query query = new CriteriaQuery(new Criteria("description").matches(11.0)); + RuntimeField runtimeField = new RuntimeField("description", "double"); + query.addRuntimeField(runtimeField); + + SearchHits searchHits = operations.search(query, SomethingToBuy.class); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var searchHit = searchHits.getSearchHit(0); + assertThat(searchHit.getId()).isEqualTo("1"); + assertThat(searchHit.getContent().getDescription()).isEqualTo("11"); + } + + @Test // #2431 + @DisplayName("should return value from runtime field defined in mapping") + void shouldReturnValueFromRuntimeFieldDefinedInMapping() { + + var person = new Person(); + var years = 10; + var birthDate = LocalDate.now().minusDays(years * 365 + 100); + person.setBirthDate(birthDate); + operations.save(person); + var query = Query.findAll(); + query.addFields("age"); + query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build()); + + var searchHits = operations.search(query, Person.class); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var foundPerson = searchHits.getSearchHit(0).getContent(); + assertThat(foundPerson.getAge()).isEqualTo(years); + assertThat(foundPerson.getBirthDate()).isEqualTo(birthDate); + } + + @Test // #2035 + @DisplayName("should use repository method with ScriptedField parameters") + void shouldUseRepositoryMethodWithScriptedFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity); + + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1", + 2); + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2", + 3); + + var searchHits = repository.findByValue(3, scriptedField1, scriptedField2); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var foundEntity = searchHits.getSearchHit(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(6); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(9); + } + + @NotNull + private static org.springframework.data.elasticsearch.core.query.ScriptedField getScriptedField(String fieldName, + int factor) { + return org.springframework.data.elasticsearch.core.query.ScriptedField.of( + fieldName, + ScriptData.of(b -> b + .withType(ScriptType.INLINE) + .withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0") + .withParams(Map.of("factor", factor)))); + } + + @Test // #2035 + @DisplayName("should use repository string query method with ScriptedField parameters") + void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity); + + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1", + 2); + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2", + 3); + + var searchHits = repository.findWithScriptedFields(3, scriptedField1, scriptedField2); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var foundEntity = searchHits.getSearchHit(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(6); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(9); + } + + @Test // #2035 + @DisplayName("should use repository method with RuntimeField parameters") + void shouldUseRepositoryMethodWithRuntimeFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity); + + var runtimeField1 = getRuntimeField("scriptedValue1", 3); + var runtimeField2 = getRuntimeField("scriptedValue2", 4); + + var searchHits = repository.findByValue(3, runtimeField1, runtimeField2); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var foundEntity = searchHits.getSearchHit(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(9); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(12); + } + + @NotNull + private static RuntimeField getRuntimeField(String fieldName, int factor) { + return new RuntimeField( + fieldName, + "long", + String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor)); + } + + @Test // #2035 + @DisplayName("should use repository string query method with RuntimeField parameters") + void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters() { + + var entity = new SAREntity(); + entity.setId("42"); + entity.setValue(3); + + repository.save(entity); + + var runtimeField1 = getRuntimeField("scriptedValue1", 3); + var runtimeField2 = getRuntimeField("scriptedValue2", 4); + + var searchHits = repository.findWithRuntimeFields(3, runtimeField1, runtimeField2); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var foundEntity = searchHits.getSearchHit(0).getContent(); + assertThat(foundEntity.value).isEqualTo(3); + assertThat(foundEntity.getScriptedValue1()).isEqualTo(9); + assertThat(foundEntity.getScriptedValue2()).isEqualTo(12); + } + + private void insert(String id, String description, double price) { + SomethingToBuy entity = new SomethingToBuy(); + entity.setId(id); + entity.setDescription(description); + entity.setPrice(price); + operations.save(entity); + } + + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-something-to-by") + private static class SomethingToBuy { + private @Id @Nullable String id; + + @Nullable + @Field(type = FieldType.Text) private String description; + + @Nullable + @Field(type = FieldType.Double) private Double price; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getDescription() { + return description; + } + + public void setDescription(@Nullable String description) { + this.description = description; + } + + @Nullable + public Double getPrice() { + return price; + } + + public void setPrice(@Nullable Double price) { + this.price = price; + } + } + + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-person") + @Mapping(runtimeFieldsPath = "/runtime-fields-person.json") + public static class Person { + @Nullable private String id; + + @Field(type = FieldType.Date, format = DateFormat.basic_date) + @Nullable private LocalDate birthDate; + + @ReadOnlyProperty // do not write to prevent ES from automapping + @Nullable private Integer age; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(@Nullable LocalDate birthDate) { + this.birthDate = birthDate; + } + + @Nullable + public Integer getAge() { + return age; + } + + public void setAge(@Nullable Integer age) { + this.age = age; + } + } + + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-sar") + public static class SAREntity { + @Nullable private String id; + @Field(type = FieldType.Integer) + @Nullable Integer value; + @ScriptedField + @Nullable Integer scriptedValue1; + @ScriptedField + @Nullable Integer scriptedValue2; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public Integer getValue() { + return value; + } + + public void setValue(@Nullable Integer value) { + this.value = value; + } + + @Nullable + public Integer getScriptedValue1() { + return scriptedValue1; + } + + public void setScriptedValue1(@Nullable Integer scriptedValue1) { + this.scriptedValue1 = scriptedValue1; + } + + @Nullable + public Integer getScriptedValue2() { + return scriptedValue2; + } + + public void setScriptedValue2(@Nullable Integer scriptedValue2) { + this.scriptedValue2 = scriptedValue2; + } + } + + @SuppressWarnings("SpringDataRepositoryMethodReturnTypeInspection") + public interface SARRepository extends ElasticsearchRepository { + SearchHits findByValue(Integer value, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2); + + @org.springframework.data.elasticsearch.annotations.Query(""" + { + "term": { + "value": { + "value": "?0" + } + } + } + """) + SearchHits findWithScriptedFields(Integer value, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1, + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2); + + SearchHits findByValue(Integer value, RuntimeField runtimeField1, RuntimeField runtimeField2); + + @org.springframework.data.elasticsearch.annotations.Query(""" + { + "term": { + "value": { + "value": "?0" + } + } + } + """) + SearchHits findWithRuntimeFields(Integer value, RuntimeField runtimeField1, RuntimeField runtimeField2); + } +}