From 616703b88063ec5f9176354562e4da2a5628a11a Mon Sep 17 00:00:00 2001 From: Yu Date: Wed, 27 Jun 2018 21:20:56 +0800 Subject: [PATCH] Add explain API to high-level REST client (#31387) Relates to #27205 --- .../client/RequestConverters.java | 16 +- .../client/RestHighLevelClient.java | 38 ++++ .../client/RequestConvertersTests.java | 45 ++++ .../org/elasticsearch/client/SearchIT.java | 211 +++++++++++++++++- .../documentation/SearchDocumentationIT.java | 87 +++++++- .../high-level/search/explain.asciidoc | 113 ++++++++++ .../high-level/supported-apis.asciidoc | 2 + .../action/explain/ExplainRequest.java | 15 +- .../action/explain/ExplainResponse.java | 111 ++++++++- .../rest/action/search/RestExplainAction.java | 63 +----- .../{ => explain}/ExplainRequestTests.java | 3 +- .../action/explain/ExplainResponseTests.java | 127 +++++++++++ 12 files changed, 763 insertions(+), 68 deletions(-) create mode 100644 docs/java-rest/high-level/search/explain.asciidoc rename server/src/test/java/org/elasticsearch/action/{ => explain}/ExplainRequestTests.java (97%) create mode 100644 server/src/test/java/org/elasticsearch/action/explain/ExplainResponseTests.java 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 63a0e0e9837..978de02bea3 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 6905cfdb8f7..7d9b02b06a1 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 b8714967b41..f2c4580e6e3 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 a87aec7c2cf..b83cc263be9 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 adc0fede1aa..3e484b0c86d 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 00000000000..9e55ad77ea2 --- /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 9ed54db8175..fa904b81cc4 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 5d8ca27657f..6fdf355c067 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 fb1fc3db1ea..0dc75e41439 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 b0adc27f447..d0196702d07 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 9f68d28b442..be636e7d987 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 00000000000..ca5c35ccab3 --- /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 values = Arrays.asList(randomAlphaOfLengthBetween(3, 10), randomInt(), randomLong(), randomDouble(), randomBoolean()); + GetResult getResult = new GetResult(randomAlphaOfLengthBetween(3, 10), + randomAlphaOfLengthBetween(3, 10), + randomAlphaOfLengthBetween(3, 10), + randomNonNegativeLong(), + true, + RandomObjects.randomSource(random()), + singletonMap(fieldName, new DocumentField(fieldName, values))); + return new ExplainResponse(index, type, id, exist, explanation, getResult); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return field -> field.equals("get") || field.startsWith("get.fields") || field.startsWith("get._source"); + } + + public void testToXContent() throws IOException { + String index = "index"; + String type = "type"; + String id = "1"; + boolean exist = true; + Explanation explanation = Explanation.match(1.0f, "description", Collections.emptySet()); + GetResult getResult = new GetResult(null, null, null, -1, true, new BytesArray("{ \"field1\" : " + + "\"value1\", \"field2\":\"value2\"}"), singletonMap("field1", new DocumentField("field1", + singletonList("value1")))); + ExplainResponse response = new ExplainResponse(index, type, id, exist, explanation, getResult); + + XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + + String generatedResponse = BytesReference.bytes(builder).utf8ToString().replaceAll("\\s+", ""); + + String expectedResponse = + ("{\n" + + " \"_index\":\"index\",\n" + + " \"_type\":\"type\",\n" + + " \"_id\":\"1\",\n" + + " \"matched\":true,\n" + + " \"explanation\":{\n" + + " \"value\":1.0,\n" + + " \"description\":\"description\",\n" + + " \"details\":[]\n" + + " },\n" + + " \"get\":{\n" + + " \"found\":true,\n" + + " \"_source\":{\n" + + " \"field1\":\"value1\",\n" + + " \"field2\":\"value2\"\n" + + " },\n" + + " \"fields\":{\n" + + " \"field1\":[\n" + + " \"value1\"\n" + + " ]\n" + + " }\n" + + " }\n" + + "}").replaceAll("\\s+", ""); + assertThat(expectedResponse, equalTo(generatedResponse)); + } + + private static Explanation randomExplanation(Explanation... explanations) { + return Explanation.match(randomFloat(), randomAlphaOfLengthBetween(1, 10), + explanations.length > 0 ? explanations : new Explanation[0]); + } +}