Add query Explain Support.

Original Pull Request #1674
Closes #725
This commit is contained in:
Peter-Josef Meisch 2021-01-30 19:50:32 +01:00 committed by GitHub
parent ddc7246c42
commit 63eebdea88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 302 additions and 30 deletions

View File

@ -1144,6 +1144,8 @@ class RequestFactory {
sourceBuilder.timeout(timeout);
}
sourceBuilder.explain(query.getExplain());
request.source(sourceBuilder);
return request;
}
@ -1224,6 +1226,8 @@ class RequestFactory {
searchRequestBuilder.setTimeout(timeout);
}
searchRequestBuilder.setExplain(query.getExplain());
return searchRequestBuilder;
}

View File

@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.elasticsearch.core.document.Explanation;
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -44,16 +45,18 @@ public class SearchHit<T> {
private final Map<String, List<String>> highlightFields = new LinkedHashMap<>();
private final Map<String, SearchHits<?>> innerHits = new LinkedHashMap<>();
@Nullable private final NestedMetaData nestedMetaData;
@Nullable private String routing;
@Nullable private final String routing;
@Nullable private final Explanation explanation;
public SearchHit(@Nullable String index, @Nullable String id, @Nullable String routing, float score,
@Nullable Object[] sortValues, @Nullable Map<String, List<String>> highlightFields, T content) {
this(index, id, routing, score, sortValues, highlightFields, null, null, content);
this(index, id, routing, score, sortValues, highlightFields, null, null, null, content);
}
public SearchHit(@Nullable String index, @Nullable String id, @Nullable String routing, float score,
@Nullable Object[] sortValues, @Nullable Map<String, List<String>> highlightFields,
@Nullable Map<String, SearchHits<?>> innerHits, @Nullable NestedMetaData nestedMetaData, T content) {
@Nullable Map<String, SearchHits<?>> innerHits, @Nullable NestedMetaData nestedMetaData,
@Nullable Explanation explanation, T content) {
this.index = index;
this.id = id;
this.routing = routing;
@ -69,7 +72,7 @@ public class SearchHit<T> {
}
this.nestedMetaData = nestedMetaData;
this.explanation = explanation;
this.content = content;
}
@ -176,4 +179,13 @@ public class SearchHit<T> {
public String getRouting() {
return routing;
}
/**
* @return the explanation for this SearchHit.
* @since 4.2
*/
@Nullable
public Explanation getExplanation() {
return explanation;
}
}

View File

@ -113,6 +113,7 @@ class SearchHitMapping<T> {
getHighlightsAndRemapFieldNames(searchDocument), //
mapInnerHits(searchDocument), //
searchDocument.getNestedMetaData(), //
searchDocument.getExplanation(), //
content); //
}
@ -196,6 +197,7 @@ class SearchHitMapping<T> {
searchDocument.getHighlightFields(), //
searchHit.getInnerHits(), //
persistentEntityWithNestedMetaData.nestedMetaData, //
searchHit.getExplanation(), //
targetObject));
});

View File

