From b8ad84edc8090e471a7c5431d2e25054e8c763dd Mon Sep 17 00:00:00 2001 From: javanna Date: Tue, 16 May 2017 18:43:58 +0200 Subject: [PATCH 1/3] Add fromXContent method to SearchResponse SearchResponse#fromXContent allows to parse a search response, including search hits, aggregations, suggestions and profile results. Only the aggs that we can parse today are supported (which means all of them but a couple that are left to support). SearchResponseTests reuses the existing test infra to randomize aggregations, suggestions and profile response. Relates to #23331 --- .../action/search/SearchResponse.java | 105 +++++++++++- .../rest/action/RestActions.java | 17 +- .../org/elasticsearch/search/SearchHits.java | 8 +- .../elasticsearch/search/suggest/Suggest.java | 2 +- .../action/search/SearchResponseTests.java | 160 ++++++++++++++++++ .../aggregations/AggregationsTests.java | 2 +- .../SearchProfileShardResultsTests.java | 2 +- .../search/suggest/SuggestTests.java | 17 +- .../search/suggest/SuggestionTests.java | 4 +- 9 files changed, 294 insertions(+), 23 deletions(-) create mode 100644 core/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java diff --git a/core/src/main/java/org/elasticsearch/action/search/SearchResponse.java b/core/src/main/java/org/elasticsearch/action/search/SearchResponse.java index 3e83996c7c630..93505e44f120f 100644 --- a/core/src/main/java/org/elasticsearch/action/search/SearchResponse.java +++ b/core/src/main/java/org/elasticsearch/action/search/SearchResponse.java @@ -21,30 +21,43 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.Nullable; +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.unit.TimeValue; import org.elasticsearch.common.xcontent.StatusToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestActions; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.internal.InternalSearchResponse; import org.elasticsearch.search.profile.ProfileShardResult; +import org.elasticsearch.search.profile.SearchProfileShardResults; import org.elasticsearch.search.suggest.Suggest; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static org.elasticsearch.action.search.ShardSearchFailure.readShardSearchFailure; +import static org.elasticsearch.common.xcontent.XContentParserUtils.*; + /** * A response of a search request. */ public class SearchResponse extends ActionResponse implements StatusToXContentObject { + private static final ParseField SCROLL_ID = new ParseField("_scroll_id"); + private static final ParseField TOOK = new ParseField("took"); + private static final ParseField TIMED_OUT = new ParseField("timed_out"); + private static final ParseField TERMINATED_EARLY = new ParseField("terminated_early"); + private static final ParseField NUM_REDUCE_PHASES = new ParseField("num_reduce_phases"); + private SearchResponseSections internalResponse; private String scrollId; @@ -175,7 +188,8 @@ public void scrollId(String scrollId) { * * @return The profile results or an empty map */ - @Nullable public Map getProfileResults() { + @Nullable + public Map getProfileResults() { return internalResponse.profile(); } @@ -189,15 +203,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException { if (scrollId != null) { - builder.field("_scroll_id", scrollId); + builder.field(SCROLL_ID.getPreferredName(), scrollId); } - builder.field("took", tookInMillis); - builder.field("timed_out", isTimedOut()); + builder.field(TOOK.getPreferredName(), tookInMillis); + builder.field(TIMED_OUT.getPreferredName(), isTimedOut()); if (isTerminatedEarly() != null) { - builder.field("terminated_early", isTerminatedEarly()); + builder.field(TERMINATED_EARLY.getPreferredName(), isTerminatedEarly()); } if (getNumReducePhases() != 1) { - builder.field("num_reduce_phases", getNumReducePhases()); + builder.field(NUM_REDUCE_PHASES.getPreferredName(), getNumReducePhases()); } RestActions.buildBroadcastShardsHeader(builder, params, getTotalShards(), getSuccessfulShards(), getFailedShards(), getShardFailures()); @@ -205,6 +219,85 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t return builder; } + public static SearchResponse fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token; + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation); + String currentFieldName = null; + SearchHits hits = null; + Aggregations aggs = null; + Suggest suggest = null; + SearchProfileShardResults profile = null; + boolean timedOut = false; + Boolean terminatedEarly = null; + int numReducePhases = 1; + long tookInMillis = -1; + int successfulShards = -1; + int totalShards = -1; + String scrollId = null; + List failures = new ArrayList<>(); + while((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (SCROLL_ID.match(currentFieldName)) { + scrollId = parser.text(); + } else if (TOOK.match(currentFieldName)) { + tookInMillis = parser.longValue(); + } else if (TIMED_OUT.match(currentFieldName)) { + timedOut = parser.booleanValue(); + } else if (TERMINATED_EARLY.match(currentFieldName)) { + terminatedEarly = parser.booleanValue(); + } else if (NUM_REDUCE_PHASES.match(currentFieldName)) { + numReducePhases = parser.intValue(); + } else { + throwUnknownField(currentFieldName, parser.getTokenLocation()); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (SearchHits.Fields.HITS.equals(currentFieldName)) { + hits = SearchHits.fromXContent(parser); + } else if (Aggregations.AGGREGATIONS_FIELD.equals(currentFieldName)) { + aggs = Aggregations.fromXContent(parser); + } else if (Suggest.NAME.equals(currentFieldName)) { + suggest = Suggest.fromXContent(parser); + } else if (SearchProfileShardResults.PROFILE_FIELD.equals(currentFieldName)) { + profile = SearchProfileShardResults.fromXContent(parser); + } else if (RestActions._SHARDS_FIELD.match(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (RestActions.FAILED_FIELD.match(currentFieldName)) { + parser.intValue(); // we don't need it but need to consume it + } else if (RestActions.SUCCESSFUL_FIELD.match(currentFieldName)) { + successfulShards = parser.intValue(); + } else if (RestActions.TOTAL_FIELD.match(currentFieldName)) { + totalShards = parser.intValue(); + } else { + throwUnknownField(currentFieldName, parser.getTokenLocation()); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (RestActions.FAILURES_FIELD.match(currentFieldName)) { + while((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + failures.add(ShardSearchFailure.fromXContent(parser)); + } + } else { + throwUnknownField(currentFieldName, parser.getTokenLocation()); + } + } else { + throwUnknownToken(token, parser.getTokenLocation()); + } + } + } else { + throwUnknownField(currentFieldName, parser.getTokenLocation()); + } + } + } + SearchResponseSections searchResponseSections = new SearchResponseSections(hits, aggs, suggest, timedOut, terminatedEarly, + profile, numReducePhases); + return new SearchResponse(searchResponseSections, scrollId, totalShards, successfulShards, tookInMillis, + failures.toArray(new ShardSearchFailure[failures.size()])); + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); diff --git a/core/src/main/java/org/elasticsearch/rest/action/RestActions.java b/core/src/main/java/org/elasticsearch/rest/action/RestActions.java index 74836b1dc2d5b..15199c5a926e3 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/RestActions.java +++ b/core/src/main/java/org/elasticsearch/rest/action/RestActions.java @@ -25,6 +25,7 @@ import org.elasticsearch.action.support.broadcast.BroadcastResponse; import org.elasticsearch.action.support.nodes.BaseNodeResponse; import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContent.Params; @@ -46,6 +47,12 @@ public class RestActions { + public static final ParseField _SHARDS_FIELD = new ParseField("_shards"); + public static final ParseField TOTAL_FIELD = new ParseField("total"); + public static final ParseField SUCCESSFUL_FIELD = new ParseField("successful"); + public static final ParseField FAILED_FIELD = new ParseField("failed"); + public static final ParseField FAILURES_FIELD = new ParseField("failures"); + public static long parseVersion(RestRequest request) { if (request.hasParam("version")) { return request.paramAsLong("version", Versions.MATCH_ANY); @@ -71,12 +78,12 @@ public static void buildBroadcastShardsHeader(XContentBuilder builder, Params pa public static void buildBroadcastShardsHeader(XContentBuilder builder, Params params, int total, int successful, int failed, ShardOperationFailedException[] shardFailures) throws IOException { - builder.startObject("_shards"); - builder.field("total", total); - builder.field("successful", successful); - builder.field("failed", failed); + builder.startObject(_SHARDS_FIELD.getPreferredName()); + builder.field(TOTAL_FIELD.getPreferredName(), total); + builder.field(SUCCESSFUL_FIELD.getPreferredName(), successful); + builder.field(FAILED_FIELD.getPreferredName(), failed); if (shardFailures != null && shardFailures.length > 0) { - builder.startArray("failures"); + builder.startArray(FAILURES_FIELD.getPreferredName()); final boolean group = params.paramAsBoolean("group_shard_failures", true); // we group by default for (ShardOperationFailedException shardFailure : group ? ExceptionsHelper.groupBy(shardFailures) : shardFailures) { builder.startObject(); diff --git a/core/src/main/java/org/elasticsearch/search/SearchHits.java b/core/src/main/java/org/elasticsearch/search/SearchHits.java index ada09d2e38bae..0b49ba8ec123d 100644 --- a/core/src/main/java/org/elasticsearch/search/SearchHits.java +++ b/core/src/main/java/org/elasticsearch/search/SearchHits.java @@ -105,10 +105,10 @@ public SearchHit[] internalHits() { return this.hits; } - static final class Fields { - static final String HITS = "hits"; - static final String TOTAL = "total"; - static final String MAX_SCORE = "max_score"; + public static final class Fields { + public static final String HITS = "hits"; + public static final String TOTAL = "total"; + public static final String MAX_SCORE = "max_score"; } @Override diff --git a/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java b/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java index 4af794ab42c60..c2f20f1a79455 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java @@ -60,7 +60,7 @@ */ public class Suggest implements Iterable>>, Streamable, ToXContent { - static final String NAME = "suggest"; + public static final String NAME = "suggest"; public static final Comparator