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;
/**
* 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.
* <p>
* Document does not allow {@code null} keys. It allows {@literal null} values.
* <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.
*
* @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.repository.query.ParametersParameterAccessor;
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.ResultProcessor;
import org.springframework.data.util.StreamUtils;
@ -47,12 +48,15 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
protected ElasticsearchQueryMethod queryMethod;
protected final ElasticsearchOperations elasticsearchOperations;
protected final ElasticsearchConverter elasticsearchConverter;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod,
ElasticsearchOperations elasticsearchOperations) {
ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.queryMethod = queryMethod;
this.elasticsearchOperations = elasticsearchOperations;
this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter();
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
@ -128,7 +132,8 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter());
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
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.repository.query.ParameterAccessor;
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.ResultProcessor;
import org.springframework.util.Assert;
@ -50,12 +51,15 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
protected final ReactiveElasticsearchQueryMethod queryMethod;
private final ReactiveElasticsearchOperations elasticsearchOperations;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations) {
ReactiveElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.queryMethod = queryMethod;
this.elasticsearchOperations = elasticsearchOperations;
this.evaluationContextProvider = evaluationContextProvider;
}
/*
@ -96,7 +100,8 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter());
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
String indexName = queryMethod.getEntityInformation().getIndexName();
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.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptedField;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.parser.PartTree;
/**
@ -34,14 +32,16 @@ import org.springframework.data.repository.query.parser.PartTree;
* @author Mark Paluch
* @author Rasmus Faber-Espensen
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery {
private final PartTree tree;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) {
super(method, elasticsearchOperations);
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, elasticsearchOperations, evaluationContextProvider);
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
this.mappingContext = elasticsearchConverter.getMappingContext();
}

View File

@ -24,6 +24,7 @@ import java.util.List;
import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.annotations.Highlight;
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.ScriptedField;
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.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
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.notNull(highlightAnnotation, "highlightAnnotation must not be null");
return new HighlightQuery(
highlightConverter.convert(highlightAnnotation),
getDomainClass());
return new HighlightQuery(highlightConverter.convert(highlightAnnotation), getDomainClass());
}
/**
@ -288,42 +287,46 @@ public class ElasticsearchQueryMethod extends QueryMethod {
* @param parameterAccessor the accessor with the query method parameter details
* @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
* @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}
* annotation was set on the method.
* @since 5.0
*/
@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)) {
return null;
}
StringQueryUtil stringQueryUtil = new StringQueryUtil(converter.getConversionService());
ConversionService conversionService = converter.getConversionService();
FetchSourceFilterBuilder fetchSourceFilterBuilder = new FetchSourceFilterBuilder();
if (sourceFilters.includes().length > 0) {
fetchSourceFilterBuilder
.withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor, stringQueryUtil));
fetchSourceFilterBuilder.withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor,
conversionService, evaluationContextProvider));
}
if (sourceFilters.excludes().length > 0) {
fetchSourceFilterBuilder
.withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor, stringQueryUtil));
fetchSourceFilterBuilder.withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor,
conversionService, evaluationContextProvider));
}
return fetchSourceFilterBuilder.build();
}
private String[] mapParameters(String[] source, ParameterAccessor parameterAccessor,
StringQueryUtil stringQueryUtil) {
private String[] mapParameters(String[] source, ElasticsearchParametersParameterAccessor parameterAccessor,
ConversionService conversionService, QueryMethodEvaluationContextProvider evaluationContextProvider) {
List<String> fieldNames = new ArrayList<>();
for (String s : source) {
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
if (fieldName.startsWith("[") && fieldName.endsWith("]")) {
// noinspection RegExpRedundantEscape
@ -367,14 +370,16 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter) {
ElasticsearchConverter elasticsearchConverter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (hasAnnotatedHighlight()) {
query.setHighlightQuery(
getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor, elasticsearchConverter)));
var highlightQuery = getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor,
elasticsearchConverter.getConversionService(), evaluationContextProvider, this));
query.setHighlightQuery(highlightQuery);
}
var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter);
var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter, evaluationContextProvider);
if (sourceFilter != null) {
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.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
@ -37,17 +36,15 @@ import org.springframework.util.Assert;
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
private final String queryString;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations);
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
Assert.notNull(queryString, "Query cannot be empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
this.queryString = queryString;
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
@ -66,15 +63,12 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue
}
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService();
var replacedString = new StringQueryUtil(conversionService).replacePlaceholders(this.queryString,
parameterAccessor);
var evaluator = new QueryStringSpELEvaluator(replacedString, parameterAccessor, queryMethod,
evaluationContextProvider, conversionService);
var query = new StringQuery(evaluator.evaluate());
query.addSort(parameterAccessor.getSort());
return query;
var processed = new QueryStringProcessor(queryString, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed)
.addSort(parameterAccessor.getSort());
}
}

