Add SpEL support for highlight query and source filter.

Original Pull Request #2853
Closes #2852
This commit is contained in:
puppylpg 2024-02-27 01:59:47 +08:00 committed by GitHub
parent 96185f94ef
commit 6af099ea34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 445 additions and 84 deletions

View File

@ -30,12 +30,12 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* A representation of a Elasticsearch document as extended {@link StringObjectMap Map}. All iterators preserve original * A representation of an Elasticsearch document as extended {@link StringObjectMap Map}. All iterators preserve original
* insertion order. * insertion order.
* <p> * <p>
* Document does not allow {@code null} keys. It allows {@literal null} values. * Document does not allow {@code null} keys. It allows {@literal null} values.
* <p> * <p>
* Implementing classes can bei either mutable or immutable. In case a subclass is immutable, its methods may throw * Implementing classes can be either mutable or immutable. In case a subclass is immutable, its methods may throw
* {@link UnsupportedOperationException} when calling modifying methods. * {@link UnsupportedOperationException} when calling modifying methods.
* *
* @author Mark Paluch * @author Mark Paluch

View File

@ -25,6 +25,7 @@ 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;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
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.data.util.StreamUtils; import org.springframework.data.util.StreamUtils;
@ -47,12 +48,15 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
protected ElasticsearchQueryMethod queryMethod; protected ElasticsearchQueryMethod queryMethod;
protected final ElasticsearchOperations elasticsearchOperations; protected final ElasticsearchOperations elasticsearchOperations;
protected final ElasticsearchConverter elasticsearchConverter; protected final ElasticsearchConverter elasticsearchConverter;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod, public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod,
ElasticsearchOperations elasticsearchOperations) { ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.queryMethod = queryMethod; this.queryMethod = queryMethod;
this.elasticsearchOperations = elasticsearchOperations; this.elasticsearchOperations = elasticsearchOperations;
this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter(); this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter();
this.evaluationContextProvider = evaluationContextProvider;
} }
@Override @Override
@ -128,7 +132,8 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
var query = createQuery(parameterAccessor); var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query"); Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter()); queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
return query; return query;
} }

View File

@ -34,6 +34,7 @@ import org.springframework.data.elasticsearch.repository.query.ReactiveElasticse
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.ParameterAccessor; 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.QueryMethodEvaluationContextProvider;
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; import org.springframework.util.Assert;
@ -50,12 +51,15 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
protected final ReactiveElasticsearchQueryMethod queryMethod; protected final ReactiveElasticsearchQueryMethod queryMethod;
private final ReactiveElasticsearchOperations elasticsearchOperations; private final ReactiveElasticsearchOperations elasticsearchOperations;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod, AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations) { ReactiveElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.queryMethod = queryMethod; this.queryMethod = queryMethod;
this.elasticsearchOperations = elasticsearchOperations; this.elasticsearchOperations = elasticsearchOperations;
this.evaluationContextProvider = evaluationContextProvider;
} }
/* /*
@ -96,7 +100,8 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
var query = createQuery(parameterAccessor); var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query"); Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter()); queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
String indexName = queryMethod.getEntityInformation().getIndexName(); String indexName = queryMethod.getEntityInformation().getIndexName();
IndexCoordinates index = IndexCoordinates.of(indexName); IndexCoordinates index = IndexCoordinates.of(indexName);

View File

@ -18,11 +18,9 @@ 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.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.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.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree;
/** /**
@ -34,14 +32,16 @@ import org.springframework.data.repository.query.parser.PartTree;
* @author Mark Paluch * @author Mark Paluch
* @author Rasmus Faber-Espensen * @author Rasmus Faber-Espensen
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
*/ */
public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery { public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery {
private final PartTree tree; private final PartTree tree;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext; private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) { public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
super(method, elasticsearchOperations); QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, elasticsearchOperations, evaluationContextProvider);
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType()); this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
this.mappingContext = elasticsearchConverter.getMappingContext(); this.mappingContext = elasticsearchConverter.getMappingContext();
} }

View File

