diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java index 55cb5ab57..b4e92db4a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java @@ -21,6 +21,7 @@ import java.lang.annotation.RetentionPolicy; /** * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.0 */ @Documented @@ -59,6 +60,8 @@ public @interface HighlightParameters { int numberOfFragments() default -1; + Query highlightQuery() default @Query; + String order() default ""; int phraseLimit() default -1; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/Highlight.java b/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/Highlight.java index 97f6eb5c4..e982c168c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/Highlight.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/Highlight.java @@ -15,14 +15,13 @@ */ package org.springframework.data.elasticsearch.core.query.highlight; -import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import org.springframework.util.Assert; /** * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.3 */ public class Highlight { @@ -57,42 +56,4 @@ public class Highlight { public List getFields() { return fields; } - - /** - * Creates a {@link Highlight} from an Annotation instance. - * - * @param highlight must not be {@literal null} - * @return highlight definition - */ - public static Highlight of(org.springframework.data.elasticsearch.annotations.Highlight highlight) { - - Assert.notNull(highlight, "highlight must not be null"); - - org.springframework.data.elasticsearch.annotations.HighlightParameters parameters = highlight.parameters(); - HighlightParameters highlightParameters = HighlightParameters.builder() // - .withBoundaryChars(parameters.boundaryChars()) // - .withBoundaryMaxScan(parameters.boundaryMaxScan()) // - .withBoundaryScanner(parameters.boundaryScanner()) // - .withBoundaryScannerLocale(parameters.boundaryScannerLocale()) // - .withEncoder(parameters.encoder()) // - .withForceSource(parameters.forceSource()) // - .withFragmenter(parameters.fragmenter()) // - .withFragmentSize(parameters.fragmentSize()) // - .withNoMatchSize(parameters.noMatchSize()) // - .withNumberOfFragments(parameters.numberOfFragments()) // - .withOrder(parameters.order()) // - .withPhraseLimit(parameters.phraseLimit()) // - .withPreTags(parameters.preTags()) // - .withPostTags(parameters.postTags()) // - .withRequireFieldMatch(parameters.requireFieldMatch()) // - .withTagsSchema(parameters.tagsSchema()) // - .withType(parameters.type()) // - .build(); - - List highlightFields = Arrays.stream(highlight.fields()) // - .map(HighlightField::of) // - .collect(Collectors.toList()); - - return new Highlight(highlightParameters, highlightFields); - } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java index 84b15423e..428072ef2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java @@ -199,7 +199,7 @@ public abstract class HighlightCommonParameters { return (SELF) this; } - public SELF withHighlightQuery(Query highlightQuery) { + public SELF withHighlightQuery(@Nullable Query highlightQuery) { this.highlightQuery = highlightQuery; return (SELF) this; } 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 6af2b618e..f5510b32a 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 @@ -15,14 +15,6 @@ */ 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; @@ -50,12 +42,19 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +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; + /** * ElasticsearchQueryMethod * @@ -66,6 +65,7 @@ import org.springframework.util.ClassUtils; * @author Christoph Strobl * @author Peter-Josef Meisch * @author Alexander Torres + * @author Haibo Liu */ public class ElasticsearchQueryMethod extends QueryMethod { @@ -81,8 +81,6 @@ public class ElasticsearchQueryMethod extends QueryMethod { @Nullable private ElasticsearchEntityMetadata metadata; @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, @@ -143,19 +141,13 @@ public class ElasticsearchQueryMethod extends QueryMethod { * @throws IllegalArgumentException if no {@link Highlight} annotation is present on the method * @see #hasAnnotatedHighlight() */ - public HighlightQuery getAnnotatedHighlightQuery() { + public HighlightQuery getAnnotatedHighlightQuery(HighlightConverter highlightConverter) { Assert.isTrue(hasAnnotatedHighlight(), "no Highlight annotation present on " + getName()); - - return highlightQueryLazy.get(); - } - - private HighlightQuery createAnnotatedHighlightQuery() { - Assert.notNull(highlightAnnotation, "highlightAnnotation must not be null"); return new HighlightQuery( - org.springframework.data.elasticsearch.core.query.highlight.Highlight.of(highlightAnnotation), + highlightConverter.convert(highlightAnnotation), getDomainClass()); } @@ -378,7 +370,7 @@ public class ElasticsearchQueryMethod extends QueryMethod { ElasticsearchConverter elasticsearchConverter) { if (hasAnnotatedHighlight()) { - query.setHighlightQuery(getAnnotatedHighlightQuery()); + query.setHighlightQuery(getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor, elasticsearchConverter))); } var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter); diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/HighlightConverter.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/HighlightConverter.java new file mode 100644 index 000000000..68b2e9c67 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/HighlightConverter.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.repository.query; + +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +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.util.Assert; + +import java.util.Arrays; +import java.util.List; + +/** + * Convert {@link org.springframework.data.elasticsearch.annotations.Highlight} to {@link Highlight}. + * + * @author Haibo Liu + */ +public class HighlightConverter { + + private final ElasticsearchParametersParameterAccessor parameterAccessor; + private final ElasticsearchConverter elasticsearchConverter; + + HighlightConverter(ElasticsearchParametersParameterAccessor parameterAccessor, + ElasticsearchConverter elasticsearchConverter) { + this.parameterAccessor = parameterAccessor; + this.elasticsearchConverter = elasticsearchConverter; + } + + /** + * Creates a {@link Highlight} from an Annotation instance. + * + * @param highlight must not be {@literal null} + * @return highlight definition + */ + Highlight convert(org.springframework.data.elasticsearch.annotations.Highlight highlight) { + + Assert.notNull(highlight, "highlight must not be null"); + + org.springframework.data.elasticsearch.annotations.HighlightParameters parameters = highlight.parameters(); + + // 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); + } + + HighlightParameters highlightParameters = HighlightParameters.builder() // + .withBoundaryChars(parameters.boundaryChars()) // + .withBoundaryMaxScan(parameters.boundaryMaxScan()) // + .withBoundaryScanner(parameters.boundaryScanner()) // + .withBoundaryScannerLocale(parameters.boundaryScannerLocale()) // + .withEncoder(parameters.encoder()) // + .withForceSource(parameters.forceSource()) // + .withFragmenter(parameters.fragmenter()) // + .withFragmentSize(parameters.fragmentSize()) // + .withNoMatchSize(parameters.noMatchSize()) // + .withNumberOfFragments(parameters.numberOfFragments()) // + .withHighlightQuery(highlightQuery) // + .withOrder(parameters.order()) // + .withPhraseLimit(parameters.phraseLimit()) // + .withPreTags(parameters.preTags()) // + .withPostTags(parameters.postTags()) // + .withRequireFieldMatch(parameters.requireFieldMatch()) // + .withTagsSchema(parameters.tagsSchema()) // + .withType(parameters.type()) // + .build(); + + List highlightFields = Arrays.stream(highlight.fields()) // + .map(HighlightField::of) // + .toList(); + + return new Highlight(highlightParameters, highlightFields); + } +} 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 c593a42a1..8760c1e25 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 @@ -32,7 +32,6 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledIf; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; @@ -46,9 +45,9 @@ import org.springframework.data.elasticsearch.annotations.Document; 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.HighlightParameters; 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; import org.springframework.data.elasticsearch.core.SearchHits; @@ -76,6 +75,7 @@ import org.springframework.lang.Nullable; * @author Peter-Josef Meisch * @author Rasmus Faber-Espensen * @author James Mudd + * @author Haibo Liu */ @SpringIntegrationTest public abstract class CustomMethodRepositoryIntegrationTests { @@ -1548,6 +1548,26 @@ public abstract class CustomMethodRepositoryIntegrationTests { assertThat(searchHit.getHighlightField("type")).hasSize(1).contains("abc"); } + @Test + void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethod() { + List entities = createSampleEntities("abc xyz", 2); + repository.saveAll(entities); + + // when + SearchHits highlightAbcHits = repository.queryByStringWithSeparateHighlight("abc", "abc"); + + assertThat(highlightAbcHits.getTotalHits()).isEqualTo(2); + SearchHit highlightAbcHit = highlightAbcHits.getSearchHit(0); + assertThat(highlightAbcHit.getHighlightField("type")).hasSize(1).contains("abc xyz"); + + // when + SearchHits highlightXyzHits = repository.queryByStringWithSeparateHighlight("abc", "xyz"); + + assertThat(highlightXyzHits.getTotalHits()).isEqualTo(2); + SearchHit highlightXyzHit = highlightXyzHits.getSearchHit(0); + assertThat(highlightXyzHit.getHighlightField("type")).hasSize(1).contains("abc xyz"); + } + @Test // DATAES-734 void shouldUseGeoSortParameter() { GeoPoint munich = new GeoPoint(48.137154, 11.5761247); @@ -1920,6 +1940,41 @@ public abstract class CustomMethodRepositoryIntegrationTests { @Highlight(fields = { @HighlightField(name = "type") }) SearchHits queryByString(String type); + @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "type":"?0" + } + } + ] + } + } + """ + ) + @Highlight( + fields = {@HighlightField(name = "type")}, + parameters = @HighlightParameters( + highlightQuery = @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "type":"?1" + } + } + ] + } + } + """ + ) + ) + ) + SearchHits queryByStringWithSeparateHighlight(String type, String highlight); + List> queryByMessage(String message); Stream> readByMessage(String message);