@ -161,16 +161,12 @@ public class DocumentAdapters {
Map<String, SearchHits> sourceInnerHits = source.getInnerHits();
if (sourceInnerHits != null) {
sourceInnerHits.forEach((name, searchHits) -> {
innerHits.put(name, SearchDocumentResponse.from(searchHits, null, null));
});
sourceInnerHits
.forEach((name, searchHits) -> innerHits.put(name, SearchDocumentResponse.from(searchHits, null, null)));
}
NestedMetaData nestedMetaData = null;
if (source.getNestedIdentity() != null) {
nestedMetaData = from(source.getNestedIdentity());
}
NestedMetaData nestedMetaData = from(source.getNestedIdentity());
Explanation explanation = from(source.getExplanation());
BytesReference sourceRef = source.getSourceRef();
@ -178,7 +174,7 @@ public class DocumentAdapters {
return new SearchDocumentAdapter(
source.getScore(), source.getSortValues(), source.getFields(), highlightFields, fromDocumentFields(source,
source.getIndex(), source.getId(), source.getVersion(), source.getSeqNo(), source.getPrimaryTerm()),
innerHits, nestedMetaData);
innerHits, nestedMetaData, explanation);
}
Document document = Document.from(source.getSourceAsMap());
@ -192,17 +188,32 @@ public class DocumentAdapters {
document.setPrimaryTerm(source.getPrimaryTerm());
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
document, innerHits, nestedMetaData);
document, innerHits, nestedMetaData, explanation);
}
private static NestedMetaData from(SearchHit.NestedIdentity nestedIdentity) {
@Nullable
private static Explanation from(@Nullable org.apache.lucene.search.Explanation explanation) {
NestedMetaData child = null;
if (nestedIdentity.getChild() != null) {
child = from(nestedIdentity.getChild());
if (explanation == null) {
return null;
}
List<Explanation> details = new ArrayList<>();
for (org.apache.lucene.search.Explanation detail : explanation.getDetails()) {
details.add(from(detail));
}
return new Explanation(explanation.isMatch(), explanation.getValue().doubleValue(), explanation.getDescription(),
details);
}
@Nullable
private static NestedMetaData from(@Nullable SearchHit.NestedIdentity nestedIdentity) {
if (nestedIdentity == null) {
return null;
}
NestedMetaData child = from(nestedIdentity.getChild());
return NestedMetaData.of(nestedIdentity.getField().string(), nestedIdentity.getOffset(), child);
}
@ -210,7 +221,7 @@ public class DocumentAdapters {
* Create an unmodifiable {@link Document} from {@link Iterable} of {@link DocumentField}s.
*
* @param documentFields the {@link DocumentField}s backing the {@link Document}.
* @param index
* @param index the index where the Document was found
* @return the adapted {@link Document}.
*/
public static Document fromDocumentFields(Iterable<DocumentField> documentFields, String index, String id,
@ -458,10 +469,11 @@ public class DocumentAdapters {
private final Map<String, List<String>> highlightFields = new HashMap<>();
private final Map<String, SearchDocumentResponse> innerHits = new HashMap<>();
@Nullable private final NestedMetaData nestedMetaData;
@Nullable private final Explanation explanation;
SearchDocumentAdapter(float score, Object[] sortValues, Map<String, DocumentField> fields,
Map<String, List<String>> highlightFields, Document delegate, Map<String, SearchDocumentResponse> innerHits,
@Nullable NestedMetaData nestedMetaData) {
@Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation) {
this.score = score;
this.sortValues = sortValues;
@ -470,6 +482,7 @@ public class DocumentAdapters {
this.highlightFields.putAll(highlightFields);
this.innerHits.putAll(innerHits);
this.nestedMetaData = nestedMetaData;
this.explanation = explanation;
}
@Override
@ -646,6 +659,12 @@ public class DocumentAdapters {
return delegate.entrySet();
}
@Override
@Nullable
public Explanation getExplanation() {
return explanation;
}
@Override
public boolean equals(Object o) {
if (this == o) {

View File

@ -0,0 +1,99 @@
/*
* Copyright 2021 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.core.document;
import java.util.List;
import java.util.Objects;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* class that holds explanations returned from an Elasticsearch search.
*
* @author Peter-Josef Meisch
*/
public class Explanation {
private final boolean match;
private final Double value;
@Nullable private final String description;
private final List<Explanation> details;
public Explanation(boolean match, Double value, @Nullable String description, List<Explanation> details) {
Assert.notNull(value, "value must not be null");
Assert.notNull(details, "details must not be null");
this.match = match;
this.value = value;
this.description = description;
this.details = details;
}
public boolean isMatch() {
return match;
}
public Double getValue() {
return value;
}
@Nullable
public String getDescription() {
return description;
}
public List<Explanation> getDetails() {
return details;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Explanation that = (Explanation) o;
if (match != that.match)
return false;
if (!value.equals(that.value))
return false;
if (!Objects.equals(description, that.description))
return false;
return details.equals(that.details);
}
@Override
public int hashCode() {
int result = (match ? 1 : 0);
result = 31 * result + value.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + details.hashCode();
return result;
}
@Override
public String toString() {
return "Explanation{" + //
"match=" + match + //
", value=" + value + //
", description='" + description + '\'' + //
", details=" + details + //
'}'; //
}
}

View File

@ -21,7 +21,7 @@ import java.util.Map;
import org.springframework.lang.Nullable;
/**
* Extension to {@link Document} exposing a search response related data.
* Extension to {@link Document} exposing search response related data.
*
* @author Mark Paluch
* @author Peter-Josef Meisch
@ -98,4 +98,11 @@ public interface SearchDocument extends Document {
default String getRouting() {
return getFieldValue("_routing");
}
/**
* @return the explanation for the SearchHit.
* @since 4.2
*/
@Nullable
Explanation getExplanation();
}

View File

@ -1,3 +1,6 @@
/**
* Classes related to the Document structure of Elasticsearch documents and search responses.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.elasticsearch.core.document;

View File

@ -61,6 +61,7 @@ abstract class AbstractQuery implements Query {
@Nullable private Integer trackTotalHitsUpTo;
@Nullable private Duration scrollTime;
@Nullable private TimeValue timeout;
private boolean explain = false;
@Override
@Nullable
@ -270,4 +271,16 @@ abstract class AbstractQuery implements Query {
public void setTimeout(@Nullable TimeValue timeout) {
this.timeout = timeout;
}
@Override
public boolean getExplain() {
return explain;
}
/**
* @param explain the explain flag on the query.
*/
public void setExplain(boolean explain) {
this.explain = explain;
}
}

View File

@ -285,4 +285,12 @@ public interface Query {
*/
@Nullable
TimeValue getTimeout();
/**
* @return {@literal true} when the query has the eplain parameter set, defaults to {@literal false}
* @since 4.2
*/
default boolean getExplain() {
return false;
}
}

View File

@ -21,6 +21,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.elasticsearch.action.get.GetResponse;
@ -31,9 +32,11 @@ import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchShardTarget;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.DocumentAdapters;
import org.springframework.data.elasticsearch.core.document.Explanation;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
/**
@ -236,4 +239,27 @@ public class DocumentAdaptersUnitTests {
assertThat(document.hasPrimaryTerm()).isTrue();
assertThat(document.getPrimaryTerm()).isEqualTo(2);
}
@Test // #725
@DisplayName("should adapt returned explanations")
void shouldAdaptReturnedExplanations() {
SearchHit searchHit = new SearchHit(42);
searchHit.explanation(org.apache.lucene.search.Explanation.match( //
3.14, //
"explanation 3.14", //
Collections.singletonList(org.apache.lucene.search.Explanation.noMatch( //
"explanation noMatch", //
Collections.emptyList()))));
SearchDocument searchDocument = DocumentAdapters.from(searchHit);
Explanation explanation = searchDocument.getExplanation();
assertThat(explanation).isNotNull();
assertThat(explanation.isMatch()).isTrue();
assertThat(explanation.getValue()).isEqualTo(3.14);
assertThat(explanation.getDescription()).isEqualTo("explanation 3.14");
List<Explanation> details = explanation.getDetails();
assertThat(details).containsExactly(new Explanation(false, 0.0, "explanation noMatch", Collections.emptyList()));
}
}

View File

@ -33,7 +33,16 @@ import java.lang.Double;
import java.lang.Integer;
import java.lang.Long;
import java.lang.Object;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -74,6 +83,7 @@ import org.springframework.data.elasticsearch.annotations.JoinTypeRelation;
import org.springframework.data.elasticsearch.annotations.JoinTypeRelations;
import org.springframework.data.elasticsearch.annotations.MultiField;
import org.springframework.data.elasticsearch.annotations.ScriptedField;
import org.springframework.data.elasticsearch.core.document.Explanation;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.index.AliasAction;
import org.springframework.data.elasticsearch.core.index.AliasActionParameters;
@ -124,7 +134,6 @@ public abstract class ElasticsearchTemplateTests {
@Autowired protected ElasticsearchOperations operations;
protected IndexOperations indexOperations;
@BeforeEach
public void before() {
indexOperations = operations.indexOps(SampleEntity.class);
@ -1546,10 +1555,8 @@ public abstract class ElasticsearchTemplateTests {
final UpdateQuery updateQuery = UpdateQuery.builder(query)
.withScriptType(org.springframework.data.elasticsearch.core.ScriptType.INLINE)
.withScript("ctx._source['message'] = params['newMessage']")
.withLang("painless")
.withParams(Collections.singletonMap("newMessage", messageAfterUpdate))
.withAbortOnVersionConflict(true)
.withScript("ctx._source['message'] = params['newMessage']").withLang("painless")
.withParams(Collections.singletonMap("newMessage", messageAfterUpdate)).withAbortOnVersionConflict(true)
.build();
// when
@ -3619,7 +3626,7 @@ public abstract class ElasticsearchTemplateTests {
softly.assertAll();
}
@Test
@Test // DATAES-907
@DisplayName("should track total hits is off")
void shouldTrackTotalHitsIsOff() {
@ -3642,6 +3649,39 @@ public abstract class ElasticsearchTemplateTests {
softly.assertAll();
}
@Test // #725
@DisplayName("should not return explanation when not requested")
void shouldNotReturnExplanationWhenNotRequested() {
SampleEntity entity = SampleEntity.builder().id("42").message("a message with text").build();
operations.save(entity);
Criteria criteria = new Criteria("message").contains("with");
CriteriaQuery query = new CriteriaQuery(criteria);
SearchHits<SampleEntity> searchHits = operations.search(query, SampleEntity.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1L);
Explanation explanation = searchHits.getSearchHit(0).getExplanation();
assertThat(explanation).isNull();
}
@Test // #725
@DisplayName("should return explanation when requested")
void shouldReturnExplanationWhenRequested() {
SampleEntity entity = SampleEntity.builder().id("42").message("a message with text").build();
operations.save(entity);
Criteria criteria = new Criteria("message").contains("with");
CriteriaQuery query = new CriteriaQuery(criteria);
query.setExplain(true);
SearchHits<SampleEntity> searchHits = operations.search(query, SampleEntity.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1L);
Explanation explanation = searchHits.getSearchHit(0).getExplanation();
assertThat(explanation).isNotNull();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ -3836,5 +3876,4 @@ public abstract class ElasticsearchTemplateTests {
@JoinTypeRelation(parent = "question", children = { "answer" }) }) private JoinField<String> myJoinField;
@Field(type = Text) private String text;
}
}

View File

@ -25,6 +25,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.elasticsearch.core.document.Explanation;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@ -1058,6 +1059,45 @@ public class ReactiveElasticsearchTemplateIntegrationTests {
.expectNext(true) //
.verifyComplete(); //
}
@Test // #725
@DisplayName("should not return explanation when not requested")
void shouldNotReturnExplanationWhenNotRequested() {
ElasticsearchTemplateTests.SampleEntity entity = ElasticsearchTemplateTests.SampleEntity.builder().id("42").message("a message with text").build();
template.save(entity).as(StepVerifier::create).expectNextCount(1).verifyComplete();
Criteria criteria = new Criteria("message").contains("with");
CriteriaQuery query = new CriteriaQuery(criteria);
template.search(query, ElasticsearchTemplateTests.SampleEntity.class)
.as(StepVerifier::create)
.consumeNextWith(searchHit -> {
Explanation explanation = searchHit.getExplanation();
assertThat(explanation).isNull();
})
.verifyComplete();
}
@Test // #725
@DisplayName("should return explanation when requested")
void shouldReturnExplanationWhenRequested() {
ElasticsearchTemplateTests.SampleEntity entity = ElasticsearchTemplateTests.SampleEntity.builder().id("42").message("a message with text").build();
template.save(entity).as(StepVerifier::create).expectNextCount(1).verifyComplete();
Criteria criteria = new Criteria("message").contains("with");
CriteriaQuery query = new CriteriaQuery(criteria);
query.setExplain(true);
template.search(query, ElasticsearchTemplateTests.SampleEntity.class)
.as(StepVerifier::create)
.consumeNextWith(searchHit -> {
Explanation explanation = searchHit.getExplanation();
assertThat(explanation).isNotNull();
})
.verifyComplete();
}
// endregion
// region Helper functions