@ -24,6 +24,7 @@ import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.Query; import org.springframework.data.elasticsearch.annotations.Query;
@ -41,13 +42,13 @@ import org.springframework.data.elasticsearch.core.query.HighlightQuery;
import org.springframework.data.elasticsearch.core.query.RuntimeField; import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptedField; 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.QueryStringProcessor;
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.Parameters; 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.query.QueryMethodEvaluationContextProvider;
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;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
@ -146,9 +147,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
Assert.isTrue(hasAnnotatedHighlight(), "no Highlight annotation present on " + getName()); Assert.isTrue(hasAnnotatedHighlight(), "no Highlight annotation present on " + getName());
Assert.notNull(highlightAnnotation, "highlightAnnotation must not be null"); Assert.notNull(highlightAnnotation, "highlightAnnotation must not be null");
return new HighlightQuery( return new HighlightQuery(highlightConverter.convert(highlightAnnotation), getDomainClass());
highlightConverter.convert(highlightAnnotation),
getDomainClass());
} }
/** /**
@ -288,42 +287,46 @@ public class ElasticsearchQueryMethod extends QueryMethod {
* @param parameterAccessor the accessor with the query method parameter details * @param parameterAccessor the accessor with the query method parameter details
* @param converter {@link ElasticsearchConverter} needed to convert entity property names to the Elasticsearch field * @param converter {@link ElasticsearchConverter} needed to convert entity property names to the Elasticsearch field
* names and for parameter conversion when the includes or excludes are defined as parameters * names and for parameter conversion when the includes or excludes are defined as parameters
* @param evaluationContextProvider to provide an evaluation context for SpEL evaluation
* @return source filter with includes and excludes for a query, {@literal null} when no {@link SourceFilters} * @return source filter with includes and excludes for a query, {@literal null} when no {@link SourceFilters}
* annotation was set on the method. * annotation was set on the method.
* @since 5.0 * @since 5.0
*/ */
@Nullable @Nullable
SourceFilter getSourceFilter(ParameterAccessor parameterAccessor, ElasticsearchConverter converter) { SourceFilter getSourceFilter(ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (sourceFilters == null || (sourceFilters.includes().length == 0 && sourceFilters.excludes().length == 0)) { if (sourceFilters == null || (sourceFilters.includes().length == 0 && sourceFilters.excludes().length == 0)) {
return null; return null;
} }
StringQueryUtil stringQueryUtil = new StringQueryUtil(converter.getConversionService()); ConversionService conversionService = converter.getConversionService();
FetchSourceFilterBuilder fetchSourceFilterBuilder = new FetchSourceFilterBuilder(); FetchSourceFilterBuilder fetchSourceFilterBuilder = new FetchSourceFilterBuilder();
if (sourceFilters.includes().length > 0) { if (sourceFilters.includes().length > 0) {
fetchSourceFilterBuilder fetchSourceFilterBuilder.withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor,
.withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor, stringQueryUtil)); conversionService, evaluationContextProvider));
} }
if (sourceFilters.excludes().length > 0) { if (sourceFilters.excludes().length > 0) {
fetchSourceFilterBuilder fetchSourceFilterBuilder.withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor,
.withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor, stringQueryUtil)); conversionService, evaluationContextProvider));
} }
return fetchSourceFilterBuilder.build(); return fetchSourceFilterBuilder.build();
} }
private String[] mapParameters(String[] source, ParameterAccessor parameterAccessor, private String[] mapParameters(String[] source, ElasticsearchParametersParameterAccessor parameterAccessor,
StringQueryUtil stringQueryUtil) { ConversionService conversionService, QueryMethodEvaluationContextProvider evaluationContextProvider) {
List<String> fieldNames = new ArrayList<>(); List<String> fieldNames = new ArrayList<>();
for (String s : source) { for (String s : source) {
if (!s.isBlank()) { if (!s.isBlank()) {
String fieldName = stringQueryUtil.replacePlaceholders(s, parameterAccessor); String fieldName = new QueryStringProcessor(s, this, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
// this could be "[\"foo\",\"bar\"]", must be split // this could be "[\"foo\",\"bar\"]", must be split
if (fieldName.startsWith("[") && fieldName.endsWith("]")) { if (fieldName.startsWith("[") && fieldName.endsWith("]")) {
// noinspection RegExpRedundantEscape // noinspection RegExpRedundantEscape
@ -367,14 +370,16 @@ public class ElasticsearchQueryMethod extends QueryMethod {
} }
void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor, void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter) { ElasticsearchConverter elasticsearchConverter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (hasAnnotatedHighlight()) { if (hasAnnotatedHighlight()) {
query.setHighlightQuery( var highlightQuery = getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor,
getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor, elasticsearchConverter))); elasticsearchConverter.getConversionService(), evaluationContextProvider, this));
query.setHighlightQuery(highlightQuery);
} }
var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter); var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter, evaluationContextProvider);
if (sourceFilter != null) { if (sourceFilter != null) {
query.addSourceFilter(sourceFilter); query.addSourceFilter(sourceFilter);
} }

View File

@ -19,8 +19,7 @@ import org.springframework.core.convert.ConversionService;
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.BaseQuery;
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.QueryStringProcessor;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -37,17 +36,15 @@ import org.springframework.util.Assert;
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery { public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
private final String queryString; private final String queryString;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations, public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) { String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations); super(queryMethod, elasticsearchOperations, evaluationContextProvider);
Assert.notNull(queryString, "Query cannot be empty"); Assert.notNull(queryString, "Query cannot be empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null"); Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
this.queryString = queryString; this.queryString = queryString;
this.evaluationContextProvider = evaluationContextProvider;
} }
@Override @Override
@ -66,15 +63,12 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue
} }
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService(); ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService();
var replacedString = new StringQueryUtil(conversionService).replacePlaceholders(this.queryString, var processed = new QueryStringProcessor(queryString, queryMethod, conversionService, evaluationContextProvider)
parameterAccessor); .createQuery(parameterAccessor);
var evaluator = new QueryStringSpELEvaluator(replacedString, parameterAccessor, queryMethod,
evaluationContextProvider, conversionService); return new StringQuery(processed)
var query = new StringQuery(evaluator.evaluate()); .addSort(parameterAccessor.getSort());
query.addSort(parameterAccessor.getSort());
return query;
} }
} }

