Add explain API to high-level REST client (#31387)

Relates to #27205
This commit is contained in:
Yu 2018-06-27 21:20:56 +08:00 committed by Luca Cavanna
parent b7246199db
commit 616703b880
12 changed files with 763 additions and 68 deletions

View File

@ -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"));

View File

@ -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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html">Explain API on elastic.co</a>
* @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<XContentParser, ExplainResponse, IOException> entityParser =
parser -> ExplainResponse.fromXContent(parser, convertExistsResponse(response));
return parseEntity(response.getEntity(), entityParser);
},
singleton(404));
}
/**
* Asynchronously executes a request using the Explain API.
*
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html">Explain API on elastic.co</a>
* @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<ExplainResponse> listener) {
performRequestAsync(explainRequest, RequestConverters::explain, options,
response -> {
CheckedFunction<XContentParser, ExplainResponse, IOException> entityParser =
parser -> ExplainResponse.fromXContent(parser, convertExistsResponse(response));
return parseEntity(response.getEntity(), entityParser);
},
listener, singleton(404));
}
/**
* Executes a request using the Ranking Evaluation API.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html">Ranking Evaluation API

View File

@ -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<String, String> 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);

View File

@ -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")

View File

@ -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<String, Object> source = getResult.getSource(); // <1>
Map<String, DocumentField> 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<ExplainResponse> listener = new ActionListener<ExplainResponse>() {
@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());

View File

@ -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 didnt 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 <<java-rest-high-query-builders, building queries>>).
["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 <<java-rest-high-document-get-request-optional-arguments, Get API>> 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.

View File

@ -35,6 +35,7 @@ The Java High Level REST Client supports the following Search APIs:
* <<java-rest-high-multi-search>>
* <<java-rest-high-field-caps>>
* <<java-rest-high-rank-eval>>
* <<java-rest-high-explain>>
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

View File

@ -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<ExplainRequest> {
public class ExplainRequest extends SingleShardRequest<ExplainRequest> 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<ExplainRequest> {
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;
}
}

View File

@ -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<ExplainResponse, Boolean> 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<Explanation, Boolean> explanationParser = new ConstructingObjectParser<>("explanation", true,
arg -> {
if ((float) arg[0] > 0) {
return Explanation.match((float) arg[0], (String) arg[1], (Collection<Explanation>) arg[2]);
} else {
return Explanation.noMatch((String) arg[1], (Collection<Explanation>) 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());
}
}

View File

@ -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<ExplainResponse>(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));
}
}

View File

@ -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;

View File

@ -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<ExplainResponse> {
@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<Object> 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<String> 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]);
}
}