Browse Source

Move test-only search response x-content-parsing code to test codebase (#105308)

Loads of code here that is only used in tests and one duplicate unused
class that was only used as an indirection to parsing the
`AsyncSearchResponse`. Moved what I could easily move via automated
refactoring to `SearchResponseUtils` in tests and removed the duplicate
now unused class from the client codebase.
Armin Braun 1 year ago
parent
commit
5c8006499a
28 changed files with 509 additions and 637 deletions
  1. 0 194
      client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/AsyncSearchResponse.java
  2. 1 1
      modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponseTests.java
  3. 1 1
      modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java
  4. 13 46
      modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/EvalQueryQuality.java
  5. 0 26
      modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RatedSearchHit.java
  6. 45 2
      modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/EvalQueryQualityTests.java
  7. 1 1
      modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RankEvalResponseTests.java
  8. 24 1
      modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedSearchHitTests.java
  9. 2 1
      qa/ccs-rolling-upgrade-remote-cluster/src/test/java/org/elasticsearch/upgrades/SearchStatesIT.java
  10. 2 61
      server/src/main/java/org/elasticsearch/action/search/MultiSearchResponse.java
  11. 18 278
      server/src/main/java/org/elasticsearch/action/search/SearchResponse.java
  12. 1 1
      server/src/test/java/org/elasticsearch/action/search/MultiSearchResponseTests.java
  13. 2 2
      server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java
  14. 326 1
      test/framework/src/main/java/org/elasticsearch/search/SearchResponseUtils.java
  15. 0 1
      x-pack/plugin/async-search/qa/security/build.gradle
  16. 44 3
      x-pack/plugin/async-search/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/search/AsyncSearchSecurityIT.java
  17. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityWithMultipleRemotesRestIT.java
  18. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityApiKeyRestIT.java
  19. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityBwcRestIT.java
  20. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCcrIT.java
  21. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCcrMigrationIT.java
  22. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityLicensingAndFeatureUsageRestIT.java
  23. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityMutualTlsIT.java
  24. 2 1
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityReloadCredentialsRestIT.java
  25. 3 2
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java
  26. 5 4
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecuritySpecialUserIT.java
  27. 3 2
      x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityTopologyRestIT.java
  28. 2 1
      x-pack/qa/repository-old-versions/src/test/java/org/elasticsearch/oldrepos/OldRepositoryAccessIT.java

+ 0 - 194
client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/AsyncSearchResponse.java

@@ -1,194 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-package org.elasticsearch.client.asyncsearch;
-
-import org.elasticsearch.ElasticsearchException;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.xcontent.ChunkedToXContent;
-import org.elasticsearch.core.Nullable;
-import org.elasticsearch.xcontent.ConstructingObjectParser;
-import org.elasticsearch.xcontent.ParseField;
-import org.elasticsearch.xcontent.ToXContentObject;
-import org.elasticsearch.xcontent.XContentBuilder;
-import org.elasticsearch.xcontent.XContentParser;
-import org.elasticsearch.xcontent.XContentParser.Token;
-
-import java.io.IOException;
-
-import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
-import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
-import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
-
-/**
- * A response of an async search request.
- */
-public class AsyncSearchResponse implements ToXContentObject {
-    @Nullable
-    private final String id;
-    @Nullable
-    private final SearchResponse searchResponse;
-    @Nullable
-    private final ElasticsearchException error;
-    private final boolean isRunning;
-    private final boolean isPartial;
-
-    private final long startTimeMillis;
-    private final long expirationTimeMillis;
-
-    /**
-     * Creates an {@link AsyncSearchResponse} with the arguments that are always present in the server response
-     */
-    AsyncSearchResponse(
-        boolean isPartial,
-        boolean isRunning,
-        long startTimeMillis,
-        long expirationTimeMillis,
-        @Nullable String id,
-        @Nullable SearchResponse searchResponse,
-        @Nullable ElasticsearchException error
-    ) {
-        this.isPartial = isPartial;
-        this.isRunning = isRunning;
-        this.startTimeMillis = startTimeMillis;
-        this.expirationTimeMillis = expirationTimeMillis;
-        this.id = id;
-        this.searchResponse = searchResponse;
-        this.error = error;
-    }
-
-    /**
-     * Returns the id of the async search request or null if the response is not stored in the cluster.
-     */
-    @Nullable
-    public String getId() {
-        return id;
-    }
-
-    /**
-     * Returns the current {@link SearchResponse} or <code>null</code> if not available.
-     *
-     * See {@link #isPartial()} to determine whether the response contains partial or complete
-     * results.
-     */
-    public SearchResponse getSearchResponse() {
-        return searchResponse;
-    }
-
-    /**
-     * Returns the failure reason or null if the query is running or has completed normally.
-     */
-    public ElasticsearchException getFailure() {
-        return error;
-    }
-
-    /**
-     * Returns <code>true</code> if the {@link SearchResponse} contains partial
-     * results computed from a subset of the total shards.
-     */
-    public boolean isPartial() {
-        return isPartial;
-    }
-
-    /**
-     * Whether the search is still running in the cluster.
-     *
-     * A value of <code>false</code> indicates that the response is final
-     * even if {@link #isPartial()} returns <code>true</code>. In such case,
-     * the partial response represents the status of the search before a
-     * non-recoverable failure.
-     */
-    public boolean isRunning() {
-        return isRunning;
-    }
-
-    /**
-     * When this response was created as a timestamp in milliseconds since epoch.
-     */
-    public long getStartTime() {
-        return startTimeMillis;
-    }
-
-    /**
-     * When this response will expired as a timestamp in milliseconds since epoch.
-     */
-    public long getExpirationTime() {
-        return expirationTimeMillis;
-    }
-
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        builder.startObject();
-        if (id != null) {
-            builder.field("id", id);
-        }
-        builder.field("is_partial", isPartial);
-        builder.field("is_running", isRunning);
-        builder.field("start_time_in_millis", startTimeMillis);
-        builder.field("expiration_time_in_millis", expirationTimeMillis);
-
-        if (searchResponse != null) {
-            builder.field("response");
-            ChunkedToXContent.wrapAsToXContent(searchResponse).toXContent(builder, params);
-        }
-        if (error != null) {
-            builder.startObject("error");
-            error.toXContent(builder, params);
-            builder.endObject();
-        }
-        builder.endObject();
-        return builder;
-    }
-
-    public static final ParseField ID_FIELD = new ParseField("id");
-    public static final ParseField IS_PARTIAL_FIELD = new ParseField("is_partial");
-    public static final ParseField IS_RUNNING_FIELD = new ParseField("is_running");
-    public static final ParseField START_TIME_FIELD = new ParseField("start_time_in_millis");
-    public static final ParseField EXPIRATION_FIELD = new ParseField("expiration_time_in_millis");
-    public static final ParseField RESPONSE_FIELD = new ParseField("response");
-    public static final ParseField ERROR_FIELD = new ParseField("error");
-
-    public static final ConstructingObjectParser<AsyncSearchResponse, Void> PARSER = new ConstructingObjectParser<>(
-        "submit_async_search_response",
-        true,
-        args -> new AsyncSearchResponse(
-            (boolean) args[0],
-            (boolean) args[1],
-            (long) args[2],
-            (long) args[3],
-            (String) args[4],
-            (SearchResponse) args[5],
-            (ElasticsearchException) args[6]
-        )
-    );
-    static {
-        PARSER.declareBoolean(constructorArg(), IS_PARTIAL_FIELD);
-        PARSER.declareBoolean(constructorArg(), IS_RUNNING_FIELD);
-        PARSER.declareLong(constructorArg(), START_TIME_FIELD);
-        PARSER.declareLong(constructorArg(), EXPIRATION_FIELD);
-        PARSER.declareString(optionalConstructorArg(), ID_FIELD);
-        PARSER.declareObject(optionalConstructorArg(), (p, c) -> AsyncSearchResponse.parseSearchResponse(p), RESPONSE_FIELD);
-        PARSER.declareObject(optionalConstructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), ERROR_FIELD);
-    }
-
-    private static SearchResponse parseSearchResponse(XContentParser p) throws IOException {
-        // we should be before the opening START_OBJECT of the response
-        ensureExpectedToken(Token.START_OBJECT, p.currentToken(), p);
-        p.nextToken();
-        return SearchResponse.innerFromXContent(p);
-    }
-
-    public static AsyncSearchResponse fromXContent(XContentParser parser) {
-        return PARSER.apply(parser, null);
-    }
-
-    @Override
-    public String toString() {
-        return Strings.toString(this);
-    }
-}

+ 1 - 1
modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponseTests.java