View File

@ -18,13 +18,15 @@ package org.springframework.data.elasticsearch.repository.query;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.core.convert.ConversionService;
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.core.query.highlight.Highlight; import org.springframework.data.elasticsearch.core.query.highlight.Highlight;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightField; import org.springframework.data.elasticsearch.core.query.highlight.HighlightField;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters; import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -35,12 +37,24 @@ import org.springframework.util.Assert;
public class HighlightConverter { public class HighlightConverter {
private final ElasticsearchParametersParameterAccessor parameterAccessor; private final ElasticsearchParametersParameterAccessor parameterAccessor;
private final ElasticsearchConverter elasticsearchConverter; private final ConversionService conversionService;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final QueryMethod queryMethod;
HighlightConverter(ElasticsearchParametersParameterAccessor parameterAccessor, HighlightConverter(ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter) { ConversionService conversionService,
QueryMethodEvaluationContextProvider evaluationContextProvider,
QueryMethod queryMethod) {
Assert.notNull(parameterAccessor, "parameterAccessor must not be null");
Assert.notNull(conversionService, "conversionService must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
Assert.notNull(queryMethod, "queryMethod must not be null");
this.parameterAccessor = parameterAccessor; this.parameterAccessor = parameterAccessor;
this.elasticsearchConverter = elasticsearchConverter; this.conversionService = conversionService;
this.evaluationContextProvider = evaluationContextProvider;
this.queryMethod = queryMethod;
} }
/** /**
@ -58,10 +72,10 @@ public class HighlightConverter {
// replace placeholders in highlight query with actual parameters // replace placeholders in highlight query with actual parameters
Query highlightQuery = null; Query highlightQuery = null;
if (!parameters.highlightQuery().value().isEmpty()) { if (!parameters.highlightQuery().value().isEmpty()) {
String rawString = parameters.highlightQuery().value(); String rawQuery = parameters.highlightQuery().value();
String queryString = new StringQueryUtil(elasticsearchConverter.getConversionService()) String query = new QueryStringProcessor(rawQuery, queryMethod, conversionService, evaluationContextProvider)
.replacePlaceholders(rawString, parameterAccessor); .createQuery(parameterAccessor);
highlightQuery = new StringQuery(queryString); highlightQuery = new StringQuery(query);
} }
HighlightParameters highlightParameters = HighlightParameters.builder() // HighlightParameters highlightParameters = HighlightParameters.builder() //

View File

@ -19,10 +19,8 @@ import org.springframework.core.convert.ConversionService;
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.BaseQuery;
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.QueryStringProcessor;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -37,16 +35,14 @@ public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsea
private final QueryMethodEvaluationContextProvider evaluationContextProvider; private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod, public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser, ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider); this(queryMethod.getAnnotatedQuery(), queryMethod, operations, evaluationContextProvider);
} }
public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod, public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser, ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) { super(queryMethod, operations, evaluationContextProvider);
super(queryMethod, operations);
Assert.notNull(query, "query must not be null"); Assert.notNull(query, "query must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null"); Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
@ -57,15 +53,11 @@ public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsea
@Override @Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(
getElasticsearchOperations().getElasticsearchConverter().getConversionService())
.replacePlaceholders(this.query, parameterAccessor);
ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter() ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService(); .getConversionService();
QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod, String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider)
evaluationContextProvider, conversionService); .createQuery(parameterAccessor);
return new StringQuery(evaluator.evaluate()); return new StringQuery(processed);
} }
@Override @Override

View File

@ -19,12 +19,14 @@ import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperatio
import org.springframework.data.elasticsearch.core.query.BaseQuery; 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.repository.query.parser.ElasticsearchQueryCreator; import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
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;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
* @since 3.2 * @since 3.2
*/ */
public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElasticsearchRepositoryQuery { public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElasticsearchRepositoryQuery {
@ -32,8 +34,9 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
private final PartTree tree; private final PartTree tree;
public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod, public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations) { ReactiveElasticsearchOperations elasticsearchOperations,
super(queryMethod, elasticsearchOperations); QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
ResultProcessor processor = queryMethod.getResultProcessor(); ResultProcessor processor = queryMethod.getResultProcessor();
this.tree = new PartTree(queryMethod.getName(), processor.getReturnedType().getDomainType()); this.tree = new PartTree(queryMethod.getName(), processor.getReturnedType().getDomainType());

View File

@ -128,7 +128,7 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(), return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
evaluationContextProvider); evaluationContextProvider);
} }
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations); return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations, evaluationContextProvider);
} }
} }

