Skip to content

Commit da669f0

Browse files
authored
Add fromXContent method to SearchResponse (#24720)
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
1 parent 9fc9db2 commit da669f0

File tree

10 files changed

+318
-24
lines changed

10 files changed

+318
-24
lines changed

core/src/main/java/org/elasticsearch/action/search/SearchResponse.java

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,45 @@
2121

2222
import org.elasticsearch.action.ActionResponse;
2323
import org.elasticsearch.common.Nullable;
24+
import org.elasticsearch.common.ParseField;
2425
import org.elasticsearch.common.Strings;
2526
import org.elasticsearch.common.io.stream.StreamInput;
2627
import org.elasticsearch.common.io.stream.StreamOutput;
2728
import org.elasticsearch.common.unit.TimeValue;
2829
import org.elasticsearch.common.xcontent.StatusToXContentObject;
2930
import org.elasticsearch.common.xcontent.XContentBuilder;
31+
import org.elasticsearch.common.xcontent.XContentParser;
3032
import org.elasticsearch.rest.RestStatus;
3133
import org.elasticsearch.rest.action.RestActions;
3234
import org.elasticsearch.search.SearchHits;
3335
import org.elasticsearch.search.aggregations.Aggregations;
3436
import org.elasticsearch.search.internal.InternalSearchResponse;
3537
import org.elasticsearch.search.profile.ProfileShardResult;
38+
import org.elasticsearch.search.profile.SearchProfileShardResults;
3639
import org.elasticsearch.search.suggest.Suggest;
3740

3841
import java.io.IOException;
42+
import java.util.ArrayList;
43+
import java.util.List;
3944
import java.util.Map;
4045

4146
import static org.elasticsearch.action.search.ShardSearchFailure.readShardSearchFailure;
47+
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
48+
import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField;
49+
import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownToken;
50+
4251

4352
/**
4453
* A response of a search request.
4554
*/
4655
public class SearchResponse extends ActionResponse implements StatusToXContentObject {
4756

57+
private static final ParseField SCROLL_ID = new ParseField("_scroll_id");
58+
private static final ParseField TOOK = new ParseField("took");
59+
private static final ParseField TIMED_OUT = new ParseField("timed_out");
60+
private static final ParseField TERMINATED_EARLY = new ParseField("terminated_early");
61+
private static final ParseField NUM_REDUCE_PHASES = new ParseField("num_reduce_phases");
62+
4863
private SearchResponseSections internalResponse;
4964

5065
private String scrollId;
@@ -175,7 +190,8 @@ public void scrollId(String scrollId) {
175190
*
176191
* @return The profile results or an empty map
177192
*/
178-
@Nullable public Map<String, ProfileShardResult> getProfileResults() {
193+
@Nullable
194+
public Map<String, ProfileShardResult> getProfileResults() {
179195
return internalResponse.profile();
180196
}
181197

@@ -189,22 +205,101 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
189205

190206
public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
191207
if (scrollId != null) {
192-
builder.field("_scroll_id", scrollId);
208+
builder.field(SCROLL_ID.getPreferredName(), scrollId);
193209
}
194-
builder.field("took", tookInMillis);
195-
builder.field("timed_out", isTimedOut());
210+
builder.field(TOOK.getPreferredName(), tookInMillis);
211+
builder.field(TIMED_OUT.getPreferredName(), isTimedOut());
196212
if (isTerminatedEarly() != null) {
197-
builder.field("terminated_early", isTerminatedEarly());
213+
builder.field(TERMINATED_EARLY.getPreferredName(), isTerminatedEarly());
198214
}
199215
if (getNumReducePhases() != 1) {
200-
builder.field("num_reduce_phases", getNumReducePhases());
216+
builder.field(NUM_REDUCE_PHASES.getPreferredName(), getNumReducePhases());
201217
}
202218
RestActions.buildBroadcastShardsHeader(builder, params, getTotalShards(), getSuccessfulShards(), getFailedShards(),
203219
getShardFailures());
204220
internalResponse.toXContent(builder, params);
205221
return builder;
206222
}
207223

224+
public static SearchResponse fromXContent(XContentParser parser) throws IOException {
225+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation);
226+
XContentParser.Token token;
227+
String currentFieldName = null;
228+
SearchHits hits = null;
229+
Aggregations aggs = null;
230+
Suggest suggest = null;
231+
SearchProfileShardResults profile = null;
232+
boolean timedOut = false;
233+
Boolean terminatedEarly = null;
234+
int numReducePhases = 1;
235+
long tookInMillis = -1;
236+
int successfulShards = -1;
237+
int totalShards = -1;
238+
String scrollId = null;
239+
List<ShardSearchFailure> failures = new ArrayList<>();
240+
while((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
241+
if (token == XContentParser.Token.FIELD_NAME) {
242+
currentFieldName = parser.currentName();
243+
} else if (token.isValue()) {
244+
if (SCROLL_ID.match(currentFieldName)) {
245+
scrollId = parser.text();
246+
} else if (TOOK.match(currentFieldName)) {
247+
tookInMillis = parser.longValue();
248+
} else if (TIMED_OUT.match(currentFieldName)) {
249+
timedOut = parser.booleanValue();
250+
} else if (TERMINATED_EARLY.match(currentFieldName)) {
251+
terminatedEarly = parser.booleanValue();
252+
} else if (NUM_REDUCE_PHASES.match(currentFieldName)) {
253+
numReducePhases = parser.intValue();
254+
} else {
255+
throwUnknownField(currentFieldName, parser.getTokenLocation());
256+
}
257+
} else if (token == XContentParser.Token.START_OBJECT) {
258+
if (SearchHits.Fields.HITS.equals(currentFieldName)) {
259+
hits = SearchHits.fromXContent(parser);
260+
} else if (Aggregations.AGGREGATIONS_FIELD.equals(currentFieldName)) {
261+
aggs = Aggregations.fromXContent(parser);
262+
} else if (Suggest.NAME.equals(currentFieldName)) {
263+
suggest = Suggest.fromXContent(parser);
264+
} else if (SearchProfileShardResults.PROFILE_FIELD.equals(currentFieldName)) {
265+
profile = SearchProfileShardResults.fromXContent(parser);
266+
} else if (RestActions._SHARDS_FIELD.match(currentFieldName)) {
267+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
268+
if (token == XContentParser.Token.FIELD_NAME) {
269+
currentFieldName = parser.currentName();
270+
} else if (token.isValue()) {
271+
if (RestActions.FAILED_FIELD.match(currentFieldName)) {
272+
parser.intValue(); // we don't need it but need to consume it
273+
} else if (RestActions.SUCCESSFUL_FIELD.match(currentFieldName)) {
274+
successfulShards = parser.intValue();
275+
} else if (RestActions.TOTAL_FIELD.match(currentFieldName)) {
276+
totalShards = parser.intValue();
277+
} else {
278+
throwUnknownField(currentFieldName, parser.getTokenLocation());
279+
}
280+
} else if (token == XContentParser.Token.START_ARRAY) {
281+
if (RestActions.FAILURES_FIELD.match(currentFieldName)) {
282+
while((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
283+
failures.add(ShardSearchFailure.fromXContent(parser));
284+
}
285+
} else {
286+
throwUnknownField(currentFieldName, parser.getTokenLocation());
287+
}
288+
} else {
289+
throwUnknownToken(token, parser.getTokenLocation());
290+
}
291+
}
292+
} else {
293+
throwUnknownField(currentFieldName, parser.getTokenLocation());
294+
}
295+
}
296+
}
297+
SearchResponseSections searchResponseSections = new SearchResponseSections(hits, aggs, suggest, timedOut, terminatedEarly,
298+
profile, numReducePhases);
299+
return new SearchResponse(searchResponseSections, scrollId, totalShards, successfulShards, tookInMillis,
300+
failures.toArray(new ShardSearchFailure[failures.size()]));
301+
}
302+
208303
@Override
209304
public void readFrom(StreamInput in) throws IOException {
210305
super.readFrom(in);

core/src/main/java/org/elasticsearch/rest/action/RestActions.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.action.support.broadcast.BroadcastResponse;
2626
import org.elasticsearch.action.support.nodes.BaseNodeResponse;
2727
import org.elasticsearch.action.support.nodes.BaseNodesResponse;
28+
import org.elasticsearch.common.ParseField;
2829
import org.elasticsearch.common.lucene.uid.Versions;
2930
import org.elasticsearch.common.xcontent.ToXContent;
3031
import org.elasticsearch.common.xcontent.ToXContent.Params;
@@ -46,6 +47,12 @@
4647

4748
public class RestActions {
4849

50+
public static final ParseField _SHARDS_FIELD = new ParseField("_shards");
51+
public static final ParseField TOTAL_FIELD = new ParseField("total");
52+
public static final ParseField SUCCESSFUL_FIELD = new ParseField("successful");
53+
public static final ParseField FAILED_FIELD = new ParseField("failed");
54+
public static final ParseField FAILURES_FIELD = new ParseField("failures");
55+
4956
public static long parseVersion(RestRequest request) {
5057
if (request.hasParam("version")) {
5158
return request.paramAsLong("version", Versions.MATCH_ANY);
@@ -71,12 +78,12 @@ public static void buildBroadcastShardsHeader(XContentBuilder builder, Params pa
7178
public static void buildBroadcastShardsHeader(XContentBuilder builder, Params params,
7279
int total, int successful, int failed,
7380
ShardOperationFailedException[] shardFailures) throws IOException {
74-
builder.startObject("_shards");
75-
builder.field("total", total);
76-
builder.field("successful", successful);
77-
builder.field("failed", failed);
81+
builder.startObject(_SHARDS_FIELD.getPreferredName());
82+
builder.field(TOTAL_FIELD.getPreferredName(), total);
83+
builder.field(SUCCESSFUL_FIELD.getPreferredName(), successful);
84+
builder.field(FAILED_FIELD.getPreferredName(), failed);
7885
if (shardFailures != null && shardFailures.length > 0) {
79-
builder.startArray("failures");
86+
builder.startArray(FAILURES_FIELD.getPreferredName());
8087
final boolean group = params.paramAsBoolean("group_shard_failures", true); // we group by default
8188
for (ShardOperationFailedException shardFailure : group ? ExceptionsHelper.groupBy(shardFailures) : shardFailures) {
8289
builder.startObject();

core/src/main/java/org/elasticsearch/search/SearchHits.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ public SearchHit[] internalHits() {
105105
return this.hits;
106106
}
107107

108-
static final class Fields {
109-
static final String HITS = "hits";
110-
static final String TOTAL = "total";
111-
static final String MAX_SCORE = "max_score";
108+
public static final class Fields {
109+
public static final String HITS = "hits";
110+
public static final String TOTAL = "total";
111+
public static final String MAX_SCORE = "max_score";
112112
}
113113

114114
@Override

core/src/main/java/org/elasticsearch/search/suggest/Suggest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
*/
6161
public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? extends Option>>>, Streamable, ToXContent {
6262

63-
static final String NAME = "suggest";
63+
public static final String NAME = "suggest";
6464

6565
public static final Comparator<Option> COMPARATOR = (first, second) -> {
6666
int cmp = Float.compare(second.getScore(), first.getScore());

0 commit comments

Comments
 (0)