diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
index 63a0e0e98377..978de02bea3c 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
@@ -65,14 +65,15 @@ import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateReque
import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.delete.DeleteRequest;
+import org.elasticsearch.action.explain.ExplainRequest;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.MultiGetRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.ingest.DeletePipelineRequest;
-import org.elasticsearch.action.ingest.PutPipelineRequest;
import org.elasticsearch.action.ingest.GetPipelineRequest;
import org.elasticsearch.action.ingest.SimulatePipelineRequest;
+import org.elasticsearch.action.ingest.PutPipelineRequest;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.MultiSearchRequest;
import org.elasticsearch.action.search.SearchRequest;
@@ -618,6 +619,19 @@ final class RequestConverters {
return request;
}
+ static Request explain(ExplainRequest explainRequest) throws IOException {
+ Request request = new Request(HttpGet.METHOD_NAME,
+ endpoint(explainRequest.index(), explainRequest.type(), explainRequest.id(), "_explain"));
+
+ Params params = new Params(request);
+ params.withStoredFields(explainRequest.storedFields());
+ params.withFetchSourceContext(explainRequest.fetchSourceContext());
+ params.withRouting(explainRequest.routing());
+ params.withPreference(explainRequest.preference());
+ request.setEntity(createEntity(explainRequest, REQUEST_BODY_CONTENT_TYPE));
+ return request;
+ }
+
static Request fieldCaps(FieldCapabilitiesRequest fieldCapabilitiesRequest) {
Request request = new Request(HttpGet.METHOD_NAME, endpoint(fieldCapabilitiesRequest.indices(), "_field_caps"));
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java
index 6905cfdb8f71..7d9b02b06a11 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java
@@ -34,6 +34,8 @@ import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
+import org.elasticsearch.action.explain.ExplainRequest;
+import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
import org.elasticsearch.action.get.GetRequest;
@@ -614,6 +616,42 @@ public class RestHighLevelClient implements Closeable {
SearchTemplateResponse::fromXContent, listener, emptySet());
}
+ /**
+ * Executes a request using the Explain API.
+ * See Explain API on elastic.co
+ * @param explainRequest the request
+ * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+ * @return the response
+ * @throws IOException in case there is a problem sending the request or parsing back the response
+ */
+ public final ExplainResponse explain(ExplainRequest explainRequest, RequestOptions options) throws IOException {
+ return performRequest(explainRequest, RequestConverters::explain, options,
+ response -> {
+ CheckedFunction entityParser =
+ parser -> ExplainResponse.fromXContent(parser, convertExistsResponse(response));
+ return parseEntity(response.getEntity(), entityParser);
+ },
+ singleton(404));
+ }
+
+ /**
+ * Asynchronously executes a request using the Explain API.
+ *
+ * See Explain API on elastic.co
+ * @param explainRequest the request
+ * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+ * @param listener the listener to be notified upon request completion
+ */
+ public final void explainAsync(ExplainRequest explainRequest, RequestOptions options, ActionListener listener) {
+ performRequestAsync(explainRequest, RequestConverters::explain, options,
+ response -> {
+ CheckedFunction entityParser =
+ parser -> ExplainResponse.fromXContent(parser, convertExistsResponse(response));
+ return parseEntity(response.getEntity(), entityParser);
+ },
+ listener, singleton(404));
+ }
+
/**
* Executes a request using the Ranking Evaluation API.
* See Ranking Evaluation API
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
index b8714967b412..f2c4580e6e3a 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
@@ -68,6 +68,7 @@ import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryReques
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkShardRequest;
import org.elasticsearch.action.delete.DeleteRequest;
+import org.elasticsearch.action.explain.ExplainRequest;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.MultiGetRequest;
@@ -111,6 +112,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.RandomCreateIndexGenerator;
import org.elasticsearch.index.VersionType;
+import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.rankeval.PrecisionAtK;
import org.elasticsearch.index.rankeval.RankEvalRequest;
@@ -1418,6 +1420,49 @@ public class RequestConvertersTests extends ESTestCase {
}
}
+ public void testExplain() throws IOException {
+ String index = randomAlphaOfLengthBetween(3, 10);
+ String type = randomAlphaOfLengthBetween(3, 10);
+ String id = randomAlphaOfLengthBetween(3, 10);
+
+ ExplainRequest explainRequest = new ExplainRequest(index, type, id);
+ explainRequest.query(QueryBuilders.termQuery(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10)));
+
+ Map expectedParams = new HashMap<>();
+
+ if (randomBoolean()) {
+ String routing = randomAlphaOfLengthBetween(3, 10);
+ explainRequest.routing(routing);
+ expectedParams.put("routing", routing);
+ }
+ if (randomBoolean()) {
+ String preference = randomAlphaOfLengthBetween(3, 10);
+ explainRequest.preference(preference);
+ expectedParams.put("preference", preference);
+ }
+ if (randomBoolean()) {
+ String[] storedFields = generateRandomStringArray(10, 5, false);
+ String storedFieldsParams = randomFields(storedFields);
+ explainRequest.storedFields(storedFields);
+ expectedParams.put("stored_fields", storedFieldsParams);
+ }
+ if (randomBoolean()) {
+ randomizeFetchSourceContextParams(explainRequest::fetchSourceContext, expectedParams);
+ }
+
+ Request request = RequestConverters.explain(explainRequest);
+ StringJoiner endpoint = new StringJoiner("/", "/", "");
+ endpoint.add(index)
+ .add(type)
+ .add(id)
+ .add("_explain");
+
+ assertEquals(HttpGet.METHOD_NAME, request.getMethod());
+ assertEquals(endpoint.toString(), request.getEndpoint());
+ assertEquals(expectedParams, request.getParameters());
+ assertToXContentBody(explainRequest, request.getEntity());
+ }
+
public void testFieldCaps() {
// Create a random request.
String[] indices = randomIndicesNames(0, 5);
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java
index a87aec7c2cf8..b83cc263be95 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java
@@ -27,6 +27,8 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.nio.entity.NStringEntity;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchStatusException;
+import org.elasticsearch.action.explain.ExplainRequest;
+import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.fieldcaps.FieldCapabilities;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
@@ -44,6 +46,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.MatchQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.ScriptQueryBuilder;
import org.elasticsearch.index.query.TermsQueryBuilder;
import org.elasticsearch.join.aggregations.Children;
@@ -63,6 +66,7 @@ import org.elasticsearch.search.aggregations.matrix.stats.MatrixStats;
import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder;
import org.elasticsearch.search.aggregations.support.ValueType;
import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
@@ -135,7 +139,44 @@ public class SearchIT extends ESRestHighLevelClientTestCase {
client().performRequest(HttpPut.METHOD_NAME, "/index3/doc/5", Collections.emptyMap(), doc);
doc = new StringEntity("{\"field\":\"value2\"}", ContentType.APPLICATION_JSON);
client().performRequest(HttpPut.METHOD_NAME, "/index3/doc/6", Collections.emptyMap(), doc);
- client().performRequest(HttpPost.METHOD_NAME, "/index1,index2,index3/_refresh");
+
+ mappings = new StringEntity(
+ "{" +
+ " \"mappings\": {" +
+ " \"doc\": {" +
+ " \"properties\": {" +
+ " \"field1\": {" +
+ " \"type\": \"keyword\"," +
+ " \"store\": true" +
+ " }," +
+ " \"field2\": {" +
+ " \"type\": \"keyword\"," +
+ " \"store\": true" +
+ " }" +
+ " }" +
+ " }" +
+ " }" +
+ "}}",
+ ContentType.APPLICATION_JSON);
+ client().performRequest(HttpPut.METHOD_NAME, "/index4", Collections.emptyMap(), mappings);
+ doc = new StringEntity("{\"field1\":\"value1\", \"field2\":\"value2\"}", ContentType.APPLICATION_JSON);
+ client().performRequest(HttpPut.METHOD_NAME, "/index4/doc/1", Collections.emptyMap(), doc);
+ StringEntity aliasFilter = new StringEntity(
+ "{" +
+ " \"actions\" : [" +
+ " {" +
+ " \"add\" : {" +
+ " \"index\" : \"index4\"," +
+ " \"alias\" : \"alias4\"," +
+ " \"filter\" : { \"term\" : { \"field2\" : \"value1\" } }" +
+ " }" +
+ " }" +
+ " ]" +
+ "}",
+ ContentType.APPLICATION_JSON);
+ client().performRequest(HttpPost.METHOD_NAME, "/_aliases", Collections.emptyMap(), aliasFilter);
+
+ client().performRequest(HttpPost.METHOD_NAME, "/index1,index2,index3,index4/_refresh");
}
public void testSearchNoQuery() throws IOException {
@@ -835,6 +876,174 @@ public class SearchIT extends ESRestHighLevelClientTestCase {
assertToXContentEquivalent(expectedSource, actualSource, XContentType.JSON);
}
+ public void testExplain() throws IOException {
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index1", "doc", "1");
+ explainRequest.query(QueryBuilders.matchAllQuery());
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertThat(explainResponse.getIndex(), equalTo("index1"));
+ assertThat(explainResponse.getType(), equalTo("doc"));
+ assertThat(Integer.valueOf(explainResponse.getId()), equalTo(1));
+ assertTrue(explainResponse.isExists());
+ assertTrue(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertThat(explainResponse.getExplanation().getValue(), equalTo(1.0f));
+ assertNull(explainResponse.getGetResult());
+ }
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index1", "doc", "1");
+ explainRequest.query(QueryBuilders.termQuery("field", "value1"));
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertThat(explainResponse.getIndex(), equalTo("index1"));
+ assertThat(explainResponse.getType(), equalTo("doc"));
+ assertThat(Integer.valueOf(explainResponse.getId()), equalTo(1));
+ assertTrue(explainResponse.isExists());
+ assertTrue(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertThat(explainResponse.getExplanation().getValue(), greaterThan(0.0f));
+ assertNull(explainResponse.getGetResult());
+ }
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index1", "doc", "1");
+ explainRequest.query(QueryBuilders.termQuery("field", "value2"));
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertThat(explainResponse.getIndex(), equalTo("index1"));
+ assertThat(explainResponse.getType(), equalTo("doc"));
+ assertThat(Integer.valueOf(explainResponse.getId()), equalTo(1));
+ assertTrue(explainResponse.isExists());
+ assertFalse(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertNull(explainResponse.getGetResult());
+ }
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index1", "doc", "1");
+ explainRequest.query(QueryBuilders.boolQuery()
+ .must(QueryBuilders.termQuery("field", "value1"))
+ .must(QueryBuilders.termQuery("field", "value2")));
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertThat(explainResponse.getIndex(), equalTo("index1"));
+ assertThat(explainResponse.getType(), equalTo("doc"));
+ assertThat(Integer.valueOf(explainResponse.getId()), equalTo(1));
+ assertTrue(explainResponse.isExists());
+ assertFalse(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertThat(explainResponse.getExplanation().getDetails().length, equalTo(2));
+ assertNull(explainResponse.getGetResult());
+ }
+ }
+
+ public void testExplainNonExistent() throws IOException {
+ {
+ ExplainRequest explainRequest = new ExplainRequest("non_existent_index", "doc", "1");
+ explainRequest.query(QueryBuilders.matchQuery("field", "value"));
+ ElasticsearchException exception = expectThrows(ElasticsearchException.class,
+ () -> execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync));
+ assertThat(exception.status(), equalTo(RestStatus.NOT_FOUND));
+ assertThat(exception.getIndex().getName(), equalTo("non_existent_index"));
+ assertThat(exception.getDetailedMessage(),
+ containsString("Elasticsearch exception [type=index_not_found_exception, reason=no such index]"));
+ }
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index1", "doc", "999");
+ explainRequest.query(QueryBuilders.matchQuery("field", "value1"));
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertThat(explainResponse.getIndex(), equalTo("index1"));
+ assertThat(explainResponse.getType(), equalTo("doc"));
+ assertThat(explainResponse.getId(), equalTo("999"));
+ assertFalse(explainResponse.isExists());
+ assertFalse(explainResponse.isMatch());
+ assertFalse(explainResponse.hasExplanation());
+ assertNull(explainResponse.getGetResult());
+ }
+ }
+
+ public void testExplainWithStoredFields() throws IOException {
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index4", "doc", "1");
+ explainRequest.query(QueryBuilders.matchAllQuery());
+ explainRequest.storedFields(new String[]{"field1"});
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertTrue(explainResponse.isExists());
+ assertTrue(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertThat(explainResponse.getExplanation().getValue(), equalTo(1.0f));
+ assertTrue(explainResponse.getGetResult().isExists());
+ assertThat(explainResponse.getGetResult().getFields().keySet(), equalTo(Collections.singleton("field1")));
+ assertThat(explainResponse.getGetResult().getFields().get("field1").getValue().toString(), equalTo("value1"));
+ assertTrue(explainResponse.getGetResult().isSourceEmpty());
+ }
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index4", "doc", "1");
+ explainRequest.query(QueryBuilders.matchAllQuery());
+ explainRequest.storedFields(new String[]{"field1", "field2"});
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertTrue(explainResponse.isExists());
+ assertTrue(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertThat(explainResponse.getExplanation().getValue(), equalTo(1.0f));
+ assertTrue(explainResponse.getGetResult().isExists());
+ assertThat(explainResponse.getGetResult().getFields().keySet().size(), equalTo(2));
+ assertThat(explainResponse.getGetResult().getFields().get("field1").getValue().toString(), equalTo("value1"));
+ assertThat(explainResponse.getGetResult().getFields().get("field2").getValue().toString(), equalTo("value2"));
+ assertTrue(explainResponse.getGetResult().isSourceEmpty());
+ }
+ }
+
+ public void testExplainWithFetchSource() throws IOException {
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index4", "doc", "1");
+ explainRequest.query(QueryBuilders.matchAllQuery());
+ explainRequest.fetchSourceContext(new FetchSourceContext(true, new String[]{"field1"}, null));
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertTrue(explainResponse.isExists());
+ assertTrue(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertThat(explainResponse.getExplanation().getValue(), equalTo(1.0f));
+ assertTrue(explainResponse.getGetResult().isExists());
+ assertThat(explainResponse.getGetResult().getSource(), equalTo(Collections.singletonMap("field1", "value1")));
+ }
+ {
+ ExplainRequest explainRequest = new ExplainRequest("index4", "doc", "1");
+ explainRequest.query(QueryBuilders.matchAllQuery());
+ explainRequest.fetchSourceContext(new FetchSourceContext(true, null, new String[] {"field2"}));
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertTrue(explainResponse.isExists());
+ assertTrue(explainResponse.isMatch());
+ assertTrue(explainResponse.hasExplanation());
+ assertThat(explainResponse.getExplanation().getValue(), equalTo(1.0f));
+ assertTrue(explainResponse.getGetResult().isExists());
+ assertThat(explainResponse.getGetResult().getSource(), equalTo(Collections.singletonMap("field1", "value1")));
+ }
+ }
+
+ public void testExplainWithAliasFilter() throws IOException {
+ ExplainRequest explainRequest = new ExplainRequest("alias4", "doc", "1");
+ explainRequest.query(QueryBuilders.matchAllQuery());
+
+ ExplainResponse explainResponse = execute(explainRequest, highLevelClient()::explain, highLevelClient()::explainAsync);
+
+ assertTrue(explainResponse.isExists());
+ assertFalse(explainResponse.isMatch());
+ }
+
public void testFieldCaps() throws IOException {
FieldCapabilitiesRequest request = new FieldCapabilitiesRequest()
.indices("index1", "index2")
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java
index adc0fede1aa7..3e484b0c86d3 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java
@@ -19,12 +19,15 @@
package org.elasticsearch.client.documentation;
+import org.apache.lucene.search.Explanation;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.LatchedActionListener;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.explain.ExplainRequest;
+import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.fieldcaps.FieldCapabilities;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
@@ -47,10 +50,12 @@ import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.document.DocumentField;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
@@ -80,6 +85,7 @@ import org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.avg.Avg;
import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.profile.ProfileResult;
@@ -835,6 +841,85 @@ public class SearchDocumentationIT extends ESRestHighLevelClientTestCase {
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
+ public void testExplain() throws Exception {
+ indexSearchTestData();
+ RestHighLevelClient client = highLevelClient();
+
+ // tag::explain-request
+ ExplainRequest request = new ExplainRequest("contributors", "doc", "1");
+ request.query(QueryBuilders.termQuery("user", "tanguy"));
+ // end::explain-request
+
+ // tag::explain-request-routing
+ request.routing("routing"); // <1>
+ // end::explain-request-routing
+
+ // tag::explain-request-preference
+ request.preference("_local"); // <1>
+ // end::explain-request-preference
+
+ // tag::explain-request-source
+ request.fetchSourceContext(new FetchSourceContext(true, new String[]{"user"}, null)); // <1>
+ // end::explain-request-source
+
+ // tag::explain-request-stored-field
+ request.storedFields(new String[]{"user"}); // <1>
+ // end::explain-request-stored-field
+
+ // tag::explain-execute
+ ExplainResponse response = client.explain(request, RequestOptions.DEFAULT);
+ // end::explain-execute
+
+ // tag::explain-response
+ String index = response.getIndex(); // <1>
+ String type = response.getType(); // <2>
+ String id = response.getId(); // <3>
+ boolean exists = response.isExists(); // <4>
+ boolean match = response.isMatch(); // <5>
+ boolean hasExplanation = response.hasExplanation(); // <6>
+ Explanation explanation = response.getExplanation(); // <7>
+ GetResult getResult = response.getGetResult(); // <8>
+ // end::explain-response
+ assertThat(index, equalTo("contributors"));
+ assertThat(type, equalTo("doc"));
+ assertThat(id, equalTo("1"));
+ assertTrue(exists);
+ assertTrue(match);
+ assertTrue(hasExplanation);
+ assertNotNull(explanation);
+ assertNotNull(getResult);
+
+ // tag::get-result
+ Map source = getResult.getSource(); // <1>
+ Map fields = getResult.getFields(); // <2>
+ // end::get-result
+ assertThat(source, equalTo(Collections.singletonMap("user", "tanguy")));
+ assertThat(fields.get("user").getValue(), equalTo("tanguy"));
+
+ // tag::explain-execute-listener
+ ActionListener listener = new ActionListener() {
+ @Override
+ public void onResponse(ExplainResponse explainResponse) {
+ // <1>
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ // <2>
+ }
+ };
+ // end::explain-execute-listener
+
+ CountDownLatch latch = new CountDownLatch(1);
+ listener = new LatchedActionListener<>(listener, latch);
+
+ // tag::explain-execute-async
+ client.explainAsync(request, RequestOptions.DEFAULT, listener); // <1>
+ // end::explain-execute-async
+
+ assertTrue(latch.await(30L, TimeUnit.SECONDS));
+ }
+
public void testFieldCaps() throws Exception {
indexSearchTestData();
RestHighLevelClient client = highLevelClient();
@@ -1046,7 +1131,7 @@ public class SearchDocumentationIT extends ESRestHighLevelClientTestCase {
assertTrue(authorsResponse.isAcknowledged());
CreateIndexRequest reviewersRequest = new CreateIndexRequest("contributors")
- .mapping("doc", "user", "type=keyword");
+ .mapping("doc", "user", "type=keyword,store=true");
CreateIndexResponse reviewersResponse = highLevelClient().indices().create(reviewersRequest, RequestOptions.DEFAULT);
assertTrue(reviewersResponse.isAcknowledged());
diff --git a/docs/java-rest/high-level/search/explain.asciidoc b/docs/java-rest/high-level/search/explain.asciidoc
new file mode 100644
index 000000000000..9e55ad77ea20
--- /dev/null
+++ b/docs/java-rest/high-level/search/explain.asciidoc
@@ -0,0 +1,113 @@
+[[java-rest-high-explain]]
+=== Explain API
+
+The explain api computes a score explanation for a query and a specific document.
+This can give useful feedback whether a document matches or didn’t match a specific query.
+
+[[java-rest-high-explain-request]]
+==== Explain Request
+
+An `ExplainRequest` expects an `index`, a `type` and an `id` to specify a certain document,
+and a query represented by `QueryBuilder` to run against it (the way of <>).
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-request]
+--------------------------------------------------
+
+===== Optional arguments
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-request-routing]
+--------------------------------------------------
+<1> Set a routing parameter
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-request-preference]
+--------------------------------------------------
+<1> Use the preference parameter e.g. to execute the search to prefer local
+shards. The default is to randomize across shards.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-request-source]
+--------------------------------------------------
+<1> Set to true to retrieve the _source of the document explained. You can also
+retrieve part of the document by using _source_include & _source_exclude
+(see <> for more details)
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-request-stored-field]
+--------------------------------------------------
+<1> Allows to control which stored fields to return as part of the document explained
+(requires the field to be stored separately in the mappings).
+
+[[java-rest-high-explain-sync]]
+==== Synchronous Execution
+
+The `explain` method executes the request synchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-execute]
+--------------------------------------------------
+
+[[java-rest-high-explain-async]]
+==== Asynchronous Execution
+
+The `explainAsync` method executes the request asynchronously,
+calling the provided `ActionListener` when the response is ready:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-execute-async]
+--------------------------------------------------
+<1> The `ExplainRequest` to execute and the `ActionListener` to use when
+the execution completes.
+
+The asynchronous method does not block and returns immediately. Once the request
+completes, the `ActionListener` is called back using the `onResponse` method
+if the execution successfully completed or using the `onFailure` method if
+it failed.
+
+A typical listener for `ExplainResponse` is constructed as follows:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-execute-listener]
+--------------------------------------------------
+<1> Called when the execution is successfully completed.
+<2> Called when the whole `FieldCapabilitiesRequest` fails.
+
+[[java-rest-high-explain-response]]
+==== ExplainResponse
+
+The `ExplainResponse` contains the following information:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[explain-response]
+--------------------------------------------------
+<1> The index name of the explained document.
+<2> The type name of the explained document.
+<3> The id of the explained document.
+<4> Indicates whether or not the explained document exists.
+<5> Indicates whether or not there is a match between the explained document and
+the provided query (the `match` is retrieved from the lucene `Explanation` behind the scenes
+if the lucene `Explanation` models a match, it returns `true`, otherwise it returns `false`).
+<6> Indicates whether or not there exists a lucene `Explanation` for this request.
+<7> Get the lucene `Explanation` object if there exists.
+<8> Get the `GetResult` object if the `_source` or the stored fields are retrieved.
+
+The `GetResult` contains two maps internally to store the fetched `_source` and stored fields.
+You can use the following methods to get them:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SearchDocumentationIT.java[get-result]
+--------------------------------------------------
+<1> Retrieve the `_source` as a map.
+<2> Retrieve the specified stored fields as a map.
diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc
index 9ed54db81755..fa904b81cc41 100644
--- a/docs/java-rest/high-level/supported-apis.asciidoc
+++ b/docs/java-rest/high-level/supported-apis.asciidoc
@@ -35,6 +35,7 @@ The Java High Level REST Client supports the following Search APIs:
* <>
* <>
* <>
+* <>
include::search/search.asciidoc[]
include::search/scroll.asciidoc[]
@@ -42,6 +43,7 @@ include::search/multi-search.asciidoc[]
include::search/search-template.asciidoc[]
include::search/field-caps.asciidoc[]
include::search/rank-eval.asciidoc[]
+include::search/explain.asciidoc[]
== Miscellaneous APIs
diff --git a/server/src/main/java/org/elasticsearch/action/explain/ExplainRequest.java b/server/src/main/java/org/elasticsearch/action/explain/ExplainRequest.java
index 5d8ca27657f9..6fdf355c0670 100644
--- a/server/src/main/java/org/elasticsearch/action/explain/ExplainRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/explain/ExplainRequest.java
@@ -22,9 +22,12 @@ package org.elasticsearch.action.explain;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.ValidateActions;
import org.elasticsearch.action.support.single.shard.SingleShardRequest;
+import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.internal.AliasFilter;
@@ -34,7 +37,9 @@ import java.io.IOException;
/**
* Explain request encapsulating the explain query and document identifier to get an explanation for.
*/
-public class ExplainRequest extends SingleShardRequest {
+public class ExplainRequest extends SingleShardRequest implements ToXContentObject {
+
+ private static final ParseField QUERY_FIELD = new ParseField("query");
private String type = "_all";
private String id;
@@ -186,4 +191,12 @@ public class ExplainRequest extends SingleShardRequest {
out.writeOptionalWriteable(fetchSourceContext);
out.writeVLong(nowInMillis);
}
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ builder.field(QUERY_FIELD.getPreferredName(), query);
+ builder.endObject();
+ return builder;
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java b/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java
index fb1fc3db1ea1..0dc75e41439d 100644
--- a/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java
+++ b/server/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java
@@ -21,11 +21,19 @@ package org.elasticsearch.action.explain;
import org.apache.lucene.search.Explanation;
import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.StatusToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.get.GetResult;
+import org.elasticsearch.rest.RestStatus;
import java.io.IOException;
+import java.util.Collection;
+import java.util.Objects;
import static org.elasticsearch.common.lucene.Lucene.readExplanation;
import static org.elasticsearch.common.lucene.Lucene.writeExplanation;
@@ -33,7 +41,17 @@ import static org.elasticsearch.common.lucene.Lucene.writeExplanation;
/**
* Response containing the score explanation.
*/
-public class ExplainResponse extends ActionResponse {
+public class ExplainResponse extends ActionResponse implements StatusToXContentObject {
+
+ private static final ParseField _INDEX = new ParseField("_index");
+ private static final ParseField _TYPE = new ParseField("_type");
+ private static final ParseField _ID = new ParseField("_id");
+ private static final ParseField MATCHED = new ParseField("matched");
+ private static final ParseField EXPLANATION = new ParseField("explanation");
+ private static final ParseField VALUE = new ParseField("value");
+ private static final ParseField DESCRIPTION = new ParseField("description");
+ private static final ParseField DETAILS = new ParseField("details");
+ private static final ParseField GET = new ParseField("get");
private String index;
private String type;
@@ -94,6 +112,11 @@ public class ExplainResponse extends ActionResponse {
return getResult;
}
+ @Override
+ public RestStatus status() {
+ return exists ? RestStatus.OK : RestStatus.NOT_FOUND;
+ }
+
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
@@ -129,4 +152,90 @@ public class ExplainResponse extends ActionResponse {
getResult.writeTo(out);
}
}
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("explain", true,
+ (arg, exists) -> new ExplainResponse((String) arg[0], (String) arg[1], (String) arg[2], exists, (Explanation) arg[3],
+ (GetResult) arg[4]));
+
+ static {
+ PARSER.declareString(ConstructingObjectParser.constructorArg(), _INDEX);
+ PARSER.declareString(ConstructingObjectParser.constructorArg(), _TYPE);
+ PARSER.declareString(ConstructingObjectParser.constructorArg(), _ID);
+ final ConstructingObjectParser explanationParser = new ConstructingObjectParser<>("explanation", true,
+ arg -> {
+ if ((float) arg[0] > 0) {
+ return Explanation.match((float) arg[0], (String) arg[1], (Collection) arg[2]);
+ } else {
+ return Explanation.noMatch((String) arg[1], (Collection) arg[2]);
+ }
+ });
+ explanationParser.declareFloat(ConstructingObjectParser.constructorArg(), VALUE);
+ explanationParser.declareString(ConstructingObjectParser.constructorArg(), DESCRIPTION);
+ explanationParser.declareObjectArray(ConstructingObjectParser.constructorArg(), explanationParser, DETAILS);
+ PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), explanationParser, EXPLANATION);
+ PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> GetResult.fromXContentEmbedded(p), GET);
+ }
+
+ public static ExplainResponse fromXContent(XContentParser parser, boolean exists) {
+ return PARSER.apply(parser, exists);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ builder.field(_INDEX.getPreferredName(), index);
+ builder.field(_TYPE.getPreferredName(), type);
+ builder.field(_ID.getPreferredName(), id);
+ builder.field(MATCHED.getPreferredName(), isMatch());
+ if (hasExplanation()) {
+ builder.startObject(EXPLANATION.getPreferredName());
+ buildExplanation(builder, explanation);
+ builder.endObject();
+ }
+ if (getResult != null) {
+ builder.startObject(GET.getPreferredName());
+ getResult.toXContentEmbedded(builder, params);
+ builder.endObject();
+ }
+ builder.endObject();
+ return builder;
+ }
+
+ private void buildExplanation(XContentBuilder builder, Explanation explanation) throws IOException {
+ builder.field(VALUE.getPreferredName(), explanation.getValue());
+ builder.field(DESCRIPTION.getPreferredName(), explanation.getDescription());
+ Explanation[] innerExps = explanation.getDetails();
+ if (innerExps != null) {
+ builder.startArray(DETAILS.getPreferredName());
+ for (Explanation exp : innerExps) {
+ builder.startObject();
+ buildExplanation(builder, exp);
+ builder.endObject();
+ }
+ builder.endArray();
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ExplainResponse other = (ExplainResponse) obj;
+ return index.equals(other.index)
+ && type.equals(other.type)
+ && id.equals(other.id)
+ && Objects.equals(explanation, other.explanation)
+ && getResult.isExists() == other.getResult.isExists()
+ && Objects.equals(getResult.sourceAsMap(), other.getResult.sourceAsMap())
+ && Objects.equals(getResult.getFields(), other.getResult.getFields());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(index, type, id, explanation, getResult.isExists(), getResult.sourceAsMap(), getResult.getFields());
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestExplainAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestExplainAction.java
index b0adc27f447f..d0196702d07e 100644
--- a/server/src/main/java/org/elasticsearch/rest/action/search/RestExplainAction.java
+++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestExplainAction.java
@@ -19,30 +19,22 @@
package org.elasticsearch.rest.action.search;
-import org.apache.lucene.search.Explanation;
import org.elasticsearch.action.explain.ExplainRequest;
-import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.rest.BaseRestHandler;
-import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
-import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.action.RestActions;
-import org.elasticsearch.rest.action.RestBuilderListener;
+import org.elasticsearch.rest.action.RestStatusToXContentListener;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import java.io.IOException;
import static org.elasticsearch.rest.RestRequest.Method.GET;
import static org.elasticsearch.rest.RestRequest.Method.POST;
-import static org.elasticsearch.rest.RestStatus.NOT_FOUND;
-import static org.elasticsearch.rest.RestStatus.OK;
/**
* Rest action for computing a score explanation for specific documents.
@@ -89,57 +81,6 @@ public class RestExplainAction extends BaseRestHandler {
explainRequest.fetchSourceContext(FetchSourceContext.parseFromRestRequest(request));
- return channel -> client.explain(explainRequest, new RestBuilderListener(channel) {
- @Override
- public RestResponse buildResponse(ExplainResponse response, XContentBuilder builder) throws Exception {
- builder.startObject();
- builder.field(Fields._INDEX, response.getIndex())
- .field(Fields._TYPE, response.getType())
- .field(Fields._ID, response.getId())
- .field(Fields.MATCHED, response.isMatch());
-
- if (response.hasExplanation()) {
- builder.startObject(Fields.EXPLANATION);
- buildExplanation(builder, response.getExplanation());
- builder.endObject();
- }
- GetResult getResult = response.getGetResult();
- if (getResult != null) {
- builder.startObject(Fields.GET);
- response.getGetResult().toXContentEmbedded(builder, request);
- builder.endObject();
- }
- builder.endObject();
- return new BytesRestResponse(response.isExists() ? OK : NOT_FOUND, builder);
- }
-
- private void buildExplanation(XContentBuilder builder, Explanation explanation) throws IOException {
- builder.field(Fields.VALUE, explanation.getValue());
- builder.field(Fields.DESCRIPTION, explanation.getDescription());
- Explanation[] innerExps = explanation.getDetails();
- if (innerExps != null) {
- builder.startArray(Fields.DETAILS);
- for (Explanation exp : innerExps) {
- builder.startObject();
- buildExplanation(builder, exp);
- builder.endObject();
- }
- builder.endArray();
- }
- }
- });
- }
-
- static class Fields {
- static final String _INDEX = "_index";
- static final String _TYPE = "_type";
- static final String _ID = "_id";
- static final String MATCHED = "matched";
- static final String EXPLANATION = "explanation";
- static final String VALUE = "value";
- static final String DESCRIPTION = "description";
- static final String DETAILS = "details";
- static final String GET = "get";
-
+ return channel -> client.explain(explainRequest, new RestStatusToXContentListener<>(channel));
}
}
diff --git a/server/src/test/java/org/elasticsearch/action/ExplainRequestTests.java b/server/src/test/java/org/elasticsearch/action/explain/ExplainRequestTests.java
similarity index 97%
rename from server/src/test/java/org/elasticsearch/action/ExplainRequestTests.java
rename to server/src/test/java/org/elasticsearch/action/explain/ExplainRequestTests.java
index 9f68d28b4422..be636e7d9875 100644
--- a/server/src/test/java/org/elasticsearch/action/ExplainRequestTests.java
+++ b/server/src/test/java/org/elasticsearch/action/explain/ExplainRequestTests.java
@@ -16,9 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.elasticsearch.action;
+package org.elasticsearch.action.explain;
-import org.elasticsearch.action.explain.ExplainRequest;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
diff --git a/server/src/test/java/org/elasticsearch/action/explain/ExplainResponseTests.java b/server/src/test/java/org/elasticsearch/action/explain/ExplainResponseTests.java
new file mode 100644
index 000000000000..ca5c35ccab3e
--- /dev/null
+++ b/server/src/test/java/org/elasticsearch/action/explain/ExplainResponseTests.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you 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
+ *
+ * http://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.elasticsearch.action.explain;
+
+import org.apache.lucene.search.Explanation;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.document.DocumentField;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.get.GetResult;
+import org.elasticsearch.test.AbstractStreamableXContentTestCase;
+import org.elasticsearch.test.RandomObjects;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.hamcrest.Matchers.equalTo;
+
+public class ExplainResponseTests extends AbstractStreamableXContentTestCase {
+ @Override
+ protected ExplainResponse doParseInstance(XContentParser parser) throws IOException {
+ return ExplainResponse.fromXContent(parser, randomBoolean());
+ }
+
+ @Override
+ protected ExplainResponse createBlankInstance() {
+ return new ExplainResponse();
+ }
+
+ @Override
+ protected ExplainResponse createTestInstance() {
+ String index = randomAlphaOfLength(5);
+ String type = randomAlphaOfLength(5);
+ String id = String.valueOf(randomIntBetween(1,100));
+ boolean exist = randomBoolean();
+ Explanation explanation = randomExplanation(randomExplanation(randomExplanation()), randomExplanation());
+ String fieldName = randomAlphaOfLength(10);
+ List