View File

@ -26,23 +26,32 @@ import org.springframework.util.Assert;
import org.springframework.util.NumberUtils; import org.springframework.util.NumberUtils;
/** /**
* To replace the placeholders like `?0`, `?1, `?2` of the query string.
*
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Niklas Herder * @author Niklas Herder
* @author Haibo Liu * @author Haibo Liu
*/ */
final public class StringQueryUtil { final public class QueryStringPlaceholderReplacer {
private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)"); private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)");
private final ConversionService conversionService; private final ConversionService conversionService;
public StringQueryUtil(ConversionService conversionService) { public QueryStringPlaceholderReplacer(ConversionService conversionService) {
Assert.notNull(conversionService, "conversionService must not be null"); Assert.notNull(conversionService, "conversionService must not be null");
this.conversionService = ElasticsearchQueryValueConversionService.getInstance(conversionService); this.conversionService = ElasticsearchQueryValueConversionService.getInstance(conversionService);
} }
/**
* Replace the placeholders of the query string.
*
* @param input raw query string
* @param accessor parameter info
* @return a plain string with placeholders replaced
*/
public String replacePlaceholders(String input, ParameterAccessor accessor) { public String replacePlaceholders(String input, ParameterAccessor accessor) {
Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input); Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input);

View File

@ -0,0 +1,67 @@
/*
* Copyright 2024 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.support;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* To process query strings with placeholder replacement and SpEL evaluation by {@link QueryStringPlaceholderReplacer}
* and {@link QueryStringSpELEvaluator}.
*
* @since 5.3
* @author Haibo Liu
*/
public class QueryStringProcessor {
private final String query;
private final QueryMethod queryMethod;
private final ConversionService conversionService;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public QueryStringProcessor(String query, QueryMethod queryMethod, ConversionService conversionService,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(query, "query must not be null");
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(conversionService, "conversionService must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.query = query;
this.queryMethod = queryMethod;
this.conversionService = conversionService;
this.evaluationContextProvider = evaluationContextProvider;
}
/**
* Process the query string with placeholder replacement and SpEL evaluation.
*
* @param parameterAccessor parameter info
* @return processed string
*/
public String createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new QueryStringPlaceholderReplacer(conversionService)
.replacePlaceholders(query, parameterAccessor);
QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider, conversionService);
return evaluator.evaluate();
}
}