@@ -98,7 +98,7 @@ public class MultiSearchTemplateResponseTests extends AbstractXContentTestCase<M
     @Override
     protected MultiSearchTemplateResponse doParseInstance(XContentParser parser) {
         // The MultiSearchTemplateResponse is identical to the multi search response so we reuse the parsing logic in multi search response
-        MultiSearchResponse mSearchResponse = MultiSearchResponse.fromXContext(parser);
+        MultiSearchResponse mSearchResponse = SearchResponseUtils.parseMultiSearchResponse(parser);
         try {
             org.elasticsearch.action.search.MultiSearchResponse.Item[] responses = mSearchResponse.getResponses();
             MultiSearchTemplateResponse.Item[] templateResponses = new MultiSearchTemplateResponse.Item[responses.length];

+ 1 - 1
modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java

@@ -63,7 +63,7 @@ public class SearchTemplateResponseTests extends AbstractXContentTestCase<Search
                     contentType
                 )
             ) {
-                searchTemplateResponse.setResponse(SearchResponse.fromXContent(searchResponseParser));
+                searchTemplateResponse.setResponse(SearchResponseUtils.parseSearchResponse(searchResponseParser));
             }
         }
         return searchTemplateResponse;

+ 13 - 46
modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/EvalQueryQuality.java

@@ -11,13 +11,10 @@ package org.elasticsearch.index.rankeval;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
-import org.elasticsearch.common.xcontent.XContentParserUtils;
 import org.elasticsearch.index.rankeval.RatedDocument.DocumentKey;
-import org.elasticsearch.xcontent.ObjectParser;
 import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContentFragment;
 import org.elasticsearch.xcontent.XContentBuilder;
-import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -35,24 +32,23 @@ public class EvalQueryQuality implements ToXContentFragment, Writeable {
     private final List<RatedSearchHit> ratedHits;
 
     public EvalQueryQuality(String id, double metricScore) {
-        this.queryId = id;
-        this.metricScore = metricScore;
-        this.ratedHits = new ArrayList<>();
+        this(id, metricScore, new ArrayList<>(), null);
     }
 
     public EvalQueryQuality(StreamInput in) throws IOException {
-        this.queryId = in.readString();
-        this.metricScore = in.readDouble();
-        this.ratedHits = in.readCollectionAsList(RatedSearchHit::new);
-        this.optionalMetricDetails = in.readOptionalNamedWriteable(MetricDetail.class);
+        this(
+            in.readString(),
+            in.readDouble(),
+            in.readCollectionAsList(RatedSearchHit::new),
+            in.readOptionalNamedWriteable(MetricDetail.class)
+        );
     }
 
-    // only used for parsing internally
-    private EvalQueryQuality(String queryId, ParsedEvalQueryQuality builder) {
+    EvalQueryQuality(String queryId, double evaluationResult, List<RatedSearchHit> ratedHits, MetricDetail optionalMetricDetails) {
         this.queryId = queryId;
-        this.metricScore = builder.evaluationResult;
-        this.optionalMetricDetails = builder.optionalMetricDetails;
-        this.ratedHits = builder.ratedHits;
+        this.metricScore = evaluationResult;
+        this.optionalMetricDetails = optionalMetricDetails;
+        this.ratedHits = ratedHits;
     }
 
     @Override
@@ -113,37 +109,8 @@ public class EvalQueryQuality implements ToXContentFragment, Writeable {
 
     static final ParseField METRIC_SCORE_FIELD = new ParseField("metric_score");
     private static final ParseField UNRATED_DOCS_FIELD = new ParseField("unrated_docs");
-    private static final ParseField HITS_FIELD = new ParseField("hits");
-    private static final ParseField METRIC_DETAILS_FIELD = new ParseField("metric_details");
-    private static final ObjectParser<ParsedEvalQueryQuality, Void> PARSER = new ObjectParser<>(
-        "eval_query_quality",
-        true,
-        ParsedEvalQueryQuality::new
-    );
-
-    private static class ParsedEvalQueryQuality {
-        double evaluationResult;
-        MetricDetail optionalMetricDetails;
-        List<RatedSearchHit> ratedHits = new ArrayList<>();
-    }
-
-    static {
-        PARSER.declareDouble((obj, value) -> obj.evaluationResult = value, METRIC_SCORE_FIELD);
-        PARSER.declareObject((obj, value) -> obj.optionalMetricDetails = value, (p, c) -> parseMetricDetail(p), METRIC_DETAILS_FIELD);
-        PARSER.declareObjectArray((obj, list) -> obj.ratedHits = list, (p, c) -> RatedSearchHit.parse(p), HITS_FIELD);
-    }
-
-    private static MetricDetail parseMetricDetail(XContentParser parser) throws IOException {
-        XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
-        XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser);
-        MetricDetail metricDetail = parser.namedObject(MetricDetail.class, parser.currentName(), null);
-        XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser);
-        return metricDetail;
-    }
-
-    public static EvalQueryQuality fromXContent(XContentParser parser, String queryId) throws IOException {
-        return new EvalQueryQuality(queryId, PARSER.apply(parser, null));
-    }
+    static final ParseField HITS_FIELD = new ParseField("hits");
+    static final ParseField METRIC_DETAILS_FIELD = new ParseField("metric_details");
 
     @Override
     public final boolean equals(Object obj) {

+ 0 - 26
modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RatedSearchHit.java

@@ -12,13 +12,9 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.search.SearchHit;
-import org.elasticsearch.xcontent.ConstructingObjectParser;
-import org.elasticsearch.xcontent.ObjectParser.ValueType;
-import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.ToXContentObject;
 import org.elasticsearch.xcontent.XContentBuilder;
-import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -67,28 +63,6 @@ public class RatedSearchHit implements Writeable, ToXContentObject {
         return builder;
     }
 
-    private static final ParseField HIT_FIELD = new ParseField("hit");
-    private static final ParseField RATING_FIELD = new ParseField("rating");
-    private static final ConstructingObjectParser<RatedSearchHit, Void> PARSER = new ConstructingObjectParser<>(
-        "rated_hit",
-        true,
-        a -> new RatedSearchHit((SearchHit) a[0], (OptionalInt) a[1])
-    );
-
-    static {
-        PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> SearchHit.fromXContent(p), HIT_FIELD);
-        PARSER.declareField(
-            ConstructingObjectParser.constructorArg(),
-            (p) -> p.currentToken() == XContentParser.Token.VALUE_NULL ? OptionalInt.empty() : OptionalInt.of(p.intValue()),
-            RATING_FIELD,
-            ValueType.INT_OR_NULL
-        );
-    }
-
-    public static RatedSearchHit parse(XContentParser parser) throws IOException {
-        return PARSER.apply(parser, null);
-    }
-
     @Override
     public final boolean equals(Object obj) {
         if (this == obj) {

+ 45 - 2
modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/EvalQueryQualityTests.java

@@ -10,10 +10,12 @@ package org.elasticsearch.index.rankeval;
 
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.xcontent.XContentParserUtils;
 import org.elasticsearch.index.shard.ShardId;
 import org.elasticsearch.search.SearchShardTarget;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.ObjectParser;
 import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentType;
@@ -31,7 +33,48 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXC
 
 public class EvalQueryQualityTests extends ESTestCase {
 
-    private static NamedWriteableRegistry namedWritableRegistry = new NamedWriteableRegistry(new RankEvalPlugin().getNamedWriteables());
+    private static final NamedWriteableRegistry namedWritableRegistry = new NamedWriteableRegistry(
+        new RankEvalPlugin().getNamedWriteables()
+    );
+
+    private static final ObjectParser<ParsedEvalQueryQuality, Void> PARSER = new ObjectParser<>(
+        "eval_query_quality",
+        true,
+        ParsedEvalQueryQuality::new
+    );
+
+    private static final class ParsedEvalQueryQuality {
+        double evaluationResult;
+        MetricDetail optionalMetricDetails;
+        List<RatedSearchHit> ratedHits = new ArrayList<>();
+    }
+
+    static {
+        PARSER.declareDouble((obj, value) -> obj.evaluationResult = value, EvalQueryQuality.METRIC_SCORE_FIELD);
+        PARSER.declareObject(
+            (obj, value) -> obj.optionalMetricDetails = value,
+            (p, c) -> parseMetricDetail(p),
+            EvalQueryQuality.METRIC_DETAILS_FIELD
+        );
+        PARSER.declareObjectArray(
+            (obj, list) -> obj.ratedHits = list,
+            (p, c) -> RatedSearchHitTests.parseInstance(p),
+            EvalQueryQuality.HITS_FIELD
+        );
+    }
+
+    public static EvalQueryQuality parseInstance(XContentParser parser, String queryId) {
+        var evalQuality = PARSER.apply(parser, null);
+        return new EvalQueryQuality(queryId, evalQuality.evaluationResult, evalQuality.ratedHits, evalQuality.optionalMetricDetails);
+    }
+
+    private static MetricDetail parseMetricDetail(XContentParser parser) throws IOException {
+        XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
+        XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser);
+        MetricDetail metricDetail = parser.namedObject(MetricDetail.class, parser.currentName(), null);
+        XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser);
+        return metricDetail;
+    }
 
     @SuppressWarnings("resource")
     @Override
@@ -93,7 +136,7 @@ public class EvalQueryQualityTests extends ESTestCase {
             ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser);
             String queryId = parser.currentName();
             ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
-            parsedItem = EvalQueryQuality.fromXContent(parser, queryId);
+            parsedItem = parseInstance(parser, queryId);
             ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.currentToken(), parser);
             ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser);
             assertNull(parser.nextToken());

