support highlight_query (#2793)

* support highlight_query

* implement highlight query with spring data elasticsearch query

* highight query by StringQuery

* split highligh fields assertion into different parts
This commit is contained in:
puppylpg 2023-12-14 04:03:59 +08:00 committed by GitHub
parent 8a3df63493
commit 0e419133a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 7 deletions

View File

@ -49,7 +49,7 @@ Also the reactive implementation that was provided up to now has been moved here
If you are using `ElasticsearchRestTemplate` directly and not the `ElasticsearchOperations` interface you'll need to adjust your imports as well. If you are using `ElasticsearchRestTemplate` directly and not the `ElasticsearchOperations` interface you'll need to adjust your imports as well.
When working with the `NativeSearchQuery` class, you'll need to switch to the `NativeQuery` class, which can take a When working with the `NativeSearchQuery` class, you'll need to switch to the `NativeQuery` class, which can take a
`Query` instance comign from the new Elasticsearch client libraries. `Query` instance coming from the new Elasticsearch client libraries.
You'll find plenty of examples in the test code. You'll find plenty of examples in the test code.
[[elasticsearch-migration-guide-4.4-5.0.breaking-changes-records]] [[elasticsearch-migration-guide-4.4-5.0.breaking-changes-records]]

View File

@ -35,14 +35,17 @@ import org.springframework.util.StringUtils;
* {@link co.elastic.clients.elasticsearch.core.search.Highlight}. * {@link co.elastic.clients.elasticsearch.core.search.Highlight}.
* *
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4 * @since 4.4
*/ */
class HighlightQueryBuilder { class HighlightQueryBuilder {
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext; private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
private final RequestConverter requestConverter;
HighlightQueryBuilder( HighlightQueryBuilder(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) { MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext, RequestConverter requestConverter) {
this.mappingContext = mappingContext; this.mappingContext = mappingContext;
this.requestConverter = requestConverter;
} }
public co.elastic.clients.elasticsearch.core.search.Highlight getHighlight(Highlight highlight, public co.elastic.clients.elasticsearch.core.search.Highlight getHighlight(Highlight highlight,
@ -52,7 +55,7 @@ class HighlightQueryBuilder {
// in the old implementation we could use one addParameters method, but in the new Elasticsearch client // in the old implementation we could use one addParameters method, but in the new Elasticsearch client
// the builder for highlight and highlightfield share no code // the builder for highlight and highlightfield share no code
addParameters(highlight.getParameters(), highlightBuilder); addParameters(highlight.getParameters(), highlightBuilder, type);
for (HighlightField highlightField : highlight.getFields()) { for (HighlightField highlightField : highlight.getFields()) {
String mappedName = mapFieldName(highlightField.getName(), type); String mappedName = mapFieldName(highlightField.getName(), type);
@ -69,7 +72,7 @@ class HighlightQueryBuilder {
* the builder for highlight and highlight fields don't share code, so we have these two methods here that basically are almost copies * the builder for highlight and highlight fields don't share code, so we have these two methods here that basically are almost copies
*/ */
private void addParameters(HighlightParameters parameters, private void addParameters(HighlightParameters parameters,
co.elastic.clients.elasticsearch.core.search.Highlight.Builder builder) { co.elastic.clients.elasticsearch.core.search.Highlight.Builder builder, @Nullable Class<?> type) {
if (StringUtils.hasLength(parameters.getBoundaryChars())) { if (StringUtils.hasLength(parameters.getBoundaryChars())) {
builder.boundaryChars(parameters.getBoundaryChars()); builder.boundaryChars(parameters.getBoundaryChars());
@ -103,6 +106,10 @@ class HighlightQueryBuilder {
builder.numberOfFragments(parameters.getNumberOfFragments()); builder.numberOfFragments(parameters.getNumberOfFragments());
} }
if (parameters.getHighlightQuery() != null) {
builder.highlightQuery(requestConverter.getQuery(parameters.getHighlightQuery(), type));
}
if (StringUtils.hasLength(parameters.getOrder())) { if (StringUtils.hasLength(parameters.getOrder())) {
builder.order(highlighterOrder(parameters.getOrder())); builder.order(highlighterOrder(parameters.getOrder()));
} }
@ -174,6 +181,10 @@ class HighlightQueryBuilder {
builder.numberOfFragments(parameters.getNumberOfFragments()); builder.numberOfFragments(parameters.getNumberOfFragments());
} }
if (parameters.getHighlightQuery() != null) {
builder.highlightQuery(requestConverter.getQuery(parameters.getHighlightQuery(), type));
}
if (StringUtils.hasLength(parameters.getOrder())) { if (StringUtils.hasLength(parameters.getOrder())) {
builder.order(highlighterOrder(parameters.getOrder())); builder.order(highlighterOrder(parameters.getOrder()));
} }

View File

@ -105,6 +105,7 @@ import org.springframework.util.StringUtils;
* @author Sascha Woo * @author Sascha Woo
* @author cdalxndr * @author cdalxndr
* @author scoobyzhang * @author scoobyzhang
* @author Haibo Liu
* @since 4.4 * @since 4.4
*/ */
@SuppressWarnings("ClassCanBeRecord") @SuppressWarnings("ClassCanBeRecord")
@ -1494,7 +1495,7 @@ class RequestConverter {
private void addHighlight(Query query, SearchRequest.Builder builder) { private void addHighlight(Query query, SearchRequest.Builder builder) {
Highlight highlight = query.getHighlightQuery() Highlight highlight = query.getHighlightQuery()
.map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext()) .map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext(), this)
.getHighlight(highlightQuery.getHighlight(), highlightQuery.getType())) .getHighlight(highlightQuery.getHighlight(), highlightQuery.getType()))
.orElse(null); .orElse(null);
@ -1504,7 +1505,7 @@ class RequestConverter {
private void addHighlight(Query query, MultisearchBody.Builder builder) { private void addHighlight(Query query, MultisearchBody.Builder builder) {
Highlight highlight = query.getHighlightQuery() Highlight highlight = query.getHighlightQuery()
.map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext()) .map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext(), this)
.getHighlight(highlightQuery.getHighlight(), highlightQuery.getType())) .getHighlight(highlightQuery.getHighlight(), highlightQuery.getType()))
.orElse(null); .orElse(null);
@ -1646,7 +1647,7 @@ class RequestConverter {
} }
@Nullable @Nullable
private co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query, co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query,
@Nullable Class<?> clazz) { @Nullable Class<?> clazz) {
if (query == null) { if (query == null) {

View File

@ -15,10 +15,13 @@
*/ */
package org.springframework.data.elasticsearch.core.query.highlight; package org.springframework.data.elasticsearch.core.query.highlight;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.3 * @since 4.3
*/ */
public abstract class HighlightCommonParameters { public abstract class HighlightCommonParameters {
@ -31,6 +34,7 @@ public abstract class HighlightCommonParameters {
private final int fragmentSize; private final int fragmentSize;
private final int noMatchSize; private final int noMatchSize;
private final int numberOfFragments; private final int numberOfFragments;
@Nullable private final Query highlightQuery;
private final String order; private final String order;
private final int phraseLimit; private final int phraseLimit;
private final String[] preTags; private final String[] preTags;
@ -51,6 +55,7 @@ public abstract class HighlightCommonParameters {
fragmentSize = builder.fragmentSize; fragmentSize = builder.fragmentSize;
noMatchSize = builder.noMatchSize; noMatchSize = builder.noMatchSize;
numberOfFragments = builder.numberOfFragments; numberOfFragments = builder.numberOfFragments;
highlightQuery = builder.highlightQuery;
order = builder.order; order = builder.order;
phraseLimit = builder.phraseLimit; phraseLimit = builder.phraseLimit;
preTags = builder.preTags; preTags = builder.preTags;
@ -95,6 +100,11 @@ public abstract class HighlightCommonParameters {
return numberOfFragments; return numberOfFragments;
} }
@Nullable
public Query getHighlightQuery() {
return highlightQuery;
}
public String getOrder() { public String getOrder() {
return order; return order;
} }
@ -130,6 +140,11 @@ public abstract class HighlightCommonParameters {
private int fragmentSize = -1; private int fragmentSize = -1;
private int noMatchSize = -1; private int noMatchSize = -1;
private int numberOfFragments = -1; private int numberOfFragments = -1;
/**
* Only the search query part of the {@link Query} takes effect,
* others are just ignored.
*/
@Nullable private Query highlightQuery = null;
private String order = ""; private String order = "";
private int phraseLimit = -1; private int phraseLimit = -1;
private String[] preTags = new String[0]; private String[] preTags = new String[0];
@ -184,6 +199,11 @@ public abstract class HighlightCommonParameters {
return (SELF) this; return (SELF) this;
} }
public SELF withHighlightQuery(Query highlightQuery) {
this.highlightQuery = highlightQuery;
return (SELF) this;
}
public SELF withOrder(String order) { public SELF withOrder(String order) {
this.order = order; this.order = order;
return (SELF) this; return (SELF) this;

View File

@ -66,6 +66,8 @@ import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.highlight.Highlight; import org.springframework.data.elasticsearch.core.query.highlight.Highlight;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightField; import org.springframework.data.elasticsearch.core.query.highlight.HighlightField;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightFieldParameters;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.data.util.StreamUtils; import org.springframework.data.util.StreamUtils;
@ -2964,6 +2966,128 @@ public abstract class ElasticsearchIntegrationTests {
assertThat(highlightField.get(1)).contains("<em>message</em>"); assertThat(highlightField.get(1)).contains("<em>message</em>");
} }
@Test // #2636
void shouldReturnHighlightFieldsWithHighlightQueryInSearchHit() {
IndexCoordinates index = createIndexCoordinatesWithHighlightMessage();
// a highlight query equals to the search query
var sameHighlightQuery = HighlightFieldParameters.builder()
.withHighlightQuery(getBuilderWithTermQuery("message", "message").build())
.build();
Query query = getBuilderWithTermQuery("message", "message") //
.withHighlightQuery(
new HighlightQuery(new Highlight(singletonList(new HighlightField("message", sameHighlightQuery))), HighlightEntity.class)
)
.build();
SearchHits<HighlightEntity> searchHits = operations.search(query, HighlightEntity.class, index);
assertThat(searchHits).isNotNull();
assertThat(searchHits.getSearchHits()).hasSize(1);
SearchHit<HighlightEntity> searchHit = searchHits.getSearchHit(0);
List<String> highlightField = searchHit.getHighlightField("message");
assertThat(highlightField).hasSize(2);
assertThat(highlightField.get(0)).contains("<em>message</em>");
assertThat(highlightField.get(1)).contains("<em>message</em>");
}
@Test // #2636
void shouldReturnDifferentHighlightFieldsWithDifferentHighlightQueryInSearchHit() {
IndexCoordinates index = createIndexCoordinatesWithHighlightMessage();
// a different highlight query from the search query
var differentHighlightQueryInField = HighlightFieldParameters.builder()
.withHighlightQuery(getBuilderWithTermQuery("message", "initial").build())
.build();
// highlight_query in field
Query highlightQueryInField = getBuilderWithTermQuery("message", "message") //
.withHighlightQuery(
new HighlightQuery(new Highlight(singletonList(new HighlightField("message", differentHighlightQueryInField))), HighlightEntity.class)
)
.build();
assertThatHighlightFieldsIsDifferentFromHighlightQuery(highlightQueryInField, index);
}
@Test // #2636
void shouldReturnDifferentHighlightFieldsWithDifferentParamHighlightQueryInSearchHit() {
IndexCoordinates index = createIndexCoordinatesWithHighlightMessage();
// a different highlight query from the search query and used in highlight param rather than field
var differentHighlightQueryInParam = HighlightParameters.builder()
.withHighlightQuery(getBuilderWithTermQuery("message", "initial").build())
.build();
// highlight_query in param
Query highlightQueryInParam = getBuilderWithTermQuery("message", "message") //
.withHighlightQuery(
new HighlightQuery(new Highlight(differentHighlightQueryInParam, singletonList(new HighlightField("message"))), HighlightEntity.class)
)
.build();
assertThatHighlightFieldsIsDifferentFromHighlightQuery(highlightQueryInParam, index);
}
@Test // #2636
void shouldReturnDifferentHighlightFieldsWithDifferentHighlightCriteriaQueryInSearchHit() {
IndexCoordinates index = createIndexCoordinatesWithHighlightMessage();
// a different highlight query from the search query, written by CriteriaQuery rather than NativeQuery
var criteriaHighlightQueryInParam = HighlightParameters.builder()
.withHighlightQuery(new CriteriaQuery(new Criteria("message").is("initial")))
.build();
// highlight_query in param
Query differentHighlightQueryUsingCriteria = getBuilderWithTermQuery("message", "message") //
.withHighlightQuery(
new HighlightQuery(new Highlight(criteriaHighlightQueryInParam, singletonList(new HighlightField("message"))), HighlightEntity.class)
)
.build();
assertThatHighlightFieldsIsDifferentFromHighlightQuery(differentHighlightQueryUsingCriteria, index);
}
@Test // #2636
void shouldReturnDifferentHighlightFieldsWithDifferentHighlightStringQueryInSearchHit() {
IndexCoordinates index = createIndexCoordinatesWithHighlightMessage();
// a different highlight query from the search query, written by StringQuery
var stringHighlightQueryInParam = HighlightParameters.builder()
.withHighlightQuery(new StringQuery(
"""
{
"term": {
"message": {
"value": "initial"
}
}
}
"""
))
.build();
// highlight_query in param
Query differentHighlightQueryUsingStringQuery = getBuilderWithTermQuery("message", "message") //
.withHighlightQuery(
new HighlightQuery(new Highlight(stringHighlightQueryInParam, singletonList(new HighlightField("message"))), HighlightEntity.class)
)
.build();
assertThatHighlightFieldsIsDifferentFromHighlightQuery(differentHighlightQueryUsingStringQuery, index);
}
private IndexCoordinates createIndexCoordinatesWithHighlightMessage() {
IndexCoordinates index = IndexCoordinates.of("test-index-highlight-entity-template");
HighlightEntity entity = new HighlightEntity("1",
"This message is a long text which contains the word to search for "
+ "in two places, the first being near the beginning and the second near the end of the message. "
+ "However, i'll use a different highlight query from the initial search query");
IndexQuery indexQuery = new IndexQueryBuilder().withId(entity.getId()).withObject(entity).build();
operations.index(indexQuery, index);
operations.indexOps(index).refresh();
return index;
}
private void assertThatHighlightFieldsIsDifferentFromHighlightQuery(Query query, IndexCoordinates index) {
SearchHits<HighlightEntity> searchHits = operations.search(query, HighlightEntity.class, index);
SearchHit<HighlightEntity> searchHit = searchHits.getSearchHit(0);
List<String> highlightField = searchHit.getHighlightField("message");
assertThat(highlightField).hasSize(1);
assertThat(highlightField.get(0)).contains("<em>initial</em>");
}
@Test // #1686 @Test // #1686
void shouldRunRescoreQueryInSearchQuery() { void shouldRunRescoreQueryInSearchQuery() {
IndexCoordinates index = IndexCoordinates.of(indexNameProvider.getPrefix() + "rescore-entity"); IndexCoordinates index = IndexCoordinates.of(indexNameProvider.getPrefix() + "rescore-entity");