View File

@ -18,13 +18,15 @@ package org.springframework.data.elasticsearch.repository.query;
import java.util.Arrays;
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.StringQuery;
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.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;
/**
@ -35,12 +37,24 @@ import org.springframework.util.Assert;
public class HighlightConverter {
private final ElasticsearchParametersParameterAccessor parameterAccessor;
private final ElasticsearchConverter elasticsearchConverter;
private final ConversionService conversionService;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final QueryMethod queryMethod;
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.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
Query highlightQuery = null;
if (!parameters.highlightQuery().value().isEmpty()) {
String rawString = parameters.highlightQuery().value();
String queryString = new StringQueryUtil(elasticsearchConverter.getConversionService())
.replacePlaceholders(rawString, parameterAccessor);
highlightQuery = new StringQuery(queryString);
String rawQuery = parameters.highlightQuery().value();
String query = new QueryStringProcessor(rawQuery, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
highlightQuery = new StringQuery(query);
}
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.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
@ -37,16 +35,14 @@ public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsea
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider);
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, evaluationContextProvider);
}
public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, operations);
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, operations, evaluationContextProvider);
Assert.notNull(query, "query must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
@ -57,15 +53,11 @@ public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsea
@Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(
getElasticsearchOperations().getElasticsearchConverter().getConversionService())
.replacePlaceholders(this.query, parameterAccessor);
ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService();
QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider, conversionService);
return new StringQuery(evaluator.evaluate());
String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed);
}
@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.CriteriaQuery;
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.parser.PartTree;
/**
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 3.2
*/
public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElasticsearchRepositoryQuery {
@ -32,8 +34,9 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
private final PartTree tree;
public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations) {
super(queryMethod, elasticsearchOperations);
ReactiveElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
ResultProcessor processor = queryMethod.getResultProcessor();
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(),
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;
/**
* To replace the placeholders like `?0`, `?1, `?2` of the query string.
*
* @author Peter-Josef Meisch
* @author Niklas Herder
* @author Haibo Liu
*/
final public class StringQueryUtil {
final public class QueryStringPlaceholderReplacer {
private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)");
private final ConversionService conversionService;
public StringQueryUtil(ConversionService conversionService) {
public QueryStringPlaceholderReplacer(ConversionService conversionService) {
Assert.notNull(conversionService, "conversionService must not be null");
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) {
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.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -50,12 +49,11 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Ivan Greene
* @author Ezequiel Antúnez Camacho
* @author Haibo Liu
* @since 3.2
*/
public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final ReactiveElasticsearchOperations operations;
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
@ -163,13 +161,12 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER,
return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations, EXPRESSION_PARSER,
evaluationContextProvider);
return new ReactiveElasticsearchStringQuery(queryMethod, operations, evaluationContextProvider);
} 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.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
/**
@ -44,6 +45,7 @@ import org.springframework.lang.Nullable;
* kept package private.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
@SpringIntegrationTest
public abstract class ElasticsearchPartQueryIntegrationTests {
@ -647,7 +649,8 @@ public abstract class ElasticsearchPartQueryIntegrationTests {
ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), new SpelAwareProxyProjectionFactory(),
operations.getElasticsearchConverter().getMappingContext());
ElasticsearchPartQuery partQuery = new ElasticsearchPartQuery(queryMethod, operations);
ElasticsearchPartQuery partQuery = new ElasticsearchPartQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT);
Query query = partQuery.createQuery(parameters);
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>");
}
@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
void shouldUseGeoSortParameter() {
GeoPoint munich = new GeoPoint(48.137154, 11.5761247);
@ -1938,6 +1958,28 @@ public abstract class CustomMethodRepositoryIntegrationTests {
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
@DisplayName("should use sourceExcludes from annotation")
void shouldUseSourceExcludesFromAnnotation() {
@ -1982,6 +2024,28 @@ public abstract class CustomMethodRepositoryIntegrationTests {
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) {
List<SampleEntity> entities = new ArrayList<>();
@ -2276,6 +2340,37 @@ public abstract class CustomMethodRepositoryIntegrationTests {
""")))
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);
Stream<SearchHit<SampleEntity>> readByMessage(String message);
@ -2307,6 +2402,9 @@ public abstract class CustomMethodRepositoryIntegrationTests {
@SourceFilters(includes = "?0")
SearchHits<SampleEntity> searchBy(Collection<String> sourceIncludes);
@SourceFilters(includes = "#{#sourceIncludes}")
SearchHits<SampleEntity> queryBy(Collection<String> sourceIncludes);
@Query("""
{
"match_all": {}
@ -2317,6 +2415,9 @@ public abstract class CustomMethodRepositoryIntegrationTests {
@SourceFilters(excludes = "?0")
SearchHits<SampleEntity> findBy(Collection<String> sourceExcludes);
@SourceFilters(excludes = "#{#sourceExcludes}")
SearchHits<SampleEntity> getBy(Collection<String> sourceExcludes);
}
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.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
/**
@ -63,8 +62,6 @@ import org.springframework.lang.Nullable;
@ExtendWith(MockitoExtension.class)
public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
SpelExpressionParser PARSER = new SpelExpressionParser();
@Mock ReactiveElasticsearchOperations operations;
@BeforeEach
@ -377,7 +374,7 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
}
private ReactiveElasticsearchStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations, PARSER,
return new ReactiveElasticsearchStringQuery(queryMethod, operations,
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.Highlight;
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.SourceFilters;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
@ -436,6 +437,60 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
.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
void countShouldErrorWhenIndexDoesNotExist() {
@ -860,6 +915,29 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
.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
@DisplayName("should use sourceExcludes from annotation")
void shouldUseSourceExcludesFromAnnotation() {
@ -906,6 +984,29 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
.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
@DisplayName("should save data from Flux and return saved data in a flux")
void shouldSaveDataFromFluxAndReturnSavedDataInAFlux() {
@ -947,6 +1048,68 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@Highlight(fields = { @HighlightField(name = "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\" } } } }")
Flux<SampleEntity> findAllViaAnnotatedQueryByMessageLike(String message);
@ -1083,6 +1246,9 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@SourceFilters(includes = "?0")
Flux<SampleEntity> searchBy(Collection<String> sourceIncludes);
@SourceFilters(includes = "#{#sourceIncludes}")
Flux<SampleEntity> queryBy(Collection<String> sourceIncludes);
@Query("""
{
"match_all": {}
@ -1093,6 +1259,9 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
@SourceFilters(excludes = "?0")
Flux<SampleEntity> findBy(Collection<String> sourceExcludes);
@SourceFilters(excludes = "#{#sourceExcludes}")
Flux<SampleEntity> getBy(Collection<String> sourceExcludes);
}
@Document(indexName = "#{@indexNameProvider.indexName()}")