+ 1 - 1
modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RankEvalResponseTests.java

@@ -69,7 +69,7 @@ public class RankEvalResponseTests extends ESTestCase {
         PARSER.declareDouble(ConstructingObjectParser.constructorArg(), EvalQueryQuality.METRIC_SCORE_FIELD);
         PARSER.declareNamedObjects(
             ConstructingObjectParser.optionalConstructorArg(),
-            (p, c, n) -> EvalQueryQuality.fromXContent(p, n),
+            (p, c, n) -> EvalQueryQualityTests.parseInstance(p, n),
             new ParseField("details")
         );
         PARSER.declareNamedObjects(ConstructingObjectParser.optionalConstructorArg(), (p, c, n) -> {

+ 24 - 1
modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedSearchHitTests.java

@@ -12,6 +12,9 @@ import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
+import org.elasticsearch.xcontent.ObjectParser;
+import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentType;
@@ -24,6 +27,26 @@ import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashC
 
 public class RatedSearchHitTests extends ESTestCase {
 
+    private static final ConstructingObjectParser<RatedSearchHit, Void> PARSER = new ConstructingObjectParser<>(
+        "rated_hit",
+        true,
+        a -> new RatedSearchHit((SearchHit) a[0], (OptionalInt) a[1])
+    );
+
+    static {
+        PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> SearchHit.fromXContent(p), new ParseField("hit"));
+        PARSER.declareField(
+            ConstructingObjectParser.constructorArg(),
+            (p) -> p.currentToken() == XContentParser.Token.VALUE_NULL ? OptionalInt.empty() : OptionalInt.of(p.intValue()),
+            new ParseField("rating"),
+            ObjectParser.ValueType.INT_OR_NULL
+        );
+    }
+
+    public static RatedSearchHit parseInstance(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
     public static RatedSearchHit randomRatedSearchHit() {
         OptionalInt rating = randomBoolean() ? OptionalInt.empty() : OptionalInt.of(randomIntBetween(0, 5));
         SearchHit searchHit = SearchHit.unpooled(randomIntBetween(0, 10), randomAlphaOfLength(10));
@@ -55,7 +78,7 @@ public class RatedSearchHitTests extends ESTestCase {
         XContentType xContentType = randomFrom(XContentType.values());
         BytesReference originalBytes = toShuffledXContent(testItem, xContentType, ToXContent.EMPTY_PARAMS, randomBoolean());
         try (XContentParser parser = createParser(xContentType.xContent(), originalBytes)) {
-            RatedSearchHit parsedItem = RatedSearchHit.parse(parser);
+            RatedSearchHit parsedItem = parseInstance(parser);
             assertNotSame(testItem, parsedItem);
             assertEquals(testItem, parsedItem);
             assertEquals(testItem.hashCode(), parsedItem.hashCode());

+ 2 - 1
qa/ccs-rolling-upgrade-remote-cluster/src/test/java/org/elasticsearch/upgrades/SearchStatesIT.java

@@ -19,6 +19,7 @@ import org.elasticsearch.client.RestClient;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
 import org.elasticsearch.test.rest.ESRestTestCase;
 import org.elasticsearch.test.rest.ObjectPath;
@@ -174,7 +175,7 @@ public class SearchStatesIT extends ESRestTestCase {
                     response.getEntity().getContent()
                 )
             ) {
-                SearchResponse searchResponse = SearchResponse.fromXContent(parser);
+                SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(parser);
                 try {
                     ElasticsearchAssertions.assertNoFailures(searchResponse);
                     ElasticsearchAssertions.assertHitCount(searchResponse, expectedDocs);

+ 2 - 61
server/src/main/java/org/elasticsearch/action/search/MultiSearchResponse.java

@@ -24,36 +24,16 @@ import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.RefCounted;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.transport.LeakTracker;
-import org.elasticsearch.xcontent.ConstructingObjectParser;
-import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContent;
-import org.elasticsearch.xcontent.XContentParser;
-import org.elasticsearch.xcontent.XContentParser.Token;
 
 import java.io.IOException;
 import java.util.Iterator;
-import java.util.List;
-
-import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
 
 /**
  * A multi search response.
  */
 public class MultiSearchResponse extends ActionResponse implements Iterable<MultiSearchResponse.Item>, ChunkedToXContentObject {
 
-    private static final ParseField RESPONSES = new ParseField(Fields.RESPONSES);
-    private static final ParseField TOOK_IN_MILLIS = new ParseField("took");
-    @SuppressWarnings("unchecked")
-    private static final ConstructingObjectParser<MultiSearchResponse, Void> PARSER = new ConstructingObjectParser<>(
-        "multi_search",
-        true,
-        a -> new MultiSearchResponse(((List<Item>) a[0]).toArray(new Item[0]), (long) a[1])
-    );
-    static {
-        PARSER.declareObjectArray(constructorArg(), (p, c) -> itemFromXContent(p), RESPONSES);
-        PARSER.declareLong(constructorArg(), TOOK_IN_MILLIS);
-    }
-
     /**
      * A search response item, holding the actual search response, or an error message if it failed.
      */
@@ -231,47 +211,8 @@ public class MultiSearchResponse extends ActionResponse implements Iterable<Mult
         );
     }
 
-    public static MultiSearchResponse fromXContext(XContentParser parser) {
-        return PARSER.apply(parser, null);
-    }
-
-    private static MultiSearchResponse.Item itemFromXContent(XContentParser parser) throws IOException {
-        // This parsing logic is a bit tricky here, because the multi search response itself is tricky:
-        // 1) The json objects inside the responses array are either a search response or a serialized exception
-        // 2) Each response json object gets a status field injected that ElasticsearchException.failureFromXContent(...) does not parse,
-        // but SearchResponse.innerFromXContent(...) parses and then ignores. The status field is not needed to parse
-        // the response item. However in both cases this method does need to parse the 'status' field otherwise the parsing of
-        // the response item in the next json array element will fail due to parsing errors.
-
-        Item item = null;
-        String fieldName = null;
-
-        Token token = parser.nextToken();
-        assert token == Token.FIELD_NAME;
-        outer: for (; token != Token.END_OBJECT; token = parser.nextToken()) {
-            switch (token) {
-                case FIELD_NAME:
-                    fieldName = parser.currentName();
-                    if ("error".equals(fieldName)) {
-                        item = new Item(null, ElasticsearchException.failureFromXContent(parser));
-                    } else if ("status".equals(fieldName) == false) {
-                        item = new Item(SearchResponse.innerFromXContent(parser), null);
-                        break outer;
-                    }
-                    break;
-                case VALUE_NUMBER:
-                    if ("status".equals(fieldName)) {
-                        // Ignore the status value
-                    }
-                    break;
-            }
-        }
-        assert parser.currentToken() == Token.END_OBJECT;
-        return item;
-    }
-
-    static final class Fields {
-        static final String RESPONSES = "responses";
+    public static final class Fields {
+        public static final String RESPONSES = "responses";
         static final String STATUS = "status";
     }
 

+ 18 - 278
server/src/main/java/org/elasticsearch/action/search/SearchResponse.java

@@ -37,11 +37,8 @@ import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.ToXContentFragment;
 import org.elasticsearch.xcontent.XContentBuilder;
-import org.elasticsearch.xcontent.XContentParser;
-import org.elasticsearch.xcontent.XContentParser.Token;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -53,7 +50,6 @@ import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 import static org.elasticsearch.action.search.ShardSearchFailure.readShardSearchFailure;
-import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
 
 /**
  * A response of a search request.
@@ -64,12 +60,12 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO
     // rather than empty string (RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY) we use internally
     public static final String LOCAL_CLUSTER_NAME_REPRESENTATION = "(local)";
 
-    private static final ParseField SCROLL_ID = new ParseField("_scroll_id");
-    private static final ParseField POINT_IN_TIME_ID = new ParseField("pit_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");
+    public static final ParseField SCROLL_ID = new ParseField("_scroll_id");
+    public static final ParseField POINT_IN_TIME_ID = new ParseField("pit_id");
+    public static final ParseField TOOK = new ParseField("took");
+    public static final ParseField TIMED_OUT = new ParseField("timed_out");
+    public static final ParseField TERMINATED_EARLY = new ParseField("terminated_early");
+    public static final ParseField NUM_REDUCE_PHASES = new ParseField("num_reduce_phases");
 
     private final SearchHits hits;
     private final InternalAggregations aggregations;
@@ -446,113 +442,6 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO
         return builder;
     }
 
-    public static SearchResponse fromXContent(XContentParser parser) throws IOException {
-        ensureExpectedToken(Token.START_OBJECT, parser.nextToken(), parser);
-        parser.nextToken();
-        return innerFromXContent(parser);
-    }
-
-    public static SearchResponse innerFromXContent(XContentParser parser) throws IOException {
-        ensureExpectedToken(Token.FIELD_NAME, parser.currentToken(), parser);
-        String currentFieldName = parser.currentName();
-        SearchHits hits = null;
-        InternalAggregations aggs = null;
-        Suggest suggest = null;
-        SearchProfileResults profile = null;
-        boolean timedOut = false;
-        Boolean terminatedEarly = null;
-        int numReducePhases = 1;
-        long tookInMillis = -1;
-        int successfulShards = -1;
-        int totalShards = -1;
-        int skippedShards = 0; // 0 for BWC
-        String scrollId = null;
-        String searchContextId = null;
-        List<ShardSearchFailure> failures = new ArrayList<>();
-        Clusters clusters = Clusters.EMPTY;
-        for (Token token = parser.nextToken(); token != Token.END_OBJECT; token = parser.nextToken()) {
-            if (token == Token.FIELD_NAME) {
-                currentFieldName = parser.currentName();
-            } else if (token.isValue()) {
-                if (SCROLL_ID.match(currentFieldName, parser.getDeprecationHandler())) {
-                    scrollId = parser.text();
-                } else if (POINT_IN_TIME_ID.match(currentFieldName, parser.getDeprecationHandler())) {
-                    searchContextId = parser.text();
-                } else if (TOOK.match(currentFieldName, parser.getDeprecationHandler())) {
-                    tookInMillis = parser.longValue();
-                } else if (TIMED_OUT.match(currentFieldName, parser.getDeprecationHandler())) {
-                    timedOut = parser.booleanValue();
-                } else if (TERMINATED_EARLY.match(currentFieldName, parser.getDeprecationHandler())) {
-                    terminatedEarly = parser.booleanValue();
-                } else if (NUM_REDUCE_PHASES.match(currentFieldName, parser.getDeprecationHandler())) {
-                    numReducePhases = parser.intValue();
-                } else {
-                    parser.skipChildren();
-                }
-            } else if (token == Token.START_OBJECT) {
-                if (SearchHits.Fields.HITS.equals(currentFieldName)) {
-                    hits = SearchHits.fromXContent(parser);
-                } else if (InternalAggregations.AGGREGATIONS_FIELD.equals(currentFieldName)) {
-                    aggs = InternalAggregations.fromXContent(parser);
-                } else if (Suggest.NAME.equals(currentFieldName)) {
-                    suggest = Suggest.fromXContent(parser);
-                } else if (SearchProfileResults.PROFILE_FIELD.equals(currentFieldName)) {
-                    profile = SearchProfileResults.fromXContent(parser);
-                } else if (RestActions._SHARDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                    while ((token = parser.nextToken()) != Token.END_OBJECT) {
-                        if (token == Token.FIELD_NAME) {
-                            currentFieldName = parser.currentName();
-                        } else if (token.isValue()) {
-                            if (RestActions.FAILED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                parser.intValue(); // we don't need it but need to consume it
-                            } else if (RestActions.SUCCESSFUL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                successfulShards = parser.intValue();
-                            } else if (RestActions.TOTAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                totalShards = parser.intValue();
-                            } else if (RestActions.SKIPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                skippedShards = parser.intValue();
-                            } else {
-                                parser.skipChildren();
-                            }
-                        } else if (token == Token.START_ARRAY) {
-                            if (RestActions.FAILURES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                while (parser.nextToken() != Token.END_ARRAY) {
-                                    failures.add(ShardSearchFailure.fromXContent(parser));
-                                }
-                            } else {
-                                parser.skipChildren();
-                            }
-                        } else {
-                            parser.skipChildren();
-                        }
-                    }
-                } else if (Clusters._CLUSTERS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                    clusters = Clusters.fromXContent(parser);
-                } else {
-                    parser.skipChildren();
-                }
-            }
-        }
-
-        return new SearchResponse(
-            hits,
-            aggs,
-            suggest,
-            timedOut,
-            terminatedEarly,
-            profile,
-            numReducePhases,
-            scrollId,
-            totalShards,
-            successfulShards,
-            skippedShards,
-            tookInMillis,
-            failures.toArray(ShardSearchFailure.EMPTY_ARRAY),
-            clusters,
-            searchContextId
-        );
-    }
-
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         assert hasReferences();
@@ -591,14 +480,14 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO
 
         public static final Clusters EMPTY = new Clusters(0, 0, 0);
 
-        static final ParseField _CLUSTERS_FIELD = new ParseField("_clusters");
-        static final ParseField TOTAL_FIELD = new ParseField("total");
-        static final ParseField SUCCESSFUL_FIELD = new ParseField("successful");
-        static final ParseField SKIPPED_FIELD = new ParseField("skipped");
-        static final ParseField RUNNING_FIELD = new ParseField("running");
-        static final ParseField PARTIAL_FIELD = new ParseField("partial");
-        static final ParseField FAILED_FIELD = new ParseField("failed");
-        static final ParseField DETAILS_FIELD = new ParseField("details");
+        public static final ParseField _CLUSTERS_FIELD = new ParseField("_clusters");
+        public static final ParseField TOTAL_FIELD = new ParseField("total");
+        public static final ParseField SUCCESSFUL_FIELD = new ParseField("successful");
+        public static final ParseField SKIPPED_FIELD = new ParseField("skipped");
+        public static final ParseField RUNNING_FIELD = new ParseField("running");
+        public static final ParseField PARTIAL_FIELD = new ParseField("partial");
+        public static final ParseField FAILED_FIELD = new ParseField("failed");
+        public static final ParseField DETAILS_FIELD = new ParseField("details");
 
         private final int total;
         private final int successful;   // not used for minimize_roundtrips=true; dynamically determined from clusterInfo map
@@ -712,7 +601,7 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO
                     + failed;
         }
 
-        private Clusters(Map<String, Cluster> clusterInfoMap) {
+        public Clusters(Map<String, Cluster> clusterInfoMap) {
             assert clusterInfoMap.size() > 0 : "this constructor should not be called with an empty Cluster info map";
             this.total = clusterInfoMap.size();
             this.clusterInfo = clusterInfoMap;
@@ -759,65 +648,6 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO
             return builder;
         }
 
-        public static Clusters fromXContent(XContentParser parser) throws IOException {
-            XContentParser.Token token = parser.currentToken();
-            ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser);
-            int total = -1;
-            int successful = -1;
-            int skipped = -1;
-            int running = 0;    // 0 for BWC
-            int partial = 0;    // 0 for BWC
-            int failed = 0;     // 0 for BWC
-            Map<String, Cluster> clusterInfoMap = ConcurrentCollections.newConcurrentMap();
-            String currentFieldName = null;
-            while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                if (token == XContentParser.Token.FIELD_NAME) {
-                    currentFieldName = parser.currentName();
-                } else if (token.isValue()) {
-                    if (Clusters.TOTAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        total = parser.intValue();
-                    } else if (Clusters.SUCCESSFUL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        successful = parser.intValue();
-                    } else if (Clusters.SKIPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        skipped = parser.intValue();
-                    } else if (Clusters.RUNNING_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        running = parser.intValue();
-                    } else if (Clusters.PARTIAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        partial = parser.intValue();
-                    } else if (Clusters.FAILED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        failed = parser.intValue();
-                    } else {
-                        parser.skipChildren();
-                    }
-                } else if (token == Token.START_OBJECT) {
-                    if (Clusters.DETAILS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        String currentDetailsFieldName = null;
-                        while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                            if (token == XContentParser.Token.FIELD_NAME) {
-                                currentDetailsFieldName = parser.currentName();  // cluster alias
-                            } else if (token == Token.START_OBJECT) {
-                                Cluster c = Cluster.fromXContent(currentDetailsFieldName, parser);
-                                clusterInfoMap.put(currentDetailsFieldName, c);
-                            } else {
-                                parser.skipChildren();
-                            }
-                        }
-                    } else {
-                        parser.skipChildren();
-                    }
-                } else {
-                    parser.skipChildren();
-                }
-            }
-            if (clusterInfoMap.isEmpty()) {
-                assert running == 0 && partial == 0 && failed == 0
-                    : "Non cross-cluster should have counter for running, partial and failed equal to 0";
-                return new Clusters(total, successful, skipped);
-            } else {
-                return new Clusters(clusterInfoMap);
-            }
-        }
-
         /**
          * @return how many total clusters the search was requested to be executed on
          */
@@ -981,10 +811,10 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO
      * See the Clusters clusterInfo Map for details.
      */
     public static class Cluster implements ToXContentFragment, Writeable {
-        static final ParseField INDICES_FIELD = new ParseField("indices");
-        static final ParseField STATUS_FIELD = new ParseField("status");
+        public static final ParseField INDICES_FIELD = new ParseField("indices");
+        public static final ParseField STATUS_FIELD = new ParseField("status");
 
-        private static final boolean SKIP_UNAVAILABLE_DEFAULT = false;
+        public static final boolean SKIP_UNAVAILABLE_DEFAULT = false;
 
         private final String clusterAlias;
         private final String indexExpression; // original index expression from the user for this cluster
@@ -1242,96 +1072,6 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO
             return builder;
         }
 
-        public static Cluster fromXContent(String clusterAlias, XContentParser parser) throws IOException {
-            XContentParser.Token token = parser.currentToken();
-            ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser);
-
-            String clusterName = clusterAlias;
-            if (clusterAlias.equals(LOCAL_CLUSTER_NAME_REPRESENTATION)) {
-                clusterName = "";
-            }
-            String indexExpression = null;
-            String status = "running";
-            boolean timedOut = false;
-            long took = -1L;
-            // these are all from the _shards section
-            int totalShards = -1;
-            int successfulShards = -1;
-            int skippedShards = -1;
-            int failedShards = -1;
-            List<ShardSearchFailure> failures = new ArrayList<>();
-
-            String currentFieldName = null;
-            while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                if (token == XContentParser.Token.FIELD_NAME) {
-                    currentFieldName = parser.currentName();
-                } else if (token.isValue()) {
-                    if (Cluster.INDICES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        indexExpression = parser.text();
-                    } else if (Cluster.STATUS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        status = parser.text();
-                    } else if (TIMED_OUT.match(currentFieldName, parser.getDeprecationHandler())) {
-                        timedOut = parser.booleanValue();
-                    } else if (TOOK.match(currentFieldName, parser.getDeprecationHandler())) {
-                        took = parser.longValue();
-                    } else {
-                        parser.skipChildren();
-                    }
-                } else if (RestActions._SHARDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                    while ((token = parser.nextToken()) != Token.END_OBJECT) {
-                        if (token == Token.FIELD_NAME) {
-                            currentFieldName = parser.currentName();
-                        } else if (token.isValue()) {
-                            if (RestActions.FAILED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                failedShards = parser.intValue();
-                            } else if (RestActions.SUCCESSFUL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                successfulShards = parser.intValue();
-                            } else if (RestActions.TOTAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                totalShards = parser.intValue();
-                            } else if (RestActions.SKIPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                                skippedShards = parser.intValue();
-                            } else {
-                                parser.skipChildren();
-                            }
-                        } else {
-                            parser.skipChildren();
-                        }
-                    }
-                } else if (token == Token.START_ARRAY) {
-                    if (RestActions.FAILURES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
-                        while (parser.nextToken() != Token.END_ARRAY) {
-                            failures.add(ShardSearchFailure.fromXContent(parser));
-                        }
-                    } else {
-                        parser.skipChildren();
-                    }
-                } else {
-                    parser.skipChildren();
-                }
-            }
-
-            Integer totalShardsFinal = totalShards == -1 ? null : totalShards;
-            Integer successfulShardsFinal = successfulShards == -1 ? null : successfulShards;
-            Integer skippedShardsFinal = skippedShards == -1 ? null : skippedShards;
-            Integer failedShardsFinal = failedShards == -1 ? null : failedShards;
-            TimeValue tookTimeValue = took == -1L ? null : new TimeValue(took);
-            boolean skipUnavailable = SKIP_UNAVAILABLE_DEFAULT;  // skipUnavailable is not exposed to XContent, so just use default
-
-            return new Cluster(
-                clusterName,
-                indexExpression,
-                skipUnavailable,
-                SearchResponse.Cluster.Status.valueOf(status.toUpperCase(Locale.ROOT)),
-                totalShardsFinal,
-                successfulShardsFinal,
-                skippedShardsFinal,
-                failedShardsFinal,
-                failures,
-                tookTimeValue,
-                timedOut
-            );
-        }
-
         public String getClusterAlias() {
             return clusterAlias;
         }

+ 1 - 1
server/src/test/java/org/elasticsearch/action/search/MultiSearchResponseTests.java

@@ -82,7 +82,7 @@ public class MultiSearchResponseTests extends ESTestCase {
     }
 
     private MultiSearchResponse doParseInstance(XContentParser parser) throws IOException {
-        return MultiSearchResponse.fromXContext(parser);
+        return SearchResponseUtils.parseMultiSearchResponse(parser);
     }
 
     private void assertEqualInstances(MultiSearchResponse expected, MultiSearchResponse actual) {

+ 2 - 2
server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java

@@ -299,7 +299,7 @@ public class SearchResponseTests extends ESTestCase {
             mutated = originalBytes;
         }
         try (XContentParser parser = createParser(xcontentType.xContent(), mutated)) {
-            SearchResponse parsed = SearchResponse.fromXContent(parser);
+            SearchResponse parsed = SearchResponseUtils.parseSearchResponse(parser);
             try {
                 assertToXContentEquivalent(
                     originalBytes,
@@ -336,7 +336,7 @@ public class SearchResponseTests extends ESTestCase {
             response.decRef();
         }
         try (XContentParser parser = createParser(xcontentType.xContent(), originalBytes)) {
-            SearchResponse parsed = SearchResponse.fromXContent(parser);
+            SearchResponse parsed = SearchResponseUtils.parseSearchResponse(parser);
             try {
                 for (int i = 0; i < parsed.getShardFailures().length; i++) {
                     ShardSearchFailure parsedFailure = parsed.getShardFailures()[i];

+ 326 - 1
test/framework/src/main/java/org/elasticsearch/search/SearchResponseUtils.java

@@ -8,13 +8,31 @@
 package org.elasticsearch.search;
 
 import org.apache.lucene.search.TotalHits;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.search.MultiSearchResponse;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.ShardSearchFailure;
 import org.elasticsearch.client.Response;
+import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
+import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.rest.action.RestActions;
+import org.elasticsearch.search.aggregations.InternalAggregations;
+import org.elasticsearch.search.profile.SearchProfileResults;
+import org.elasticsearch.search.suggest.Suggest;
 import org.elasticsearch.test.rest.ESRestTestCase;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
+import org.elasticsearch.xcontent.ParseField;
+import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
 
 public enum SearchResponseUtils {
     ;
@@ -34,7 +52,7 @@ public enum SearchResponseUtils {
 
     public static SearchResponse responseAsSearchResponse(Response searchResponse) throws IOException {
         try (var parser = ESRestTestCase.responseAsParser(searchResponse)) {
-            return SearchResponse.fromXContent(parser);
+            return parseSearchResponse(parser);
         }
     }
 
@@ -64,4 +82,311 @@ public enum SearchResponseUtils {
             clusters
         );
     }
+
+    public static SearchResponse parseSearchResponse(XContentParser parser) throws IOException {
+        ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
+        parser.nextToken();
+        return parseInnerSearchResponse(parser);
+    }
+
+    private static final ParseField RESPONSES = new ParseField(MultiSearchResponse.Fields.RESPONSES);
+    private static final ParseField TOOK_IN_MILLIS = new ParseField("took");
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<MultiSearchResponse, Void> MULTI_SEARCH_RESPONSE_PARSER = new ConstructingObjectParser<>(
+        "multi_search",
+        true,
+        a -> new MultiSearchResponse(((List<MultiSearchResponse.Item>) a[0]).toArray(new MultiSearchResponse.Item[0]), (long) a[1])
+    );
+    static {
+        MULTI_SEARCH_RESPONSE_PARSER.declareObjectArray(constructorArg(), (p, c) -> itemFromXContent(p), RESPONSES);
+        MULTI_SEARCH_RESPONSE_PARSER.declareLong(constructorArg(), TOOK_IN_MILLIS);
+    }
+
+    public static MultiSearchResponse parseMultiSearchResponse(XContentParser parser) {
+        return MULTI_SEARCH_RESPONSE_PARSER.apply(parser, null);
+    }
+
+    private static MultiSearchResponse.Item itemFromXContent(XContentParser parser) throws IOException {
+        // This parsing logic is a bit tricky here, because the multi search response itself is tricky:
+        // 1) The json objects inside the responses array are either a search response or a serialized exception
+        // 2) Each response json object gets a status field injected that ElasticsearchException.failureFromXContent(...) does not parse,
+        // but SearchResponse.innerFromXContent(...) parses and then ignores. The status field is not needed to parse
+        // the response item. However in both cases this method does need to parse the 'status' field otherwise the parsing of
+        // the response item in the next json array element will fail due to parsing errors.
+
+        MultiSearchResponse.Item item = null;
+        String fieldName = null;
+
+        XContentParser.Token token = parser.nextToken();
+        assert token == XContentParser.Token.FIELD_NAME;
+        outer: for (; token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) {
+            switch (token) {
+                case FIELD_NAME:
+                    fieldName = parser.currentName();
+                    if ("error".equals(fieldName)) {
+                        item = new MultiSearchResponse.Item(null, ElasticsearchException.failureFromXContent(parser));
+                    } else if ("status".equals(fieldName) == false) {
+                        item = new MultiSearchResponse.Item(parseInnerSearchResponse(parser), null);
+                        break outer;
+                    }
+                    break;
+                case VALUE_NUMBER:
+                    if ("status".equals(fieldName)) {
+                        // Ignore the status value
+                    }
+                    break;
+            }
+        }
+        assert parser.currentToken() == XContentParser.Token.END_OBJECT;
+        return item;
+    }
+
+    public static SearchResponse parseInnerSearchResponse(XContentParser parser) throws IOException {
+        ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser);
+        String currentFieldName = parser.currentName();
+        SearchHits hits = null;
+        InternalAggregations aggs = null;
+        Suggest suggest = null;
+        SearchProfileResults profile = null;
+        boolean timedOut = false;
+        Boolean terminatedEarly = null;
+        int numReducePhases = 1;
+        long tookInMillis = -1;
+        int successfulShards = -1;
+        int totalShards = -1;
+        int skippedShards = 0; // 0 for BWC
+        String scrollId = null;
+        String searchContextId = null;
+        List<ShardSearchFailure> failures = new ArrayList<>();
+        SearchResponse.Clusters clusters = SearchResponse.Clusters.EMPTY;
+        for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) {
+            if (token == XContentParser.Token.FIELD_NAME) {
+                currentFieldName = parser.currentName();
+            } else if (token.isValue()) {
+                if (SearchResponse.SCROLL_ID.match(currentFieldName, parser.getDeprecationHandler())) {
+                    scrollId = parser.text();
+                } else if (SearchResponse.POINT_IN_TIME_ID.match(currentFieldName, parser.getDeprecationHandler())) {
+                    searchContextId = parser.text();
+                } else if (SearchResponse.TOOK.match(currentFieldName, parser.getDeprecationHandler())) {
+                    tookInMillis = parser.longValue();
+                } else if (SearchResponse.TIMED_OUT.match(currentFieldName, parser.getDeprecationHandler())) {
+                    timedOut = parser.booleanValue();
+                } else if (SearchResponse.TERMINATED_EARLY.match(currentFieldName, parser.getDeprecationHandler())) {
+                    terminatedEarly = parser.booleanValue();
+                } else if (SearchResponse.NUM_REDUCE_PHASES.match(currentFieldName, parser.getDeprecationHandler())) {
+                    numReducePhases = parser.intValue();
+                } else {
+                    parser.skipChildren();
+                }
+            } else if (token == XContentParser.Token.START_OBJECT) {
+                if (SearchHits.Fields.HITS.equals(currentFieldName)) {
+                    hits = SearchHits.fromXContent(parser);
+                } else if (InternalAggregations.AGGREGATIONS_FIELD.equals(currentFieldName)) {
+                    aggs = InternalAggregations.fromXContent(parser);
+                } else if (Suggest.NAME.equals(currentFieldName)) {
+                    suggest = Suggest.fromXContent(parser);
+                } else if (SearchProfileResults.PROFILE_FIELD.equals(currentFieldName)) {
+                    profile = SearchProfileResults.fromXContent(parser);
+                } else if (RestActions._SHARDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    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.getDeprecationHandler())) {
+                                parser.intValue(); // we don't need it but need to consume it
+                            } else if (RestActions.SUCCESSFUL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                                successfulShards = parser.intValue();
+                            } else if (RestActions.TOTAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                                totalShards = parser.intValue();
+                            } else if (RestActions.SKIPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                                skippedShards = parser.intValue();
+                            } else {
+                                parser.skipChildren();
+                            }
+                        } else if (token == XContentParser.Token.START_ARRAY) {
+                            if (RestActions.FAILURES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                                while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
+                                    failures.add(ShardSearchFailure.fromXContent(parser));
+                                }
+                            } else {
+                                parser.skipChildren();
+                            }
+                        } else {
+                            parser.skipChildren();
+                        }
+                    }
+                } else if (SearchResponse.Clusters._CLUSTERS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    clusters = parseClusters(parser);
+                } else {
+                    parser.skipChildren();
+                }
+            }
+        }
+
+        return new SearchResponse(
+            hits,
+            aggs,
+            suggest,
+            timedOut,
+            terminatedEarly,
+            profile,
+            numReducePhases,
+            scrollId,
+            totalShards,
+            successfulShards,
+            skippedShards,
+            tookInMillis,
+            failures.toArray(ShardSearchFailure.EMPTY_ARRAY),
+            clusters,
+            searchContextId
+        );
+    }
+
+    private static SearchResponse.Clusters parseClusters(XContentParser parser) throws IOException {
+        XContentParser.Token token = parser.currentToken();
+        ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser);
+        int total = -1;
+        int successful = -1;
+        int skipped = -1;
+        int running = 0;    // 0 for BWC
+        int partial = 0;    // 0 for BWC
+        int failed = 0;     // 0 for BWC
+        Map<String, SearchResponse.Cluster> clusterInfoMap = ConcurrentCollections.newConcurrentMap();
+        String currentFieldName = null;
+        while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+            if (token == XContentParser.Token.FIELD_NAME) {
+                currentFieldName = parser.currentName();
+            } else if (token.isValue()) {
+                if (SearchResponse.Clusters.TOTAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    total = parser.intValue();
+                } else if (SearchResponse.Clusters.SUCCESSFUL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    successful = parser.intValue();
+                } else if (SearchResponse.Clusters.SKIPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    skipped = parser.intValue();
+                } else if (SearchResponse.Clusters.RUNNING_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    running = parser.intValue();
+                } else if (SearchResponse.Clusters.PARTIAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    partial = parser.intValue();
+                } else if (SearchResponse.Clusters.FAILED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    failed = parser.intValue();
+                } else {
+                    parser.skipChildren();
+                }
+            } else if (token == XContentParser.Token.START_OBJECT) {
+                if (SearchResponse.Clusters.DETAILS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    String currentDetailsFieldName = null;
+                    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+                        if (token == XContentParser.Token.FIELD_NAME) {
+                            currentDetailsFieldName = parser.currentName();  // cluster alias
+                        } else if (token == XContentParser.Token.START_OBJECT) {
+                            SearchResponse.Cluster c = parseCluster(currentDetailsFieldName, parser);
+                            clusterInfoMap.put(currentDetailsFieldName, c);
+                        } else {
+                            parser.skipChildren();
+                        }
+                    }
+                } else {
+                    parser.skipChildren();
+                }
+            } else {
+                parser.skipChildren();
+            }
+        }
+        if (clusterInfoMap.isEmpty()) {
+            assert running == 0 && partial == 0 && failed == 0
+                : "Non cross-cluster should have counter for running, partial and failed equal to 0";
+            return new SearchResponse.Clusters(total, successful, skipped);
+        } else {
+            return new SearchResponse.Clusters(clusterInfoMap);
+        }
+    }
+
+    private static SearchResponse.Cluster parseCluster(String clusterAlias, XContentParser parser) throws IOException {
+        XContentParser.Token token = parser.currentToken();
+        ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser);
+
+        String clusterName = clusterAlias;
+        if (clusterAlias.equals(SearchResponse.LOCAL_CLUSTER_NAME_REPRESENTATION)) {
+            clusterName = "";
+        }
+        String indexExpression = null;
+        String status = "running";
+        boolean timedOut = false;
+        long took = -1L;
+        // these are all from the _shards section
+        int totalShards = -1;
+        int successfulShards = -1;
+        int skippedShards = -1;
+        int failedShards = -1;
+        List<ShardSearchFailure> failures = new ArrayList<>();
+
+        String currentFieldName = null;
+        while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+            if (token == XContentParser.Token.FIELD_NAME) {
+                currentFieldName = parser.currentName();
+            } else if (token.isValue()) {
+                if (SearchResponse.Cluster.INDICES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    indexExpression = parser.text();
+                } else if (SearchResponse.Cluster.STATUS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    status = parser.text();
+                } else if (SearchResponse.TIMED_OUT.match(currentFieldName, parser.getDeprecationHandler())) {
+                    timedOut = parser.booleanValue();
+                } else if (SearchResponse.TOOK.match(currentFieldName, parser.getDeprecationHandler())) {
+                    took = parser.longValue();
+                } else {
+                    parser.skipChildren();
+                }
+            } else if (RestActions._SHARDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                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.getDeprecationHandler())) {
+                            failedShards = parser.intValue();
+                        } else if (RestActions.SUCCESSFUL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                            successfulShards = parser.intValue();
+                        } else if (RestActions.TOTAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                            totalShards = parser.intValue();
+                        } else if (RestActions.SKIPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                            skippedShards = parser.intValue();
+                        } else {
+                            parser.skipChildren();
+                        }
+                    } else {
+                        parser.skipChildren();
+                    }
+                }
+            } else if (token == XContentParser.Token.START_ARRAY) {
+                if (RestActions.FAILURES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
+                        failures.add(ShardSearchFailure.fromXContent(parser));
+                    }
+                } else {
+                    parser.skipChildren();
+                }
+            } else {
+                parser.skipChildren();
+            }
+        }
+
+        Integer totalShardsFinal = totalShards == -1 ? null : totalShards;
+        Integer successfulShardsFinal = successfulShards == -1 ? null : successfulShards;
+        Integer skippedShardsFinal = skippedShards == -1 ? null : skippedShards;
+        Integer failedShardsFinal = failedShards == -1 ? null : failedShards;
+        TimeValue tookTimeValue = took == -1L ? null : new TimeValue(took);
+        return new SearchResponse.Cluster(
+            clusterName,
+            indexExpression,
+            // skipUnavailable is not exposed to XContent, so just use default
+            SearchResponse.Cluster.SKIP_UNAVAILABLE_DEFAULT,
+            SearchResponse.Cluster.Status.valueOf(status.toUpperCase(Locale.ROOT)),
+            totalShardsFinal,
+            successfulShardsFinal,
+            skippedShardsFinal,
+            failedShardsFinal,
+            failures,
+            tookTimeValue,
+            timedOut
+        );
+    }
 }

