Scripted and runtime fields improvements.

Original Pull Request #2663
Closes #2035
This commit is contained in:
Peter-Josef Meisch 2023-08-11 16:12:52 +02:00 committed by GitHub
parent 82ae11833a
commit 1fb034a9a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1404 additions and 299 deletions

View File

@ -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]] [[elasticsearch-migration-guide-5.1-5.2.breaking-changes]]
== Breaking Changes == Breaking Changes
=== Bulk failures
In the `org.springframework.data.elasticsearch.BulkFailureException` class, the return type of the `getFailedDocuments` is changed from `Map<String, String>` In the `org.springframework.data.elasticsearch.BulkFailureException` class, the return type of the `getFailedDocuments` is changed from `Map<String, String>`
to `Map<String, FailureDetails>`, which allows to get additional details about failure reasons. to `Map<String, FailureDetails>`, 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) { 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]] [[elasticsearch-migration-guide-5.1-5.2.deprecations]]
== Deprecations == Deprecations

View File

@ -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<Person, String> {
SearchHits<Person> findAllBy(ScriptedField scriptedField);
SearchHits<Person> 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<Person> 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<Person> 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<Person> 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.

View File

@ -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. 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
<<repositories.custom-implementations>>) as <<repositories.custom-implementations>>) as
an example how this can be integrated into a repository call. an example how this can be integrated into a repository call.
@ -399,7 +399,7 @@ public class PersonCustomRepositoryImpl implements PersonCustomRepository {
var query = SearchTemplateQuery.builder() <.> var query = SearchTemplateQuery.builder() <.>
.withId("person-firstname") <.> .withId("person-firstname") <.>
.withParams( .withParams(
Map.of( <.> Map.of( <.>
"firstName", firstName, "firstName", firstName,
"from", pageable.getOffset(), "from", pageable.getOffset(),
"size", pageable.getPageSize() "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. 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. 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]

View File

@ -3,6 +3,7 @@ package org.springframework.data.elasticsearch.annotations;
import java.lang.annotation.*; 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 * @author Ryan Murfitt
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)

View File

@ -67,7 +67,7 @@ import org.jetbrains.annotations.NotNull;
import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RefreshPolicy; 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.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.index.*; import org.springframework.data.elasticsearch.core.index.*;
@ -1333,10 +1333,8 @@ class RequestConverter {
} }
if (!isEmpty(query.getFields())) { if (!isEmpty(query.getFields())) {
builder.fields(fb -> { var fieldAndFormats = query.getFields().stream().map(field -> FieldAndFormat.of(b -> b.field(field))).toList();
query.getFields().forEach(fb::field); builder.fields(fieldAndFormats);
return fb;
});
} }
if (!isEmpty(query.getStoredFields())) { if (!isEmpty(query.getStoredFields())) {

View File

@ -29,7 +29,6 @@ import java.util.stream.Collectors;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RuntimeField;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -548,6 +547,16 @@ public class BaseQuery implements Query {
this.docValueFields = 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 @Override
public List<ScriptedField> getScriptedFields() { public List<ScriptedField> getScriptedFields() {
return scriptedFields; return scriptedFields;

View File

@ -25,7 +25,6 @@ import java.util.List;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RuntimeField;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;

View File

@ -15,7 +15,10 @@
*/ */
package org.springframework.data.elasticsearch.core.query; package org.springframework.data.elasticsearch.core.query;
import java.util.function.Function;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/** /**
* SourceFilter implementation for providing includes and excludes. * 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[] includes;
@Nullable private final String[] excludes; @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<FetchSourceFilterBuilder, FetchSourceFilterBuilder> 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) { public FetchSourceFilter(@Nullable final String[] includes, @Nullable final String[] excludes) {
this.includes = includes; this.includes = includes;
this.excludes = excludes; this.excludes = excludes;

View File

@ -25,7 +25,6 @@ import java.util.Optional;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RuntimeField;
import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.data.elasticsearch.core; package org.springframework.data.elasticsearch.core.query;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;

View File

@ -16,9 +16,10 @@
package org.springframework.data.elasticsearch.core.query; package org.springframework.data.elasticsearch.core.query;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import org.springframework.data.elasticsearch.core.ScriptType;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/** /**
* value class combining script information. * value class combining script information.
@ -26,6 +27,88 @@ import org.springframework.lang.Nullable;
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @since 4.4 * @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<String, Object> params) { @Nullable String scriptName, @Nullable Map<String, Object> params) {
public ScriptData(ScriptType type, @Nullable String language, @Nullable String script, @Nullable String scriptName,
@Nullable Map<String, Object> 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<String, Object> params) {
return new ScriptData(type, language, script, scriptName, params);
}
public static ScriptData of(Function<Builder, Builder> 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<String, Object> 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<String, Object> params) {
this.params = params;
return this;
}
public ScriptData build() {
Assert.notNull(type, "type must be set");
return new ScriptData(type, language, script, scriptName, params);
}
}
} }

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.data.elasticsearch.core; package org.springframework.data.elasticsearch.core.query;
/** /**
* Define script types for update queries. * Define script types for update queries.

View File

@ -18,6 +18,8 @@ package org.springframework.data.elasticsearch.core.query;
import org.springframework.util.Assert; 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 * @author Peter-Josef Meisch
* @since 4.4 * @since 4.4
*/ */
@ -26,6 +28,13 @@ public class ScriptedField {
private final String fieldName; private final String fieldName;
private final ScriptData scriptData; 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) { public ScriptedField(String fieldName, ScriptData scriptData) {
Assert.notNull(fieldName, "fieldName must not be null"); Assert.notNull(fieldName, "fieldName must not be null");

View File

@ -19,7 +19,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.data.elasticsearch.core.RefreshPolicy; 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.data.elasticsearch.core.document.Document;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;

View File

@ -25,6 +25,7 @@ import org.springframework.data.elasticsearch.core.SearchHitsImpl;
import org.springframework.data.elasticsearch.core.TotalHitsRelation; import org.springframework.data.elasticsearch.core.TotalHitsRelation;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethod;
@ -75,7 +76,7 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
@Override @Override
public Object execute(Object[] parameters) { public Object execute(Object[] parameters) {
ParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); ElasticsearchParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters);
ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
Class<?> clazz = resultProcessor.getReturnedType().getDomainType(); Class<?> clazz = resultProcessor.getReturnedType().getDomainType();
@ -135,29 +136,19 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
public Query createQuery(Object[] parameters) { public Query createQuery(Object[] parameters) {
ParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); ElasticsearchParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters);
ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
Class<?> returnedType = resultProcessor.getReturnedType().getDomainType();
Query query = createQuery(parameterAccessor);
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query"); Assert.notNull(query, "unsupported query");
if (queryMethod.hasAnnotatedHighlight()) { queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter());
query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery());
}
var sourceFilter = queryMethod.getSourceFilter(parameterAccessor,
elasticsearchOperations.getElasticsearchConverter());
if (sourceFilter != null) {
query.addSourceFilter(sourceFilter);
}
return query; return query;
} }
private ParametersParameterAccessor getParameterAccessor(Object[] parameters) { private ElasticsearchParametersParameterAccessor getParameterAccessor(Object[] parameters) {
return new ParametersParameterAccessor(queryMethod.getParameters(), parameters); return new ElasticsearchParametersParameterAccessor(queryMethod, parameters);
} }
@Nullable @Nullable
@ -185,5 +176,5 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
return result; return result;
} }
protected abstract Query createQuery(ParametersParameterAccessor accessor); protected abstract BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor);
} }

View File

@ -26,6 +26,7 @@ import org.springframework.data.elasticsearch.core.SearchHitSupport;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; 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.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingConverter; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingConverter;
@ -35,6 +36,7 @@ import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.util.Assert;
/** /**
* AbstractElasticsearchRepositoryQuery * AbstractElasticsearchRepositoryQuery
@ -79,7 +81,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
return Mono.defer(() -> (Mono<Object>) execute(parameterAccessor)); return Mono.defer(() -> (Mono<Object>) execute(parameterAccessor));
} }
private Object execute(ElasticsearchParameterAccessor parameterAccessor) { private Object execute(ElasticsearchParametersParameterAccessor parameterAccessor) {
ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
var returnedType = processor.getReturnedType(); var returnedType = processor.getReturnedType();
@ -90,17 +92,10 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
typeToRead = queryMethod.unwrappedReturnType; typeToRead = queryMethod.unwrappedReturnType;
} }
Query query = createQuery(parameterAccessor); var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
if (queryMethod.hasAnnotatedHighlight()) { queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter());
query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery());
}
var sourceFilter = queryMethod.getSourceFilter(parameterAccessor,
elasticsearchOperations.getElasticsearchConverter());
if (sourceFilter != null) {
query.addSourceFilter(sourceFilter);
}
String indexName = queryMethod.getEntityInformation().getIndexName(); String indexName = queryMethod.getEntityInformation().getIndexName();
IndexCoordinates index = IndexCoordinates.of(indexName); IndexCoordinates index = IndexCoordinates.of(indexName);
@ -111,18 +106,18 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
return execution.execute(query, domainType, typeToRead, index); return execution.execute(query, domainType, typeToRead, index);
} }
private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor,
Converter<Object, Object> resultProcessing) {
return new ResultProcessingExecution(getExecutionToWrap(accessor, elasticsearchOperations), resultProcessing);
}
/** /**
* Creates a {@link Query} instance using the given {@link ParameterAccessor} * Creates a {@link Query} instance using the given {@link ParameterAccessor}
* *
* @param accessor must not be {@literal null}. * @param accessor must not be {@literal null}.
* @return * @return
*/ */
protected abstract Query createQuery(ElasticsearchParameterAccessor accessor); protected abstract BaseQuery createQuery(ElasticsearchParameterAccessor accessor);
private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor,
Converter<Object, Object> resultProcessing) {
return new ResultProcessingExecution(getExecutionToWrap(accessor, elasticsearchOperations), resultProcessing);
}
private ReactiveElasticsearchQueryExecution getExecutionToWrap(ElasticsearchParameterAccessor accessor, private ReactiveElasticsearchQueryExecution getExecutionToWrap(ElasticsearchParameterAccessor accessor,
ReactiveElasticsearchOperations operations) { ReactiveElasticsearchOperations operations) {

View File

@ -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());
}
}