View File

@ -39,7 +39,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -50,12 +49,11 @@ import org.springframework.util.Assert;
* @author Christoph Strobl * @author Christoph Strobl
* @author Ivan Greene * @author Ivan Greene
* @author Ezequiel Antúnez Camacho * @author Ezequiel Antúnez Camacho
* @author Haibo Liu
* @since 3.2 * @since 3.2
*/ */
public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport { public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final ReactiveElasticsearchOperations operations; private final ReactiveElasticsearchOperations operations;
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext; private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
@ -163,13 +161,12 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa
if (namedQueries.hasQuery(namedQueryName)) { if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName); String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER, return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations,
evaluationContextProvider); evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) { } else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations, EXPRESSION_PARSER, return new ReactiveElasticsearchStringQuery(queryMethod, operations, evaluationContextProvider);
evaluationContextProvider);
} else { } else {
return new ReactivePartTreeElasticsearchQuery(queryMethod, operations); return new ReactivePartTreeElasticsearchQuery(queryMethod, operations, evaluationContextProvider);
} }
} }
} }

View File

@ -36,6 +36,7 @@ import org.springframework.data.elasticsearch.repository.query.ElasticsearchPart
import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod; import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@ -44,6 +45,7 @@ import org.springframework.lang.Nullable;
* kept package private. * kept package private.
* *
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
*/ */
@SpringIntegrationTest @SpringIntegrationTest
public abstract class ElasticsearchPartQueryIntegrationTests { public abstract class ElasticsearchPartQueryIntegrationTests {
@ -647,7 +649,8 @@ public abstract class ElasticsearchPartQueryIntegrationTests {
ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method, ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), new SpelAwareProxyProjectionFactory(), new DefaultRepositoryMetadata(SampleRepository.class), new SpelAwareProxyProjectionFactory(),
operations.getElasticsearchConverter().getMappingContext()); operations.getElasticsearchConverter().getMappingContext());
ElasticsearchPartQuery partQuery = new ElasticsearchPartQuery(queryMethod, operations); ElasticsearchPartQuery partQuery = new ElasticsearchPartQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT);
Query query = partQuery.createQuery(parameters); Query query = partQuery.createQuery(parameters);
return buildQueryString(query, Book.class); return buildQueryString(query, Book.class);
} }

View File

