diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java index c8107a62f..ef34cc849 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java @@ -15,8 +15,14 @@ */ package org.springframework.data.elasticsearch.annotations; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.QueryAnnotation; -import java.lang.annotation.*; /** * Query @@ -34,10 +40,18 @@ import java.lang.annotation.*; public @interface Query { /** - * @return Elasticsearch query to be used when executing query. May contain placeholders eg. ?0 + * @return Elasticsearch query to be used when executing query. May contain placeholders eg. ?0. Alias for query. */ + @AliasFor("query") String value() default ""; + /** + * @return Elasticsearch query to be used when executing query. May contain placeholders eg. ?0. Alias for value + * @since 5.0 + */ + @AliasFor("value") + String query() default ""; + /** * Named Query Named looked up by repository. * diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java b/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java new file mode 100644 index 000000000..30319bdf8 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 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.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be placed on repository methods to define the properties that should be requested from + * Elasticsearch when the method is run. + * + * @author Alexander Torres + * @author Peter-Josef Meisch + * @since 5.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +public @interface SourceFilters { + + /** + * Properties to be requested from Elasticsearch to be included in the response. These can be passed in as literals + * like + * + *
+	 * {@code @SourceFilters(includes = {"property1", "property2"})}
+	 * 
+ * + * or as a parameterized value + * + *
+	 * {@code @SourceFilters(includes = "?0")}
+	 * 
+ * + * when the list of properties is passed as a function parameter. + */ + String[] includes() default ""; + + /** + * Properties to be requested from Elasticsearch to be excluded in the response. These can be passed in as literals + * like + * + *
+	 * {@code @SourceFilters(excludes = {"property1", "property2"})}
+	 * 
+ * + * or as a parameterized value + * + *
+	 * {@code @SourceFilters(excludes = "?0")}
+	 * 
+ * + * when the list of properties is passed as a function parameter. + */ + String[] excludes() default ""; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java index 0805f1d91..635c194ba 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java @@ -16,6 +16,9 @@ package org.springframework.data.elasticsearch.repository.query; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; @@ -31,12 +34,14 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository protected static final int DEFAULT_STREAM_BATCH_SIZE = 500; protected ElasticsearchQueryMethod queryMethod; - protected ElasticsearchOperations elasticsearchOperations; + protected final ElasticsearchOperations elasticsearchOperations; + protected final ElasticsearchConverter elasticsearchConverter; public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations) { this.queryMethod = queryMethod; this.elasticsearchOperations = elasticsearchOperations; + this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter(); } @Override @@ -49,4 +54,19 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository * @since 4.2 */ public abstract boolean isCountQuery(); + + protected void prepareQuery(Query query, Class clazz, ParameterAccessor parameterAccessor) { + + elasticsearchConverter.updateQuery(query, clazz); + + if (queryMethod.hasAnnotatedHighlight()) { + query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery()); + } + + var sourceFilter = queryMethod.getSourceFilter(parameterAccessor, + elasticsearchOperations.getElasticsearchConverter()); + if (sourceFilter != null) { + query.addSourceFilter(sourceFilter); + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java index 6e49eb651..cbd7de8d3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java @@ -27,6 +27,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.ByQueryResponse; import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.SourceFilter; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingConverter; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingExecution; import org.springframework.data.mapping.context.MappingContext; @@ -88,6 +89,12 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery()); } + var sourceFilter = queryMethod.getSourceFilter(parameterAccessor, + elasticsearchOperations.getElasticsearchConverter()); + if (sourceFilter != null) { + query.addSourceFilter(sourceFilter); + } + Class targetType = processor.getReturnedType().getTypeToRead(); String indexName = queryMethod.getEntityInformation().getIndexName(); IndexCoordinates index = IndexCoordinates.of(indexName); diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java index cea66fe49..57ea3f73d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java @@ -23,7 +23,6 @@ import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchHitsImpl; import org.springframework.data.elasticsearch.core.TotalHitsRelation; -import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; @@ -49,13 +48,11 @@ import org.springframework.util.ClassUtils; public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery { private final PartTree tree; - private final ElasticsearchConverter elasticsearchConverter; private final MappingContext mappingContext; public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) { super(method, elasticsearchOperations); this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType()); - this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter(); this.mappingContext = elasticsearchConverter.getMappingContext(); } @@ -66,18 +63,16 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery @Override public Object execute(Object[] parameters) { - Class clazz = queryMethod.getResultProcessor().getReturnedType().getDomainType(); - ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - CriteriaQuery query = createQuery(accessor); + Class clazz = queryMethod.getResultProcessor().getReturnedType().getDomainType(); + ParametersParameterAccessor parameterAccessor = new ParametersParameterAccessor(queryMethod.getParameters(), + parameters); + + CriteriaQuery query = createQuery(parameterAccessor); Assert.notNull(query, "unsupported query"); - elasticsearchConverter.updateQuery(query, clazz); - - if (queryMethod.hasAnnotatedHighlight()) { - query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery()); - } + prepareQuery(query, clazz, parameterAccessor); IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz); @@ -89,11 +84,11 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery } if (tree.isDelete()) { - result = countOrGetDocumentsForDelete(query, accessor); + result = countOrGetDocumentsForDelete(query, parameterAccessor); elasticsearchOperations.delete(query, clazz, index); elasticsearchOperations.indexOps(index).refresh(); } else if (queryMethod.isPageQuery()) { - query.setPageable(accessor.getPageable()); + query.setPageable(parameterAccessor.getPageable()); SearchHits searchHits = elasticsearchOperations.search(query, clazz, index); if (queryMethod.isSearchPageMethod()) { result = SearchHitSupport.searchPageFor(searchHits, query.getPageable()); @@ -101,15 +96,15 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery result = SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(searchHits, query.getPageable())); } } else if (queryMethod.isStreamQuery()) { - if (accessor.getPageable().isUnpaged()) { + if (parameterAccessor.getPageable().isUnpaged()) { query.setPageable(PageRequest.of(0, DEFAULT_STREAM_BATCH_SIZE)); } else { - query.setPageable(accessor.getPageable()); + query.setPageable(parameterAccessor.getPageable()); } result = StreamUtils.createStreamFromIterator(elasticsearchOperations.searchForStream(query, clazz, index)); } else if (queryMethod.isCollectionQuery()) { - if (accessor.getPageable().isUnpaged()) { + if (parameterAccessor.getPageable().isUnpaged()) { int itemCount = (int) elasticsearchOperations.count(query, clazz, index); if (itemCount == 0) { @@ -119,7 +114,7 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery query.setPageable(PageRequest.of(0, Math.max(1, itemCount))); } } else { - query.setPageable(accessor.getPageable()); + query.setPageable(parameterAccessor.getPageable()); } if (result == null) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java index 4e6259d99..a70c4b23b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java @@ -17,22 +17,31 @@ package org.springframework.data.elasticsearch.repository.query; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.stream.Stream; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.annotations.SourceFilters; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchPage; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; import org.springframework.data.elasticsearch.core.query.HighlightQuery; +import org.springframework.data.elasticsearch.core.query.SourceFilter; +import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; @@ -49,16 +58,19 @@ import org.springframework.util.ClassUtils; * @author Mark Paluch * @author Christoph Strobl * @author Peter-Josef Meisch + * @author Alexander Torres */ public class ElasticsearchQueryMethod extends QueryMethod { private final MappingContext, ElasticsearchPersistentProperty> mappingContext; - private @Nullable ElasticsearchEntityMetadata metadata; + @Nullable private ElasticsearchEntityMetadata metadata; protected final Method method; // private in base class, but needed here and in derived classes as well @Nullable private final Query queryAnnotation; @Nullable private final Highlight highlightAnnotation; private final Lazy highlightQueryLazy = Lazy.of(this::createAnnotatedHighlightQuery); + @Nullable private final SourceFilters sourceFilters; + public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory factory, MappingContext, ElasticsearchPersistentProperty> mappingContext) { @@ -70,6 +82,7 @@ public class ElasticsearchQueryMethod extends QueryMethod { this.mappingContext = mappingContext; this.queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); this.highlightAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Highlight.class); + this.sourceFilters = AnnotatedElementUtils.findMergedAnnotation(method, SourceFilters.class); verifyCountQueryTypes(); } @@ -92,8 +105,9 @@ public class ElasticsearchQueryMethod extends QueryMethod { /** * @return the query String. Must not be {@literal null} when {@link #hasAnnotatedQuery()} returns true */ + @Nullable public String getAnnotatedQuery() { - return queryAnnotation.value(); + return queryAnnotation != null ? queryAnnotation.value() : null; } /** @@ -246,4 +260,86 @@ public class ElasticsearchQueryMethod extends QueryMethod { return queryAnnotation != null && queryAnnotation.count(); } + /** + * @return {@literal true} if the method is annotated with {@link SourceFilters}. + * @since 5.0 + */ + public boolean hasSourceFilters() { + return sourceFilters != null; + } + + /** + * @return the {@link SourceFilters} annotation for this method. + * @since 5.0 + */ + @Nullable + public SourceFilters getSourceFilters() { + return sourceFilters; + } + + /** + * Uses the sourceFilters property to create a {@link SourceFilter} to be added to a + * {@link org.springframework.data.elasticsearch.core.query.Query} + * + * @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 + * @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) { + + if (sourceFilters == null || (sourceFilters.includes().length == 0 && sourceFilters.excludes().length == 0)) { + return null; + } + + ElasticsearchPersistentEntity persistentEntity = converter.getMappingContext() + .getPersistentEntity(getEntityInformation().getJavaType()); + + StringQueryUtil stringQueryUtil = new StringQueryUtil(converter.getConversionService()); + FetchSourceFilterBuilder fetchSourceFilterBuilder = new FetchSourceFilterBuilder(); + + if (sourceFilters.includes().length > 0) { + fetchSourceFilterBuilder + .withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor, stringQueryUtil, persistentEntity)); + } + + if (sourceFilters.excludes().length > 0) { + fetchSourceFilterBuilder + .withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor, stringQueryUtil, persistentEntity)); + } + + return fetchSourceFilterBuilder.build(); + } + + private String[] mapParameters(String[] source, ParameterAccessor parameterAccessor, StringQueryUtil stringQueryUtil, + @Nullable ElasticsearchPersistentEntity persistentEntity) { + + List unmappedFieldNames = new ArrayList<>(); + + for (String s : source) { + + if (!s.isBlank()) { + String fieldName = stringQueryUtil.replacePlaceholders(s, parameterAccessor); + // this could be "[\"foo\",\"bar\"]", must be split + if (fieldName.startsWith("[") && fieldName.endsWith("]")) { + unmappedFieldNames.addAll( // + Arrays.asList(fieldName.substring(1, fieldName.length() - 2) // + .replaceAll("\\\"", "") // + .split(","))); // + } else { + unmappedFieldNames.add(fieldName); + } + } + } + + return unmappedFieldNames.stream().map(fieldName -> { + ElasticsearchPersistentProperty property = persistentEntity != null + ? persistentEntity.getPersistentProperty(fieldName) + : null; + return property != null ? property.getFieldName() : fieldName; + }).toArray(String[]::new); + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java index 0875d8579..0d857b302 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java @@ -21,6 +21,7 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; import org.springframework.data.repository.query.ParametersParameterAccessor; @@ -38,13 +39,13 @@ import org.springframework.util.Assert; */ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery { - private String query; + private final String queryString; public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations, - String query) { + String queryString) { super(queryMethod, elasticsearchOperations); - Assert.notNull(query, "Query cannot be empty"); - this.query = query; + Assert.notNull(queryString, "Query cannot be empty"); + this.queryString = queryString; } @Override @@ -56,40 +57,42 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue public Object execute(Object[] parameters) { Class clazz = queryMethod.getResultProcessor().getReturnedType().getDomainType(); - ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); + ParametersParameterAccessor parameterAccessor = new ParametersParameterAccessor(queryMethod.getParameters(), + parameters); - StringQuery stringQuery = createQuery(accessor); - - Assert.notNull(stringQuery, "unsupported query"); + Query query = createQuery(parameterAccessor); + Assert.notNull(query, "unsupported query"); if (queryMethod.hasAnnotatedHighlight()) { - stringQuery.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery()); + query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery()); } + prepareQuery(query, clazz, parameterAccessor); + IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz); - Object result = null; + Object result; if (isCountQuery()) { - result = elasticsearchOperations.count(stringQuery, clazz, index); + result = elasticsearchOperations.count(query, clazz, index); } else if (queryMethod.isPageQuery()) { - stringQuery.setPageable(accessor.getPageable()); - SearchHits searchHits = elasticsearchOperations.search(stringQuery, clazz, index); + query.setPageable(parameterAccessor.getPageable()); + SearchHits searchHits = elasticsearchOperations.search(query, clazz, index); if (queryMethod.isSearchPageMethod()) { - result = SearchHitSupport.searchPageFor(searchHits, stringQuery.getPageable()); + result = SearchHitSupport.searchPageFor(searchHits, query.getPageable()); } else { - result = SearchHitSupport - .unwrapSearchHits(SearchHitSupport.searchPageFor(searchHits, stringQuery.getPageable())); + result = SearchHitSupport.unwrapSearchHits(SearchHitSupport.searchPageFor(searchHits, query.getPageable())); } } else if (queryMethod.isStreamQuery()) { - stringQuery.setPageable( - accessor.getPageable().isPaged() ? accessor.getPageable() : PageRequest.of(0, DEFAULT_STREAM_BATCH_SIZE)); - result = StreamUtils.createStreamFromIterator(elasticsearchOperations.searchForStream(stringQuery, clazz, index)); + query.setPageable(parameterAccessor.getPageable().isPaged() ? parameterAccessor.getPageable() + : PageRequest.of(0, DEFAULT_STREAM_BATCH_SIZE)); + result = StreamUtils.createStreamFromIterator(elasticsearchOperations.searchForStream(query, clazz, index)); } else if (queryMethod.isCollectionQuery()) { - stringQuery.setPageable(accessor.getPageable().isPaged() ? accessor.getPageable() : Pageable.unpaged()); - result = elasticsearchOperations.search(stringQuery, clazz, index); + query.setPageable( + parameterAccessor.getPageable().isPaged() ? parameterAccessor.getPageable() : Pageable.unpaged()); + result = elasticsearchOperations.search(query, clazz, index); } else { - result = elasticsearchOperations.searchOne(stringQuery, clazz, index); + result = elasticsearchOperations.searchOne(query, clazz, index); } return (queryMethod.isNotSearchHitMethod() && queryMethod.isNotSearchPageMethod()) @@ -99,7 +102,7 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue protected StringQuery createQuery(ParametersParameterAccessor parameterAccessor) { String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService()) - .replacePlaceholders(this.query, parameterAccessor); + .replacePlaceholders(this.queryString, parameterAccessor); return new StringQuery(queryString); } diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java index 1d3feef59..580bb1d96 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java @@ -48,6 +48,7 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.HighlightField; import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.annotations.SourceFilters; import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; @@ -80,12 +81,11 @@ import org.springframework.lang.Nullable; @SpringIntegrationTest public abstract class CustomMethodRepositoryIntegrationTests implements NewElasticsearchClientDevelopment { + @Autowired ElasticsearchOperations operations; @Autowired private IndexNameProvider indexNameProvider; @Autowired private SampleCustomMethodRepository repository; @Autowired private SampleStreamingCustomMethodRepository streamingRepository; - @Autowired ElasticsearchOperations operations; - boolean rhlcWithCluster8() { var clusterVersion = ((AbstractElasticsearchTemplate) operations).getClusterVersion(); return (oldElasticsearchClient() && clusterVersion != null && clusterVersion.startsWith("8")); @@ -1675,6 +1675,94 @@ public abstract class CustomMethodRepositoryIntegrationTests implements NewElast assertThat(returnedIds).containsAll(ids); } + @Test // #2146 + @DisplayName("should use sourceIncludes from annotation") + void shouldUseSourceIncludesFromAnnotation() { + + 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.searchWithSourceFilterIncludesAnnotation(); + + 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 sourceIncludes from parameter") + void shouldUseSourceIncludesFromParameter() { + + 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.searchBy(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() { + + 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.searchWithSourceFilterExcludesAnnotation(); + + 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 source excludes from parameter") + void shouldUseSourceExcludesFromParameter() { + + 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.findBy(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 createSampleEntities(String type, int numberOfEntities) { List entities = new ArrayList<>(); @@ -1690,93 +1778,6 @@ public abstract class CustomMethodRepositoryIntegrationTests implements NewElast return entities; } - @Document(indexName = "#{@indexNameProvider.indexName()}") - static class SampleEntity { - @Nullable - @Id private String id; - @Nullable - @Field(type = Text, store = true, fielddata = true) private String type; - @Nullable - @Field(type = Text, store = true, fielddata = true) private String message; - @Nullable - @Field(type = Keyword) private String keyword; - @Nullable private int rate; - @Nullable private boolean available; - @Nullable private GeoPoint location; - @Nullable - @Version private Long version; - - @Nullable - public String getId() { - return id; - } - - public void setId(@Nullable String id) { - this.id = id; - } - - @Nullable - public String getType() { - return type; - } - - public void setType(@Nullable String type) { - this.type = type; - } - - @Nullable - public String getMessage() { - return message; - } - - public void setMessage(@Nullable String message) { - this.message = message; - } - - @Nullable - public String getKeyword() { - return keyword; - } - - public void setKeyword(@Nullable String keyword) { - this.keyword = keyword; - } - - public int getRate() { - return rate; - } - - public void setRate(int rate) { - this.rate = rate; - } - - public boolean isAvailable() { - return available; - } - - public void setAvailable(boolean available) { - this.available = available; - } - - @Nullable - public GeoPoint getLocation() { - return location; - } - - public void setLocation(@Nullable GeoPoint location) { - this.location = location; - } - - @Nullable - public java.lang.Long getVersion() { - return version; - } - - public void setVersion(@Nullable java.lang.Long version) { - this.version = version; - } - } - /** * @author Rizwan Idrees * @author Mohsin Husen @@ -1911,11 +1912,30 @@ public abstract class CustomMethodRepositoryIntegrationTests implements NewElast @Query("{\"ids\" : {\"values\" : ?0 }}") List getByIds(Collection ids); + + @Query(""" + { + "match_all": {} + } + """) + @SourceFilters(includes = { "message", "customFieldNameMessage" }) + SearchHits searchWithSourceFilterIncludesAnnotation(); + + @SourceFilters(includes = "?0") + SearchHits searchBy(Collection sourceIncludes); + + @Query(""" + { + "match_all": {} + } + """) + @SourceFilters(excludes = { "type", "keyword" }) + SearchHits searchWithSourceFilterExcludesAnnotation(); + + @SourceFilters(excludes = "?0") + SearchHits findBy(Collection sourceExcludes); } - /** - * @author Rasmus Faber-Espensen - */ public interface SampleStreamingCustomMethodRepository extends ElasticsearchRepository { Stream findByType(String type); @@ -1928,4 +1948,103 @@ public abstract class CustomMethodRepositoryIntegrationTests implements NewElast Stream> streamSearchHitsByType(String type); } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class SampleEntity { + @Nullable + @Id private String id; + @Nullable + @Field(type = Text, store = true, fielddata = true) private String type; + @Nullable + @Field(type = Text, store = true, fielddata = true) private String message; + @Nullable + @Field(type = Keyword) private String keyword; + @Nullable private int rate; + @Nullable private boolean available; + @Nullable private GeoPoint location; + @Nullable + @Version private Long version; + + @Field(name = "custom_field_name", type = Text) + @Nullable private String customFieldNameMessage; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable String type) { + this.type = type; + } + + @Nullable + public String getMessage() { + return message; + } + + public void setMessage(@Nullable String message) { + this.message = message; + } + + @Nullable + public String getCustomFieldNameMessage() { + return customFieldNameMessage; + } + + public void setCustomFieldNameMessage(@Nullable String customFieldNameMessage) { + this.customFieldNameMessage = customFieldNameMessage; + } + + @Nullable + public String getKeyword() { + return keyword; + } + + public void setKeyword(@Nullable String keyword) { + this.keyword = keyword; + } + + public int getRate() { + return rate; + } + + public void setRate(int rate) { + this.rate = rate; + } + + public boolean isAvailable() { + return available; + } + + public void setAvailable(boolean available) { + this.available = available; + } + + @Nullable + public GeoPoint getLocation() { + return location; + } + + public void setLocation(@Nullable GeoPoint location) { + this.location = location; + } + + @Nullable + public java.lang.Long getVersion() { + return version; + } + + public void setVersion(@Nullable java.lang.Long version) { + this.version = version; + } + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java index 3368732f0..e995d1a80 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java @@ -23,6 +23,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -47,6 +48,7 @@ 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.Query; +import org.springframework.data.elasticsearch.annotations.SourceFilters; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -634,6 +636,98 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { } + @Test // #2146 + @DisplayName("should use sourceIncludes from annotation") + void shouldUseSourceIncludesFromAnnotation() { + + var entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity).block(); + + repository.searchWithSourceFilterIncludesAnnotation() // + .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 sourceIncludes from parameter") + void shouldUseSourceIncludesFromParameter() { + + var entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity).block(); + + repository.searchBy(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() { + + var entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity).block(); + + repository.searchWithSourceFilterExcludesAnnotation() // + .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 source excludes from parameter") + void shouldUseSourceExcludesFromParameter() { + + var entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity).block(); + + repository.findBy(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(); + } + Mono bulkIndex(SampleEntity... entities) { return operations.saveAll(Arrays.asList(entities), IndexCoordinates.of(indexNameProvider.indexName())).then(); } @@ -683,6 +777,27 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { @Query("{\"bool\": {\"must\": [{ \"terms\": { \"message\": ?0 } }, { \"terms\": { \"rate\": ?1 } }] } }") Flux findAllViaAnnotatedQueryByMessageInAndRatesIn(List messages, List rates); + @Query(query = """ + { + "match_all": {} + } + """) + @SourceFilters(includes = { "message", "customFieldNameMessage" }) + Flux searchWithSourceFilterIncludesAnnotation(); + + @SourceFilters(includes = "?0") + Flux searchBy(Collection sourceIncludes); + + @Query(""" + { + "match_all": {} + } + """) + @SourceFilters(excludes = { "type", "keyword" }) + Flux searchWithSourceFilterExcludesAnnotation(); + + @SourceFilters(excludes = "?0") + Flux findBy(Collection sourceExcludes); } @Document(indexName = "#{@indexNameProvider.indexName()}") @@ -693,10 +808,15 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { @Field(type = FieldType.Text, store = true, fielddata = true) private String type; @Nullable @Field(type = FieldType.Text, store = true, fielddata = true) private String message; + @Nullable + @Field(type = FieldType.Keyword) private String keyword; + @Nullable private int rate; @Nullable private boolean available; @Nullable @Version private Long version; + @Field(name = "custom_field_name", type = FieldType.Text) + @Nullable private String customFieldNameMessage; public SampleEntity() {} @@ -733,6 +853,15 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { this.type = type; } + @Nullable + public String getKeyword() { + return keyword; + } + + public void setKeyword(@Nullable String keyword) { + this.keyword = keyword; + } + @Nullable public String getMessage() { return message; @@ -766,5 +895,14 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { public void setVersion(@Nullable java.lang.Long version) { this.version = version; } + + @Nullable + public String getCustomFieldNameMessage() { + return customFieldNameMessage; + } + + public void setCustomFieldNameMessage(@Nullable String customFieldNameMessage) { + this.customFieldNameMessage = customFieldNameMessage; + } } }