View File

@ -16,13 +16,12 @@
package org.springframework.data.elasticsearch.repository.query; package org.springframework.data.elasticsearch.repository.query;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.core.MethodParameter; 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.repository.query.Parameters;
import org.springframework.data.util.TypeInformation;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
@ -31,39 +30,47 @@ import org.springframework.data.repository.query.Parameters;
*/ */
public class ElasticsearchParameters extends Parameters<ElasticsearchParameters, ElasticsearchParameter> { public class ElasticsearchParameters extends Parameters<ElasticsearchParameters, ElasticsearchParameter> {
public ElasticsearchParameters(Method method) { private final List<ElasticsearchParameter> scriptedFields = new ArrayList<>();
super(method); private final List<ElasticsearchParameter> 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 ElasticsearchParameter parameterFactory(MethodParameter methodParameter, TypeInformation<?> domainType) {
return new ElasticsearchParameter(methodParameter, domainType);
} }
private ElasticsearchParameters(List<ElasticsearchParameter> parameters) { private ElasticsearchParameters(List<ElasticsearchParameter> parameters) {
super(parameters); super(parameters);
} }
@Override
protected ElasticsearchParameter createParameter(MethodParameter parameter) {
return new ElasticsearchParameter(parameter);
}
@Override @Override
protected ElasticsearchParameters createFrom(List<ElasticsearchParameter> parameters) { protected ElasticsearchParameters createFrom(List<ElasticsearchParameter> parameters) {
return new ElasticsearchParameters(parameters); return new ElasticsearchParameters(parameters);
} }
/** List<ElasticsearchParameter> getScriptedFields() {
* Custom {@link Parameter} implementation adding parameters of type {@link Distance} to the special ones. return scriptedFields;
* }
* @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<ElasticsearchParameter> getRuntimeFields() {
return runtimeFields;
} }
} }

View File

@ -15,9 +15,6 @@
*/ */
package org.springframework.data.elasticsearch.repository.query; package org.springframework.data.elasticsearch.repository.query;
import java.util.Arrays;
import java.util.List;
import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor;
/** /**
@ -27,7 +24,7 @@ import org.springframework.data.repository.query.ParametersParameterAccessor;
class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
implements ElasticsearchParameterAccessor { implements ElasticsearchParameterAccessor {
private final List<Object> values; private final Object[] values;
/** /**
* Creates a new {@link ElasticsearchParametersParameterAccessor}. * Creates a new {@link ElasticsearchParametersParameterAccessor}.
@ -38,11 +35,11 @@ class ElasticsearchParametersParameterAccessor extends ParametersParameterAccess
ElasticsearchParametersParameterAccessor(ElasticsearchQueryMethod method, Object... values) { ElasticsearchParametersParameterAccessor(ElasticsearchQueryMethod method, Object... values) {
super(method.getParameters(), values); super(method.getParameters(), values);
this.values = Arrays.asList(values); this.values = values;
} }
@Override @Override
public Object[] getValues() { public Object[] getValues() {
return values.toArray(); return values;
} }
} }

View File

@ -19,9 +19,10 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.Query; 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.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree;
/** /**
@ -60,12 +61,11 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery
return tree.isExistsProjection(); return tree.isExistsProjection();
} }
protected Query createQuery(ParametersParameterAccessor accessor) { protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery(); BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery();
if (tree.isLimiting()) { if (tree.getMaxResults() != null) {
// noinspection ConstantConditions
query.setMaxResults(tree.getMaxResults()); query.setMaxResults(tree.getMaxResults());
} }

View File

@ -34,14 +34,19 @@ import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; 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.FetchSourceFilterBuilder;
import org.springframework.data.elasticsearch.core.query.HighlightQuery; 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.core.query.SourceFilter;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.ParameterAccessor; 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.query.QueryMethod;
import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters; 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 // base class uses them in order to use our variables
protected final Method method; protected final Method method;
protected final Class<?> unwrappedReturnType; protected final Class<?> unwrappedReturnType;
private Boolean unwrappedReturnTypeFromSearchHit = null; @Nullable private Boolean unwrappedReturnTypeFromSearchHit = null;
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext; private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
@Nullable private ElasticsearchEntityMetadata<?> metadata; @Nullable private ElasticsearchEntityMetadata<?> metadata;
@ -97,6 +102,11 @@ public class ElasticsearchQueryMethod extends QueryMethod {
verifyCountQueryTypes(); verifyCountQueryTypes();
} }
@Override
protected Parameters<?, ?> createParameters(Method method, TypeInformation<?> domainType) {
return new ElasticsearchParameters(method, domainType);
}
protected void verifyCountQueryTypes() { protected void verifyCountQueryTypes() {
if (hasCountQueryAnnotation()) { if (hasCountQueryAnnotation()) {
@ -363,6 +373,49 @@ public class ElasticsearchQueryMethod extends QueryMethod {
} }
} }
} }
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 // endregion
} }

View File

@ -16,10 +16,10 @@
package org.springframework.data.elasticsearch.repository.query; package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations; 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.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -57,7 +57,7 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue
return false; return false;
} }
protected Query createQuery(ParametersParameterAccessor parameterAccessor) { protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService()) String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService())
.replacePlaceholders(this.queryString, parameterAccessor); .replacePlaceholders(this.queryString, parameterAccessor);

View File

@ -81,7 +81,7 @@ class ReactiveElasticsearchParametersParameterAccessor extends ElasticsearchPara
@Override @Override
public Object[] getValues() { public Object[] getValues() {
Object[] result = new Object[getValues().length]; Object[] result = new Object[super.getValues().length];
for (int i = 0; i < result.length; i++) { for (int i = 0; i < result.length; i++) {
result[i] = getValue(i); result[i] = getValue(i);
} }

View File

@ -30,7 +30,6 @@ import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; 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.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
@ -103,8 +102,8 @@ public class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
} }
@Override @Override
protected ElasticsearchParameters createParameters(Method method) { protected ElasticsearchParameters createParameters(Method method, TypeInformation<?> domainType) {
return new ElasticsearchParameters(method); return new ElasticsearchParameters(method, domainType);
} }
/** /**

View File

@ -16,8 +16,8 @@
package org.springframework.data.elasticsearch.repository.query; package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; 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.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator; import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree;
@ -40,7 +40,7 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
} }
@Override @Override
protected Query createQuery(ElasticsearchParameterAccessor accessor) { protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) {
CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery(); CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery();
if (tree.isLimiting()) { if (tree.isLimiting()) {

View File

@ -45,6 +45,7 @@ import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RescorerQuery; import org.springframework.data.elasticsearch.core.query.RescorerQuery;
import org.springframework.data.elasticsearch.core.query.ScriptData; 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.ScriptedField;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.elasticsearch.utils.IndexNameProvider;

View File

@ -1630,7 +1630,7 @@ public abstract class ElasticsearchIntegrationTests {
final Query query = operations.matchAllQuery(); final Query query = operations.matchAllQuery();
final UpdateQuery updateQuery = UpdateQuery.builder(query) 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") .withScript("ctx._source['message'] = params['newMessage']").withLang("painless")
.withParams(Collections.singletonMap("newMessage", messageAfterUpdate)).withAbortOnVersionConflict(true) .withParams(Collections.singletonMap("newMessage", messageAfterUpdate)).withAbortOnVersionConflict(true)
.build(); .build();

View File

@ -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<SomethingToBuy> 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<SomethingToBuy> 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;
}
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.data.elasticsearch.core; package org.springframework.data.elasticsearch.core.query;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;

View File

@ -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");
}
}
}

View File

@ -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<SearchHit<SomethingToBuy>> 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<SearchHit<SomethingToBuy>> 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<SAREntity, String> {
Flux<SearchHit<SAREntity>> 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<SearchHit<SAREntity>> findWithScriptedFields(Integer value,
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1,
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2);
Flux<SearchHit<SAREntity>> findByValue(Integer value, RuntimeField runtimeField1, RuntimeField runtimeField2);
@org.springframework.data.elasticsearch.annotations.Query("""
{
"term": {
"value": {
"value": "?0"
}
}
}
""")
Flux<SearchHit<SAREntity>> findWithRuntimeFields(Integer value, RuntimeField runtimeField1,
RuntimeField runtimeField2);
}
}

View File

@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; 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.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
@ -26,15 +27,16 @@ import org.springframework.test.context.ContextConfiguration;
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @since 4.4 * @since 4.4
*/ */
@ContextConfiguration(classes = { RuntimeFieldsELCIntegrationTests.Config.class }) @ContextConfiguration(classes = { ScriptedAndRuntimeFieldsELCIntegrationTests.Config.class })
public class RuntimeFieldsELCIntegrationTests extends RuntimeFieldsIntegrationTests { public class ScriptedAndRuntimeFieldsELCIntegrationTests extends ScriptedAndRuntimeFieldsIntegrationTests {
@Configuration @Configuration
@Import({ ElasticsearchTemplateConfiguration.class }) @Import({ ElasticsearchTemplateConfiguration.class })
@EnableElasticsearchRepositories(considerNestedRepositories = true)
static class Config { static class Config {
@Bean @Bean
IndexNameProvider indexNameProvider() { IndexNameProvider indexNameProvider() {
return new IndexNameProvider("runtime-fields-rest-template"); return new IndexNameProvider("scripted-runtime");
} }
} }
} }

View File

@ -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<SomethingToBuy> 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<SomethingToBuy> 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<SAREntity, String> {
SearchHits<SAREntity> 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<SAREntity> findWithScriptedFields(Integer value,
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1,
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2);
SearchHits<SAREntity> findByValue(Integer value, RuntimeField runtimeField1, RuntimeField runtimeField2);
@org.springframework.data.elasticsearch.annotations.Query("""
{
"term": {
"value": {
"value": "?0"
}
}
}
""")
SearchHits<SAREntity> findWithRuntimeFields(Integer value, RuntimeField runtimeField1, RuntimeField runtimeField2);
}
}