@ -1740,6 +1740,26 @@ public abstract class CustomMethodRepositoryIntegrationTests {
assertThat(highlightXyzHit.getHighlightField("type")).hasSize(1).contains("abc <em>xyz</em>"); assertThat(highlightXyzHit.getHighlightField("type")).hasSize(1).contains("abc <em>xyz</em>");
} }
@Test
void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethodSpEL() {
List<SampleEntity> entities = createSampleEntities("abc xyz", 2);
repository.saveAll(entities);
// when
SearchHits<SampleEntity> highlightAbcHits = repository.queryByStringWithSeparateHighlightSpEL("abc", "abc");
assertThat(highlightAbcHits.getTotalHits()).isEqualTo(2);
SearchHit<SampleEntity> highlightAbcHit = highlightAbcHits.getSearchHit(0);
assertThat(highlightAbcHit.getHighlightField("type")).hasSize(1).contains("<em>abc</em> xyz");
// when
SearchHits<SampleEntity> highlightXyzHits = repository.queryByStringWithSeparateHighlightSpEL("abc", "xyz");
assertThat(highlightXyzHits.getTotalHits()).isEqualTo(2);
SearchHit<SampleEntity> highlightXyzHit = highlightXyzHits.getSearchHit(0);
assertThat(highlightXyzHit.getHighlightField("type")).hasSize(1).contains("abc <em>xyz</em>");
}
@Test // DATAES-734 @Test // DATAES-734
void shouldUseGeoSortParameter() { void shouldUseGeoSortParameter() {
GeoPoint munich = new GeoPoint(48.137154, 11.5761247); GeoPoint munich = new GeoPoint(48.137154, 11.5761247);
@ -1938,6 +1958,28 @@ public abstract class CustomMethodRepositoryIntegrationTests {
assertThat(foundEntity.getKeyword()).isNull(); assertThat(foundEntity.getKeyword()).isNull();
} }
@Test
@DisplayName("should use sourceIncludes from parameter SpEL")
void shouldUseSourceIncludesFromParameterSpEL() {
SampleEntity entity = new SampleEntity();
entity.setId("42");
entity.setMessage("message");
entity.setCustomFieldNameMessage("customFieldNameMessage");
entity.setType("type");
entity.setKeyword("keyword");
repository.save(entity);
var searchHits = repository.queryBy(List.of("message", "customFieldNameMessage"));
assertThat(searchHits.hasSearchHits()).isTrue();
var foundEntity = searchHits.getSearchHit(0).getContent();
assertThat(foundEntity.getMessage()).isEqualTo("message");
assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage");
assertThat(foundEntity.getType()).isNull();
assertThat(foundEntity.getKeyword()).isNull();
}
@Test // #2146 @Test // #2146
@DisplayName("should use sourceExcludes from annotation") @DisplayName("should use sourceExcludes from annotation")
void shouldUseSourceExcludesFromAnnotation() { void shouldUseSourceExcludesFromAnnotation() {
@ -1982,6 +2024,28 @@ public abstract class CustomMethodRepositoryIntegrationTests {
assertThat(foundEntity.getKeyword()).isNull(); assertThat(foundEntity.getKeyword()).isNull();
} }
@Test
@DisplayName("should use source excludes from parameter SpEL")
void shouldUseSourceExcludesFromParameterSpEL() {
SampleEntity entity = new SampleEntity();
entity.setId("42");
entity.setMessage("message");
entity.setCustomFieldNameMessage("customFieldNameMessage");
entity.setType("type");
entity.setKeyword("keyword");
repository.save(entity);
var searchHits = repository.getBy(List.of("type", "keyword"));
assertThat(searchHits.hasSearchHits()).isTrue();
var foundEntity = searchHits.getSearchHit(0).getContent();
assertThat(foundEntity.getMessage()).isEqualTo("message");
assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage");
assertThat(foundEntity.getType()).isNull();
assertThat(foundEntity.getKeyword()).isNull();
}
private List<SampleEntity> createSampleEntities(String type, int numberOfEntities) { private List<SampleEntity> createSampleEntities(String type, int numberOfEntities) {
List<SampleEntity> entities = new ArrayList<>(); List<SampleEntity> entities = new ArrayList<>();
@ -2276,6 +2340,37 @@ public abstract class CustomMethodRepositoryIntegrationTests {
"""))) """)))
SearchHits<SampleEntity> queryByStringWithSeparateHighlight(String type, String highlight); SearchHits<SampleEntity> queryByStringWithSeparateHighlight(String type, String highlight);
@Query("""
{
"bool":{
"must":[
{
"match":{
"type":"#{#type}"
}
}
]
}
}
""")
@Highlight(
fields = { @HighlightField(name = "type") },
parameters = @HighlightParameters(
highlightQuery = @Query("""
{
"bool":{
"must":[
{
"match":{
"type":"#{#highlight}"
}
}
]
}
}
""")))
SearchHits<SampleEntity> queryByStringWithSeparateHighlightSpEL(String type, String highlight);
List<SearchHit<SampleEntity>> queryByMessage(String message); List<SearchHit<SampleEntity>> queryByMessage(String message);
Stream<SearchHit<SampleEntity>> readByMessage(String message); Stream<SearchHit<SampleEntity>> readByMessage(String message);
@ -2307,6 +2402,9 @@ public abstract class CustomMethodRepositoryIntegrationTests {
@SourceFilters(includes = "?0") @SourceFilters(includes = "?0")
SearchHits<SampleEntity> searchBy(Collection<String> sourceIncludes); SearchHits<SampleEntity> searchBy(Collection<String> sourceIncludes);
@SourceFilters(includes = "#{#sourceIncludes}")
SearchHits<SampleEntity> queryBy(Collection<String> sourceIncludes);
@Query(""" @Query("""
{ {
"match_all": {} "match_all": {}
@ -2317,6 +2415,9 @@ public abstract class CustomMethodRepositoryIntegrationTests {
@SourceFilters(excludes = "?0") @SourceFilters(excludes = "?0")
SearchHits<SampleEntity> findBy(Collection<String> sourceExcludes); SearchHits<SampleEntity> findBy(Collection<String> sourceExcludes);
@SourceFilters(excludes = "#{#sourceExcludes}")
SearchHits<SampleEntity> getBy(Collection<String> sourceExcludes);
} }
public interface SampleStreamingCustomMethodRepository extends ElasticsearchRepository<SampleEntity, String> { public interface SampleStreamingCustomMethodRepository extends ElasticsearchRepository<SampleEntity, String> {

View File

@ -52,7 +52,6 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@ -63,8 +62,6 @@ import org.springframework.lang.Nullable;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase { public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
SpelExpressionParser PARSER = new SpelExpressionParser();
@Mock ReactiveElasticsearchOperations operations; @Mock ReactiveElasticsearchOperations operations;
@BeforeEach @BeforeEach
@ -377,7 +374,7 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
} }
private ReactiveElasticsearchStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) { private ReactiveElasticsearchStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations, PARSER, return new ReactiveElasticsearchStringQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT); QueryMethodEvaluationContextProvider.DEFAULT);
} }

View File

@ -51,6 +51,7 @@ import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.HighlightField; import org.springframework.data.elasticsearch.annotations.HighlightField;
import org.springframework.data.elasticsearch.annotations.HighlightParameters;
import org.springframework.data.elasticsearch.annotations.Query; import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.annotations.SourceFilters; import org.springframework.data.elasticsearch.annotations.SourceFilters;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
@ -436,6 +437,60 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
.verifyComplete(); .verifyComplete();
} }
@Test
void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethod() {
bulkIndex(new SampleEntity("id-one", "abc xyz"), //
new SampleEntity("id-two", "abc xyz"), //
new SampleEntity("id-three", "abc xyz")) //
.block();
repository.queryByMessageWithSeparateHighlight("abc", "abc") //
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> {
List<String> hitHighlightField = searchHit.getHighlightField("message");
return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("<em>abc</em> xyz");
}) //
.expectNextCount(2) //
.verifyComplete();
repository.queryByMessageWithSeparateHighlight("abc", "xyz") //
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> {
List<String> hitHighlightField = searchHit.getHighlightField("message");
return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("abc <em>xyz</em>");
}) //
.expectNextCount(2) //
.verifyComplete();
}
@Test
void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethodSpEL() {
bulkIndex(new SampleEntity("id-one", "abc xyz"), //
new SampleEntity("id-two", "abc xyz"), //
new SampleEntity("id-three", "abc xyz")) //
.block();
repository.queryByMessageWithSeparateHighlightSpEL("abc", "abc") //
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> {
List<String> hitHighlightField = searchHit.getHighlightField("message");
return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("<em>abc</em> xyz");
}) //
.expectNextCount(2) //
.verifyComplete();
repository.queryByMessageWithSeparateHighlightSpEL("abc", "xyz") //
.as(StepVerifier::create) //
.expectNextMatches(searchHit -> {
List<String> hitHighlightField = searchHit.getHighlightField("message");
return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("abc <em>xyz</em>");
}) //
.expectNextCount(2) //
.verifyComplete();
}
@Test // DATAES-519, DATAES-767, DATAES-822 @Test // DATAES-519, DATAES-767, DATAES-822
void countShouldErrorWhenIndexDoesNotExist() { void countShouldErrorWhenIndexDoesNotExist() {
@ -860,6 +915,29 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
.verifyComplete(); .verifyComplete();
} }
@Test
@DisplayName("should use sourceIncludes from parameter SpEL")
void shouldUseSourceIncludesFromParameterSpEL() {
var entity = new SampleEntity();
entity.setId("42");
entity.setMessage("message");
entity.setCustomFieldNameMessage("customFieldNameMessage");
entity.setType("type");
entity.setKeyword("keyword");
repository.save(entity).block();
repository.queryBy(List.of("message", "customFieldNameMessage")) //
.as(StepVerifier::create) //
.consumeNextWith(foundEntity -> { //
assertThat(foundEntity.getMessage()).isEqualTo("message"); //
assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage"); //
assertThat(foundEntity.getType()).isNull(); //
assertThat(foundEntity.getKeyword()).isNull(); //
}) //
.verifyComplete();
}
@Test // #2146 @Test // #2146
@DisplayName("should use sourceExcludes from annotation") @DisplayName("should use sourceExcludes from annotation")
void shouldUseSourceExcludesFromAnnotation() { void shouldUseSourceExcludesFromAnnotation() {
@ -906,6 +984,29 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
.verifyComplete(); .verifyComplete();
} }
@Test
@DisplayName("should use source excludes from parameter SpEL")
void shouldUseSourceExcludesFromParameterSpEL() {
var entity = new SampleEntity();
entity.setId("42");
entity.setMessage("message");
entity.setCustomFieldNameMessage("customFieldNameMessage");
entity.setType("type");
entity.setKeyword("keyword");
repository.save(entity).block();
repository.getBy(List.of("type", "keyword")) //
.as(StepVerifier::create) //
.consumeNextWith(foundEntity -> { //
assertThat(foundEntity.getMessage()).isEqualTo("message"); //
assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage"); //
assertThat(foundEntity.getType()).isNull(); //
assertThat(foundEntity.getKeyword()).isNull(); //
}) //
.verifyComplete();
}
@Test // #2496 @Test // #2496
@DisplayName("should save data from Flux and return saved data in a flux") @DisplayName("should save data from Flux and return saved data in a flux")
void shouldSaveDataFromFluxAndReturnSavedDataInAFlux() { void shouldSaveDataFromFluxAndReturnSavedDataInAFlux() {
@ -947,6 +1048,68 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@Highlight(fields = { @HighlightField(name = "message") }) @Highlight(fields = { @HighlightField(name = "message") })
Flux<SearchHit<SampleEntity>> queryByMessageWithString(String message); Flux<SearchHit<SampleEntity>> queryByMessageWithString(String message);
@Query("""
{
"bool":{
"must":[
{
"match":{
"message":"?0"
}
}
]
}
}
""")
@Highlight(
fields = { @HighlightField(name = "message") },
parameters = @HighlightParameters(
highlightQuery = @Query("""
{
"bool":{
"must":[
{
"match":{
"message":"?1"
}
}
]
}
}
""")))
Flux<SearchHit<SampleEntity>> queryByMessageWithSeparateHighlight(String message, String highlight);
@Query("""
{
"bool":{
"must":[
{
"match":{
"message":"#{#message}"
}
}
]
}
}
""")
@Highlight(
fields = { @HighlightField(name = "message") },
parameters = @HighlightParameters(
highlightQuery = @Query("""
{
"bool":{
"must":[
{
"match":{
"message":"#{#highlight}"
}
}
]
}
}
""")))
Flux<SearchHit<SampleEntity>> queryByMessageWithSeparateHighlightSpEL(String message, String highlight);
@Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }") @Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }")
Flux<SampleEntity> findAllViaAnnotatedQueryByMessageLike(String message); Flux<SampleEntity> findAllViaAnnotatedQueryByMessageLike(String message);
@ -1083,6 +1246,9 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@SourceFilters(includes = "?0") @SourceFilters(includes = "?0")
Flux<SampleEntity> searchBy(Collection<String> sourceIncludes); Flux<SampleEntity> searchBy(Collection<String> sourceIncludes);
@SourceFilters(includes = "#{#sourceIncludes}")
Flux<SampleEntity> queryBy(Collection<String> sourceIncludes);
@Query(""" @Query("""
{ {
"match_all": {} "match_all": {}
@ -1093,6 +1259,9 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@SourceFilters(excludes = "?0") @SourceFilters(excludes = "?0")
Flux<SampleEntity> findBy(Collection<String> sourceExcludes); Flux<SampleEntity> findBy(Collection<String> sourceExcludes);
@SourceFilters(excludes = "#{#sourceExcludes}")
Flux<SampleEntity> getBy(Collection<String> sourceExcludes);
} }
@Document(indexName = "#{@indexNameProvider.indexName()}") @Document(indexName = "#{@indexNameProvider.indexName()}")