+ 0 - 1
x-pack/plugin/async-search/qa/security/build.gradle

@@ -4,7 +4,6 @@ dependencies {
   javaRestTestImplementation(testArtifact(project(xpackModule('core'))))
   javaRestTestImplementation project(xpackModule('async-search'))
   javaRestTestImplementation project(':test:framework')
-  javaRestTestImplementation project(":client:rest-high-level")
 }
 
 tasks.named("javaRestTest").configure {

+ 44 - 3
x-pack/plugin/async-search/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/search/AsyncSearchSecurityIT.java

@@ -8,12 +8,12 @@
 package org.elasticsearch.xpack.search;
 
 import org.apache.http.util.EntityUtils;
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.client.ResponseException;
-import org.elasticsearch.client.asyncsearch.AsyncSearchResponse;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.settings.SecureString;
@@ -23,15 +23,20 @@ import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.local.distribution.DistributionType;
 import org.elasticsearch.test.cluster.util.resource.Resource;
 import org.elasticsearch.test.rest.ESRestTestCase;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
 import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentType;
 import org.elasticsearch.xcontent.json.JsonXContent;
 import org.elasticsearch.xpack.core.async.AsyncExecutionId;
+import org.elasticsearch.xpack.core.search.action.AsyncSearchResponse;
 import org.hamcrest.CustomMatcher;
 import org.hamcrest.Matcher;
 import org.junit.Before;
@@ -41,6 +46,9 @@ import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
+import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
 import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
 import static org.elasticsearch.xpack.core.XPackPlugin.ASYNC_RESULTS_INDEX;
 import static org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField.RUN_AS_USER_HEADER;
@@ -52,6 +60,38 @@ import static org.hamcrest.Matchers.greaterThan;
 
 public class AsyncSearchSecurityIT extends ESRestTestCase {
 
+    private static final ConstructingObjectParser<AsyncSearchResponse, Void> ASYNC_SEARCH_RESPONSE_PARSER = new ConstructingObjectParser<>(
+        "submit_async_search_response",
+        true,
+        args -> new AsyncSearchResponse(
+            (String) args[4],
+            (SearchResponse) args[5],
+            (ElasticsearchException) args[6],
+            (boolean) args[0],
+            (boolean) args[1],
+            (long) args[2],
+            (long) args[3]
+        )
+    );
+    static {
+        ASYNC_SEARCH_RESPONSE_PARSER.declareBoolean(constructorArg(), new ParseField("is_partial"));
+        ASYNC_SEARCH_RESPONSE_PARSER.declareBoolean(constructorArg(), new ParseField("is_running"));
+        ASYNC_SEARCH_RESPONSE_PARSER.declareLong(constructorArg(), new ParseField("start_time_in_millis"));
+        ASYNC_SEARCH_RESPONSE_PARSER.declareLong(constructorArg(), new ParseField("expiration_time_in_millis"));
+        ASYNC_SEARCH_RESPONSE_PARSER.declareString(optionalConstructorArg(), new ParseField("id"));
+        ASYNC_SEARCH_RESPONSE_PARSER.declareObject(optionalConstructorArg(), (p, c) -> {
+            // we should be before the opening START_OBJECT of the response
+            ensureExpectedToken(XContentParser.Token.START_OBJECT, p.currentToken(), p);
+            p.nextToken();
+            return SearchResponseUtils.parseInnerSearchResponse(p);
+        }, new ParseField("response"));
+        ASYNC_SEARCH_RESPONSE_PARSER.declareObject(
+            optionalConstructorArg(),
+            (p, c) -> ElasticsearchException.fromXContent(p),
+            new ParseField("error")
+        );
+    }
+
     @ClassRule
     public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
         .distribution(DistributionType.DEFAULT)
@@ -183,13 +223,14 @@ public class AsyncSearchSecurityIT extends ESRestTestCase {
     private SearchHit[] getSearchHits(String asyncId, String user) throws IOException {
         final Response resp = getAsyncSearch(asyncId, user);
         assertOK(resp);
-        SearchResponse searchResponse = AsyncSearchResponse.fromXContent(
+        SearchResponse searchResponse = ASYNC_SEARCH_RESPONSE_PARSER.apply(
             XContentHelper.createParser(
                 NamedXContentRegistry.EMPTY,
                 LoggingDeprecationHandler.INSTANCE,
                 new BytesArray(EntityUtils.toByteArray(resp.getEntity())),
                 XContentType.JSON
-            )
+            ),
+            null
         ).getSearchResponse();
         try {
             return searchResponse.getHits().asUnpooled().getHits();

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityWithMultipleRemotesRestIT.java

@@ -16,6 +16,7 @@ import org.elasticsearch.client.RestClient;
 import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.xcontent.ObjectPath;
 import org.junit.AfterClass;
@@ -216,7 +217,7 @@ public abstract class AbstractRemoteClusterSecurityWithMultipleRemotesRestIT ext
         assertOK(response);
         final SearchResponse searchResponse;
         try (var parser = responseAsParser(response)) {
-            searchResponse = SearchResponse.fromXContent(parser);
+            searchResponse = SearchResponseUtils.parseSearchResponse(parser);
         }
         try {
             final List<String> actualIndices = Arrays.stream(searchResponse.getHits().getHits())

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityApiKeyRestIT.java

@@ -15,6 +15,7 @@ import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.rest.ObjectPath;
 import org.junit.ClassRule;
@@ -185,7 +186,7 @@ public class RemoteClusterSecurityApiKeyRestIT extends AbstractRemoteClusterSecu
             assertOK(response);
             final SearchResponse searchResponse;
             try (var parser = responseAsParser(response)) {
-                searchResponse = SearchResponse.fromXContent(parser);
+                searchResponse = SearchResponseUtils.parseSearchResponse(parser);
             }
             try {
                 final List<String> actualIndices = Arrays.stream(searchResponse.getHits().getHits())

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityBwcRestIT.java

@@ -14,6 +14,7 @@ import org.elasticsearch.client.Response;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.local.distribution.DistributionType;
 import org.elasticsearch.test.cluster.util.Version;
@@ -191,7 +192,7 @@ public class RemoteClusterSecurityBwcRestIT extends AbstractRemoteClusterSecurit
             assertOK(response);
             final SearchResponse searchResponse;
             try (var parser = responseAsParser(response)) {
-                searchResponse = SearchResponse.fromXContent(parser);
+                searchResponse = SearchResponseUtils.parseSearchResponse(parser);
             }
             try {
                 final List<String> actualIndices = Arrays.stream(searchResponse.getHits().getHits())

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCcrIT.java

@@ -14,6 +14,7 @@ import org.elasticsearch.client.Response;
 import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.util.resource.Resource;
 import org.elasticsearch.test.rest.ObjectPath;
@@ -278,7 +279,7 @@ public class RemoteClusterSecurityCcrIT extends AbstractRemoteClusterSecurityTes
             assertOK(response);
             final SearchResponse searchResponse;
             try (var parser = responseAsParser(response)) {
-                searchResponse = SearchResponse.fromXContent(parser);
+                searchResponse = SearchResponseUtils.parseSearchResponse(parser);
             }
             try {
                 assertThat(searchResponse.getHits().getTotalHits().value, equalTo(numberOfDocs));

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCcrMigrationIT.java

@@ -17,6 +17,7 @@ import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.AnnotationTestOrdering;
 import org.elasticsearch.test.AnnotationTestOrdering.Order;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
@@ -359,7 +360,7 @@ public class RemoteClusterSecurityCcrMigrationIT extends AbstractRemoteClusterSe
                 throw new AssertionError(e);
             }
             assertOK(response);
-            final SearchResponse searchResponse = SearchResponse.fromXContent(responseAsParser(response));
+            final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response));
             try {
                 assertThat(searchResponse.getHits().getTotalHits().value, equalTo(numberOfDocs));
                 assertThat(

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityLicensingAndFeatureUsageRestIT.java

@@ -18,6 +18,7 @@ import org.elasticsearch.client.RestClient;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.rest.ObjectPath;
 import org.junit.ClassRule;
@@ -175,7 +176,7 @@ public class RemoteClusterSecurityLicensingAndFeatureUsageRestIT extends Abstrac
             // Check that we can search the fulfilling cluster from the querying cluster after license upgrade to trial.
             final Response response = performRequestWithRemoteSearchUser(searchRequest);
             assertOK(response);
-            final SearchResponse searchResponse = SearchResponse.fromXContent(responseAsParser(response));
+            final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response));
             try {
                 assertSearchResultContainsIndices(searchResponse, REMOTE_INDEX_NAME);
             } finally {

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityMutualTlsIT.java

@@ -13,6 +13,7 @@ import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.util.resource.Resource;
 import org.elasticsearch.test.junit.RunnableTestRuleAdapter;
@@ -114,7 +115,7 @@ public class RemoteClusterSecurityMutualTlsIT extends AbstractRemoteClusterSecur
                 "GET",
                 String.format(Locale.ROOT, "/my_remote_cluster:*/_search?ccs_minimize_roundtrips=%s", randomBoolean())
             );
-            final SearchResponse metricSearchResponse = SearchResponse.fromXContent(
+            final SearchResponse metricSearchResponse = SearchResponseUtils.parseSearchResponse(
                 responseAsParser(performRequestWithRemoteMetricUser(metricSearchRequest))
             );
             try {

+ 2 - 1
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityReloadCredentialsRestIT.java

@@ -16,6 +16,7 @@ import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.MutableSettingsProvider;
 import org.elasticsearch.test.cluster.util.resource.Resource;
@@ -212,7 +213,7 @@ public class RemoteClusterSecurityReloadCredentialsRestIT extends AbstractRemote
             )
         );
         assertOK(response);
-        final SearchResponse searchResponse = SearchResponse.fromXContent(responseAsParser(response));
+        final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response));
         try {
             final List<String> actualIndices = Arrays.stream(searchResponse.getHits().getHits())
                 .map(SearchHit::getIndex)

+ 3 - 2
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java

@@ -16,6 +16,7 @@ import org.elasticsearch.common.UUIDs;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.util.resource.Resource;
 import org.elasticsearch.test.junit.RunnableTestRuleAdapter;
@@ -223,7 +224,7 @@ public class RemoteClusterSecurityRestIT extends AbstractRemoteClusterSecurityTe
             );
             final Response response = performRequestWithRemoteSearchUser(searchRequest);
             assertOK(response);
-            final SearchResponse searchResponse = SearchResponse.fromXContent(responseAsParser(response));
+            final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response));
             try {
                 final List<String> actualIndices = Arrays.stream(searchResponse.getHits().getHits())
                     .map(SearchHit::getIndex)
@@ -242,7 +243,7 @@ public class RemoteClusterSecurityRestIT extends AbstractRemoteClusterSecurityTe
                 "GET",
                 String.format(Locale.ROOT, "/my_remote_cluster:*/_search?ccs_minimize_roundtrips=%s", randomBoolean())
             );
-            final SearchResponse metricSearchResponse = SearchResponse.fromXContent(
+            final SearchResponse metricSearchResponse = SearchResponseUtils.parseSearchResponse(
                 responseAsParser(performRequestWithRemoteMetricUser(metricSearchRequest))
             );
             try {

+ 5 - 4
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecuritySpecialUserIT.java

@@ -14,6 +14,7 @@ import org.elasticsearch.client.Response;
 import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.util.resource.Resource;
 import org.elasticsearch.test.junit.RunnableTestRuleAdapter;
@@ -137,7 +138,7 @@ public class RemoteClusterSecuritySpecialUserIT extends AbstractRemoteClusterSec
                 new Request("GET", "/my_remote_cluster:" + randomFrom("*", "shared-*", "shared-logs") + "/_search")
             );
             assertOK(response1);
-            final SearchResponse searchResponse1 = SearchResponse.fromXContent(responseAsParser(response1));
+            final SearchResponse searchResponse1 = SearchResponseUtils.parseSearchResponse(responseAsParser(response1));
             try {
                 assertThat(
                     Arrays.stream(searchResponse1.getHits().getHits()).map(SearchHit::getIndex).collect(Collectors.toList()),
@@ -181,7 +182,7 @@ public class RemoteClusterSecuritySpecialUserIT extends AbstractRemoteClusterSec
                 )
             );
             assertOK(response3);
-            final SearchResponse searchResponse3 = SearchResponse.fromXContent(responseAsParser(response3));
+            final SearchResponse searchResponse3 = SearchResponseUtils.parseSearchResponse(responseAsParser(response3));
             try {
                 assertThat(
                     Arrays.stream(searchResponse3.getHits().getHits()).map(SearchHit::getIndex).collect(Collectors.toList()),
@@ -201,7 +202,7 @@ public class RemoteClusterSecuritySpecialUserIT extends AbstractRemoteClusterSec
             kibanaServiceSearchRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + serviceToken));
             final Response kibanaServiceSearchResponse = client().performRequest(kibanaServiceSearchRequest);
             assertOK(kibanaServiceSearchResponse);
-            final SearchResponse searchResponse4 = SearchResponse.fromXContent(responseAsParser(kibanaServiceSearchResponse));
+            final SearchResponse searchResponse4 = SearchResponseUtils.parseSearchResponse(responseAsParser(kibanaServiceSearchResponse));
             try {
                 assertThat(
                     Arrays.stream(searchResponse4.getHits().getHits()).map(SearchHit::getIndex).collect(Collectors.toList()),
@@ -223,7 +224,7 @@ public class RemoteClusterSecuritySpecialUserIT extends AbstractRemoteClusterSec
             );
             final Response elasticUserSearchResponse = client().performRequest(elasticUserSearchRequest);
             assertOK(elasticUserSearchResponse);
-            final SearchResponse searchResponse5 = SearchResponse.fromXContent(responseAsParser(elasticUserSearchResponse));
+            final SearchResponse searchResponse5 = SearchResponseUtils.parseSearchResponse(responseAsParser(elasticUserSearchResponse));
             try {
                 assertThat(
                     Arrays.stream(searchResponse5.getHits().getHits()).map(SearchHit::getIndex).collect(Collectors.toList()),

+ 3 - 2
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityTopologyRestIT.java

@@ -13,6 +13,7 @@ import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.util.resource.Resource;
 import org.elasticsearch.test.junit.RunnableTestRuleAdapter;
@@ -127,7 +128,7 @@ public class RemoteClusterSecurityTopologyRestIT extends AbstractRemoteClusterSe
         {
             final var documentFieldValues = new HashSet<>();
             final var searchRequest = new Request("GET", "/my_remote_cluster:*/_search?scroll=1h&size=1");
-            final SearchResponse searchResponse = SearchResponse.fromXContent(
+            final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(
                 responseAsParser(performRequestWithRemoteMetricUser(searchRequest))
             );
             final Request scrollRequest = new Request("GET", "/_search/scroll");
@@ -148,7 +149,7 @@ public class RemoteClusterSecurityTopologyRestIT extends AbstractRemoteClusterSe
 
             // Fetch all documents
             for (int i = 0; i < 5; i++) {
-                final SearchResponse scrollResponse = SearchResponse.fromXContent(
+                final SearchResponse scrollResponse = SearchResponseUtils.parseSearchResponse(
                     responseAsParser(performRequestWithRemoteMetricUser(scrollRequest))
                 );
                 try {

+ 2 - 1
x-pack/qa/repository-old-versions/src/test/java/org/elasticsearch/oldrepos/OldRepositoryAccessIT.java

@@ -29,6 +29,7 @@ import org.elasticsearch.core.PathUtils;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchResponseUtils;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
 import org.elasticsearch.search.sort.SortBuilders;
 import org.elasticsearch.search.sort.SortOrder;
@@ -496,7 +497,7 @@ public class OldRepositoryAccessIT extends ESRestTestCase {
             request.setJsonEntity(builder.toString());
         }
         request.setOptions(options);
-        return SearchResponse.fromXContent(responseAsParser(client().performRequest(request)));
+        return SearchResponseUtils.parseSearchResponse(responseAsParser(client().performRequest(request)));
     }
 
     private int getIdAsNumeric(String id) {