Browse Source

Add XContent chunking to SearchResponse (#94736)

This commit adds xcontent chunking to SearchResponse and MultiSearchResponse
by making SearchHits implement ChunkedToXContent.

Relates to #89838
Alan Woodward 2 years ago
parent
commit
a3edf6b454
30 changed files with 259 additions and 156 deletions
  1. 2 2
      client/client-benchmark-noop-api-plugin/src/main/java/org/elasticsearch/plugin/noop/action/search/RestNoopSearchAction.java
  2. 2 1
      client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/AsyncSearchResponse.java
  3. 3 3
      modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java
  4. 4 6
      server/src/main/java/org/elasticsearch/ElasticsearchException.java
  5. 33 22
      server/src/main/java/org/elasticsearch/action/search/MultiSearchResponse.java
  6. 21 11
      server/src/main/java/org/elasticsearch/action/search/SearchResponse.java
  7. 32 15
      server/src/main/java/org/elasticsearch/action/search/SearchResponseSections.java
  8. 21 0
      server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentObject.java
  9. 20 0
      server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java
  10. 2 2
      server/src/main/java/org/elasticsearch/rest/action/search/RestKnnSearchAction.java
  11. 2 2
      server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java
  12. 2 2
      server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java
  13. 2 2
      server/src/main/java/org/elasticsearch/rest/action/search/RestSearchScrollAction.java
  14. 2 1
      server/src/main/java/org/elasticsearch/search/SearchHit.java
  15. 25 28
      server/src/main/java/org/elasticsearch/search/SearchHits.java
  16. 6 5
      server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalTopHits.java
  17. 2 1
      server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java
  18. 1 2
      server/src/main/java/org/elasticsearch/search/internal/InternalSearchResponse.java
  19. 31 31
      server/src/test/java/org/elasticsearch/action/search/MultiSearchResponseTests.java
  20. 13 2
      server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java
  21. 10 4
      server/src/test/java/org/elasticsearch/search/SearchHitsTests.java
  22. 4 3
      server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalTopHitsTests.java
  23. 2 1
      test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java
  24. 2 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncSearchResponse.java
  25. 5 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/Payload.java
  26. 2 2
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RestQuerySearchApplicationAction.java
  27. 2 2
      x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java
  28. 2 2
      x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetSearchAction.java
  29. 2 2
      x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/rest/RestRollupSearchAction.java
  30. 2 1
      x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java

+ 2 - 2
client/client-benchmark-noop-api-plugin/src/main/java/org/elasticsearch/plugin/noop/action/search/RestNoopSearchAction.java

@@ -11,7 +11,7 @@ import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
-import org.elasticsearch.rest.action.RestStatusToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 
 import java.util.List;
 
@@ -38,6 +38,6 @@ public class RestNoopSearchAction extends BaseRestHandler {
     @Override
     public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) {
         SearchRequest searchRequest = new SearchRequest();
-        return channel -> client.execute(NoopSearchAction.INSTANCE, searchRequest, new RestStatusToXContentListener<>(channel));
+        return channel -> client.execute(NoopSearchAction.INSTANCE, searchRequest, new RestChunkedToXContentListener<>(channel));
     }
 }

+ 2 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/AsyncSearchResponse.java

@@ -10,6 +10,7 @@ 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;
@@ -133,7 +134,7 @@ public class AsyncSearchResponse implements ToXContentObject {
 
         if (searchResponse != null) {
             builder.field("response");
-            searchResponse.toXContent(builder, params);
+            ChunkedToXContent.wrapAsToXContent(searchResponse).toXContent(builder, params);
         }
         if (error != null) {
             builder.startObject("error");

+ 3 - 3
modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java

@@ -13,6 +13,7 @@ import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.common.xcontent.StatusToXContentObject;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.xcontent.ParseField;
@@ -101,16 +102,15 @@ public class SearchTemplateResponse extends ActionResponse implements StatusToXC
         return builder;
     }
 
-    public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
+    void innerToXContent(XContentBuilder builder, Params params) throws IOException {
         if (hasResponse()) {
-            response.innerToXContent(builder, params);
+            ChunkedToXContent.wrapAsToXContent(p -> response.innerToXContentChunked(p)).toXContent(builder, params);
         } else {
             // we can assume the template is always json as we convert it before compiling it
             try (InputStream stream = source.streamInput()) {
                 builder.rawField(TEMPLATE_OUTPUT_FIELD.getPreferredName(), stream, XContentType.JSON);
             }
         }
-        return builder;
     }
 
     @Override

+ 4 - 6
server/src/main/java/org/elasticsearch/ElasticsearchException.java

@@ -570,12 +570,11 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
      * This method is usually used when the {@link Exception} is rendered as a full XContent object, and its output can be parsed
      * by the {@link #failureFromXContent(XContentParser)} method.
      */
-    public static void generateFailureXContent(XContentBuilder builder, Params params, @Nullable Exception e, boolean detailed)
+    public static XContentBuilder generateFailureXContent(XContentBuilder builder, Params params, @Nullable Exception e, boolean detailed)
         throws IOException {
         // No exception to render as an error
         if (e == null) {
-            builder.field(ERROR, "unknown");
-            return;
+            return builder.field(ERROR, "unknown");
         }
 
         // Render the exception with a simple message
@@ -589,8 +588,7 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
                 }
                 t = t.getCause();
             }
-            builder.field(ERROR, message);
-            return;
+            return builder.field(ERROR, message);
         }
 
         // Render the exception with all details
@@ -606,7 +604,7 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
             builder.endArray();
         }
         generateThrowableXContent(builder, params, e);
-        builder.endObject();
+        return builder.endObject();
     }
 
     /**

+ 33 - 22
server/src/main/java/org/elasticsearch/action/search/MultiSearchResponse.java

@@ -16,12 +16,14 @@ import org.elasticsearch.common.collect.Iterators;
 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.ChunkedToXContent;
+import org.elasticsearch.common.xcontent.ChunkedToXContentHelper;
+import org.elasticsearch.common.xcontent.ChunkedToXContentObject;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.xcontent.ConstructingObjectParser;
 import org.elasticsearch.xcontent.ParseField;
-import org.elasticsearch.xcontent.ToXContentObject;
-import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentParser.Token;
 
@@ -34,7 +36,7 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg
 /**
  * A multi search response.
  */
-public class MultiSearchResponse extends ActionResponse implements Iterable<MultiSearchResponse.Item>, ToXContentObject {
+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");
@@ -52,7 +54,7 @@ public class MultiSearchResponse extends ActionResponse implements Iterable<Mult
     /**
      * A search response item, holding the actual search response, or an error message if it failed.
      */
-    public static class Item implements Writeable {
+    public static class Item implements Writeable, ChunkedToXContent {
         private final SearchResponse response;
         private final Exception exception;
 
@@ -82,6 +84,25 @@ public class MultiSearchResponse extends ActionResponse implements Iterable<Mult
             }
         }
 
+        @Override
+        public Iterator<? extends ToXContent> toXContentChunked(ToXContent.Params params) {
+            if (isFailure()) {
+                return Iterators.concat(
+                    ChunkedToXContentHelper.startObject(),
+                    Iterators.single((b, p) -> ElasticsearchException.generateFailureXContent(b, p, Item.this.getFailure(), true)),
+                    Iterators.single((b, p) -> b.field(Fields.STATUS, ExceptionsHelper.status(Item.this.getFailure()).getStatus())),
+                    ChunkedToXContentHelper.endObject()
+                );
+            } else {
+                return Iterators.concat(
+                    ChunkedToXContentHelper.startObject(),
+                    Item.this.getResponse().innerToXContentChunked(params),
+                    Iterators.single((b, p) -> b.field(Fields.STATUS, Item.this.getResponse().status().getStatus())),
+                    ChunkedToXContentHelper.endObject()
+                );
+            }
+        }
+
         /**
          * Is it a failed search?
          */
@@ -150,24 +171,14 @@ public class MultiSearchResponse extends ActionResponse implements Iterable<Mult
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        builder.startObject();
-        builder.field("took", tookInMillis);
-        builder.startArray(Fields.RESPONSES);
-        for (Item item : items) {
-            builder.startObject();
-            if (item.isFailure()) {
-                ElasticsearchException.generateFailureXContent(builder, params, item.getFailure(), true);
-                builder.field(Fields.STATUS, ExceptionsHelper.status(item.getFailure()).getStatus());
-            } else {
-                item.getResponse().innerToXContent(builder, params);
-                builder.field(Fields.STATUS, item.getResponse().status().getStatus());
-            }
-            builder.endObject();
-        }
-        builder.endArray();
-        builder.endObject();
-        return builder;
+    public Iterator<? extends ToXContent> toXContentChunked(ToXContent.Params params) {
+        return Iterators.concat(
+            ChunkedToXContentHelper.startObject(),
+            Iterators.single((b, p) -> b.field("took", tookInMillis).startArray(Fields.RESPONSES)),
+            Iterators.flatMap(Iterators.forArray(items), item -> item.toXContentChunked(params)),
+            Iterators.single((b, p) -> b.endArray()),
+            ChunkedToXContentHelper.endObject()
+        );
     }
 
     public static MultiSearchResponse fromXContext(XContentParser parser) {

+ 21 - 11
server/src/main/java/org/elasticsearch/action/search/SearchResponse.java

@@ -12,10 +12,12 @@ import org.apache.lucene.search.TotalHits;
 import org.elasticsearch.TransportVersion;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.collect.Iterators;
 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.StatusToXContentObject;
+import org.elasticsearch.common.xcontent.ChunkedToXContentHelper;
+import org.elasticsearch.common.xcontent.ChunkedToXContentObject;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.rest.RestStatus;
@@ -29,6 +31,7 @@ import org.elasticsearch.search.profile.SearchProfileResults;
 import org.elasticsearch.search.profile.SearchProfileShardResult;
 import org.elasticsearch.search.suggest.Suggest;
 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;
@@ -36,6 +39,7 @@ import org.elasticsearch.xcontent.XContentParser.Token;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -47,7 +51,7 @@ import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpect
 /**
  * A response of a search request.
  */
-public class SearchResponse extends ActionResponse implements StatusToXContentObject {
+public class SearchResponse extends ActionResponse implements ChunkedToXContentObject {
 
     private static final ParseField SCROLL_ID = new ParseField("_scroll_id");
     private static final ParseField POINT_IN_TIME_ID = new ParseField("pit_id");
@@ -129,7 +133,6 @@ public class SearchResponse extends ActionResponse implements StatusToXContentOb
             : "SearchResponse can't have both scrollId [" + scrollId + "] and searchContextId [" + pointInTimeId + "]";
     }
 
-    @Override
     public RestStatus status() {
         return RestStatus.status(successfulShards, totalShards, shardFailures);
     }
@@ -264,14 +267,23 @@ public class SearchResponse extends ActionResponse implements StatusToXContentOb
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        builder.startObject();
-        innerToXContent(builder, params);
-        builder.endObject();
-        return builder;
+    public Iterator<? extends ToXContent> toXContentChunked(ToXContent.Params params) {
+        return Iterators.concat(
+            ChunkedToXContentHelper.startObject(),
+            this.innerToXContentChunked(params),
+            ChunkedToXContentHelper.endObject()
+        );
+    }
+
+    public Iterator<? extends ToXContent> innerToXContentChunked(ToXContent.Params params) {
+        return Iterators.concat(
+            ChunkedToXContentHelper.singleChunk(SearchResponse.this::headerToXContent),
+            Iterators.single(clusters),
+            internalResponse.toXContentChunked(params)
+        );
     }
 
-    public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
+    public XContentBuilder headerToXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
         if (scrollId != null) {
             builder.field(SCROLL_ID.getPreferredName(), scrollId);
         }
@@ -295,8 +307,6 @@ public class SearchResponse extends ActionResponse implements StatusToXContentOb
             getFailedShards(),
             getShardFailures()
         );
-        clusters.toXContent(builder, params);
-        internalResponse.toXContent(builder, params);
         return builder;
     }
 

+ 32 - 15
server/src/main/java/org/elasticsearch/action/search/SearchResponseSections.java

@@ -8,17 +8,19 @@
 
 package org.elasticsearch.action.search;
 
+import org.elasticsearch.common.collect.Iterators;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.search.SearchHits;
 import org.elasticsearch.search.aggregations.Aggregations;
 import org.elasticsearch.search.profile.SearchProfileResults;
 import org.elasticsearch.search.profile.SearchProfileShardResult;
 import org.elasticsearch.search.suggest.Suggest;
-import org.elasticsearch.xcontent.ToXContentFragment;
-import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.ToXContent;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.Map;
 
 /**
@@ -29,7 +31,7 @@ import java.util.Map;
  * to parse aggregations into, which are not serializable. This is the common part that can be
  * shared between core and client.
  */
-public class SearchResponseSections implements ToXContentFragment {
+public class SearchResponseSections implements ChunkedToXContent {
 
     protected final SearchHits hits;
     protected final Aggregations aggregations;
@@ -98,18 +100,33 @@ public class SearchResponseSections implements ToXContentFragment {
     }
 
     @Override
-    public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        hits.toXContent(builder, params);
-        if (aggregations != null) {
-            aggregations.toXContent(builder, params);
-        }
-        if (suggest != null) {
-            suggest.toXContent(builder, params);
-        }
-        if (profileResults != null) {
-            profileResults.toXContent(builder, params);
-        }
-        return builder;
+    public Iterator<? extends ToXContent> toXContentChunked(ToXContent.Params params) {
+        return Iterators.concat(
+            Iterators.flatMap(Iterators.single(hits), r -> r.toXContentChunked(params)),
+            Iterators.single((ToXContent) (b, p) -> {
+                if (aggregations != null) {
+                    aggregations.toXContent(b, p);
+                }
+                return b;
+            }),
+            Iterators.single((b, p) -> {
+                if (suggest != null) {
+                    suggest.toXContent(b, p);
+                }
+                return b;
+            }),
+            Iterators.single((b, p) -> {
+                if (profileResults != null) {
+                    profileResults.toXContent(b, p);
+                }
+                return b;
+            })
+        );
+    }
+
+    @Override
+    public boolean isFragment() {
+        return true;
     }
 
     protected void writeTo(StreamOutput out) throws IOException {

+ 21 - 0
server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentObject.java

@@ -7,6 +7,11 @@
  */
 package org.elasticsearch.common.xcontent;
 
+import org.elasticsearch.xcontent.ToXContent;
+import org.elasticsearch.xcontent.ToXContentObject;
+
+import java.util.Iterator;
+
 /**
  * Chunked equivalent of {@link org.elasticsearch.xcontent.ToXContentObject} that serializes as a full object.
  */
@@ -16,4 +21,20 @@ public interface ChunkedToXContentObject extends ChunkedToXContent {
     default boolean isFragment() {
         return false;
     }
+
+    /**
+     * Wraps the given instance in a {@link ToXContentObject} that will fully serialize the instance when serialized.
+     *
+     * @param chunkedToXContent instance to wrap
+     * @return x-content instance
+     */
+    static ToXContentObject wrapAsToXContentObject(ChunkedToXContentObject chunkedToXContent) {
+        return (builder, params) -> {
+            Iterator<? extends ToXContent> serialization = chunkedToXContent.toXContentChunked(params);
+            while (serialization.hasNext()) {
+                serialization.next().toXContent(builder, params);
+            }
+            return builder;
+        };
+    }
 }

+ 20 - 0
server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java

@@ -496,6 +496,16 @@ public class XContentHelper {
         return toXContent(toXContent, xContentType, ToXContent.EMPTY_PARAMS, humanReadable);
     }
 
+    /**
+     * Returns the bytes that represent the XContent output of the provided {@link ChunkedToXContent} object, using the provided
+     * {@link XContentType}. Wraps the output into a new anonymous object according to the value returned
+     * by the {@link ToXContent#isFragment()} method returns.
+     */
+    public static BytesReference toXContent(ChunkedToXContent toXContent, XContentType xContentType, boolean humanReadable)
+        throws IOException {
+        return toXContent(ChunkedToXContent.wrapAsToXContent(toXContent), xContentType, humanReadable);
+    }
+
     /**
      * Returns the bytes that represent the XContent output of the provided {@link ToXContent} object, using the provided
      * {@link XContentType}. Wraps the output into a new anonymous object according to the value returned
@@ -516,6 +526,16 @@ public class XContentHelper {
         }
     }
 
+    /**
+     * Returns the bytes that represent the XContent output of the provided {@link ChunkedToXContent} object, using the provided
+     * {@link XContentType}. Wraps the output into a new anonymous object according to the value returned
+     * by the {@link ToXContent#isFragment()} method returns.
+     */
+    public static BytesReference toXContent(ChunkedToXContent toXContent, XContentType xContentType, Params params, boolean humanReadable)
+        throws IOException {
+        return toXContent(ChunkedToXContent.wrapAsToXContent(toXContent), xContentType, params, humanReadable);
+    }
+
     /**
      * Guesses the content type based on the provided bytes which may be compressed.
      *

+ 2 - 2
server/src/main/java/org/elasticsearch/rest/action/search/RestKnnSearchAction.java

@@ -14,7 +14,7 @@ import org.elasticsearch.core.RestApiVersion;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.action.RestCancellableNodeClient;
-import org.elasticsearch.rest.action.RestStatusToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.search.vectors.KnnSearchRequestParser;
 
 import java.io.IOException;
@@ -55,6 +55,6 @@ public class RestKnnSearchAction extends BaseRestHandler {
         SearchRequestBuilder searchRequestBuilder = cancellableNodeClient.prepareSearch();
         parser.toSearchRequest(searchRequestBuilder);
 
-        return channel -> searchRequestBuilder.execute(new RestStatusToXContentListener<>(channel));
+        return channel -> searchRequestBuilder.execute(new RestChunkedToXContentListener<>(channel));
     }
 }

+ 2 - 2
server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java

@@ -26,7 +26,7 @@ import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.action.RestCancellableNodeClient;
-import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
 import org.elasticsearch.usage.SearchUsageHolder;
 import org.elasticsearch.xcontent.XContent;
@@ -82,7 +82,7 @@ public class RestMultiSearchAction extends BaseRestHandler {
         );
         return channel -> {
             final RestCancellableNodeClient cancellableClient = new RestCancellableNodeClient(client, request.getHttpChannel());
-            cancellableClient.execute(MultiSearchAction.INSTANCE, multiSearchRequest, new RestToXContentListener<>(channel));
+            cancellableClient.execute(MultiSearchAction.INSTANCE, multiSearchRequest, new RestChunkedToXContentListener<>(channel));
         };
     }
 

+ 2 - 2
server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java

@@ -29,7 +29,7 @@ import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.action.RestActions;
 import org.elasticsearch.rest.action.RestCancellableNodeClient;
-import org.elasticsearch.rest.action.RestStatusToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.search.Scroll;
 import org.elasticsearch.search.SearchService;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
@@ -121,7 +121,7 @@ public class RestSearchAction extends BaseRestHandler {
 
         return channel -> {
             RestCancellableNodeClient cancelClient = new RestCancellableNodeClient(client, request.getHttpChannel());
-            cancelClient.execute(SearchAction.INSTANCE, searchRequest, new RestStatusToXContentListener<>(channel));
+            cancelClient.execute(SearchAction.INSTANCE, searchRequest, new RestChunkedToXContentListener<>(channel));
         };
     }
 

+ 2 - 2
server/src/main/java/org/elasticsearch/rest/action/search/RestSearchScrollAction.java

@@ -14,7 +14,7 @@ import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
-import org.elasticsearch.rest.action.RestStatusToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.search.Scroll;
 import org.elasticsearch.xcontent.XContentParseException;
 
@@ -66,7 +66,7 @@ public class RestSearchScrollAction extends BaseRestHandler {
                 }
             }
         });
-        return channel -> client.searchScroll(searchScrollRequest, new RestStatusToXContentListener<>(channel));
+        return channel -> client.searchScroll(searchScrollRequest, new RestChunkedToXContentListener<>(channel));
     }
 
     @Override

+ 2 - 1
server/src/main/java/org/elasticsearch/search/SearchHit.java

@@ -21,6 +21,7 @@ import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.text.Text;
 import org.elasticsearch.common.util.Maps;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.core.Nullable;
@@ -782,7 +783,7 @@ public final class SearchHit implements Writeable, ToXContentObject, Iterable<Do
             builder.startObject(Fields.INNER_HITS);
             for (Map.Entry<String, SearchHits> entry : innerHits.entrySet()) {
                 builder.startObject(entry.getKey());
-                entry.getValue().toXContent(builder, params);
+                ChunkedToXContent.wrapAsToXContent(entry.getValue()).toXContent(builder, params);
                 builder.endObject();
             }
             builder.endObject();

+ 25 - 28
server/src/main/java/org/elasticsearch/search/SearchHits.java

@@ -16,10 +16,11 @@ 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.lucene.Lucene;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
+import org.elasticsearch.common.xcontent.ChunkedToXContentHelper;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.rest.action.search.RestSearchAction;
-import org.elasticsearch.xcontent.ToXContentFragment;
-import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
@@ -31,7 +32,7 @@ import java.util.Objects;
 
 import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
 
-public final class SearchHits implements Writeable, ToXContentFragment, Iterable<SearchHit> {
+public final class SearchHits implements Writeable, ChunkedToXContent, Iterable<SearchHit> {
 
     public static final SearchHit[] EMPTY = new SearchHit[0];
     public static final SearchHits EMPTY_WITH_TOTAL_HITS = new SearchHits(EMPTY, new TotalHits(0, Relation.EQUAL_TO), 0);
@@ -170,31 +171,27 @@ public final class SearchHits implements Writeable, ToXContentFragment, Iterable
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        builder.startObject(Fields.HITS);
-        boolean totalHitAsInt = params.paramAsBoolean(RestSearchAction.TOTAL_HITS_AS_INT_PARAM, false);
-        if (totalHitAsInt) {
-            long total = totalHits == null ? -1 : totalHits.value;
-            builder.field(Fields.TOTAL, total);
-        } else if (totalHits != null) {
-            builder.startObject(Fields.TOTAL);
-            builder.field("value", totalHits.value);
-            builder.field("relation", totalHits.relation == Relation.EQUAL_TO ? "eq" : "gte");
-            builder.endObject();
-        }
-        if (Float.isNaN(maxScore)) {
-            builder.nullField(Fields.MAX_SCORE);
-        } else {
-            builder.field(Fields.MAX_SCORE, maxScore);
-        }
-        builder.field(Fields.HITS);
-        builder.startArray();
-        for (SearchHit hit : hits) {
-            hit.toXContent(builder, params);
-        }
-        builder.endArray();
-        builder.endObject();
-        return builder;
+    public Iterator<? extends ToXContent> toXContentChunked(ToXContent.Params params) {
+        return Iterators.concat(Iterators.single((b, p) -> b.startObject(Fields.HITS)), Iterators.single((b, p) -> {
+            boolean totalHitAsInt = params.paramAsBoolean(RestSearchAction.TOTAL_HITS_AS_INT_PARAM, false);
+            if (totalHitAsInt) {
+                long total = totalHits == null ? -1 : totalHits.value;
+                b.field(Fields.TOTAL, total);
+            } else if (totalHits != null) {
+                b.startObject(Fields.TOTAL);
+                b.field("value", totalHits.value);
+                b.field("relation", totalHits.relation == Relation.EQUAL_TO ? "eq" : "gte");
+                b.endObject();
+            }
+            return b;
+        }), Iterators.single((b, p) -> {
+            if (Float.isNaN(maxScore)) {
+                b.nullField(Fields.MAX_SCORE);
+            } else {
+                b.field(Fields.MAX_SCORE, maxScore);
+            }
+            return b;
+        }), ChunkedToXContentHelper.array(Fields.HITS, Iterators.forArray(hits)), ChunkedToXContentHelper.endObject());
     }
 
     public static SearchHits fromXContent(XContentParser parser) throws IOException {

+ 6 - 5
server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalTopHits.java

@@ -17,6 +17,7 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.lucene.search.TopDocsAndMaxScore;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.SearchHits;
 import org.elasticsearch.search.aggregations.AggregationReduceContext;
@@ -33,10 +34,10 @@ import java.util.Map;
  * Results of the {@link TopHitsAggregator}.
  */
 public class InternalTopHits extends InternalAggregation implements TopHits {
-    private int from;
-    private int size;
-    private TopDocsAndMaxScore topDocs;
-    private SearchHits searchHits;
+    private final int from;
+    private final int size;
+    private final TopDocsAndMaxScore topDocs;
+    private final SearchHits searchHits;
 
     public InternalTopHits(
         String name,
@@ -191,7 +192,7 @@ public class InternalTopHits extends InternalAggregation implements TopHits {
 
     @Override
     public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
-        searchHits.toXContent(builder, params);
+        ChunkedToXContent.wrapAsToXContent(searchHits).toXContent(builder, params);
         return builder;
     }
 

+ 2 - 1
server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.search.aggregations.metrics;
 
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.search.SearchHits;
 import org.elasticsearch.search.aggregations.ParsedAggregation;
 import org.elasticsearch.xcontent.ObjectParser;
@@ -33,7 +34,7 @@ public class ParsedTopHits extends ParsedAggregation implements TopHits {
 
     @Override
     protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
-        return searchHits.toXContent(builder, params);
+        return ChunkedToXContent.wrapAsToXContent(searchHits).toXContent(builder, params);
     }
 
     private static final ObjectParser<ParsedTopHits, Void> PARSER = new ObjectParser<>(

+ 1 - 2
server/src/main/java/org/elasticsearch/search/internal/InternalSearchResponse.java

@@ -16,14 +16,13 @@ import org.elasticsearch.search.SearchHits;
 import org.elasticsearch.search.aggregations.InternalAggregations;
 import org.elasticsearch.search.profile.SearchProfileResults;
 import org.elasticsearch.search.suggest.Suggest;
-import org.elasticsearch.xcontent.ToXContentFragment;
 
 import java.io.IOException;
 
 /**
  * {@link SearchResponseSections} subclass that can be serialized over the wire.
  */
-public class InternalSearchResponse extends SearchResponseSections implements Writeable, ToXContentFragment {
+public class InternalSearchResponse extends SearchResponseSections implements Writeable {
     public static final InternalSearchResponse EMPTY_WITH_TOTAL_HITS = new InternalSearchResponse(
         SearchHits.EMPTY_WITH_TOTAL_HITS,
         null,

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

@@ -8,24 +8,26 @@
 package org.elasticsearch.action.search;
 
 import org.elasticsearch.ElasticsearchException;
-import org.elasticsearch.common.Strings;
 import org.elasticsearch.search.internal.InternalSearchResponse;
-import org.elasticsearch.test.AbstractXContentTestCase;
+import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.util.function.Predicate;
-import java.util.function.Supplier;
 
+import static org.elasticsearch.test.AbstractXContentTestCase.chunkedXContentTester;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.nullValue;
 
-public class MultiSearchResponseTests extends AbstractXContentTestCase<MultiSearchResponse> {
+public class MultiSearchResponseTests extends ESTestCase {
 
-    @Override
-    protected MultiSearchResponse createTestInstance() {
+    // We can not subclass AbstractSerializingTestCase because it
+    // can only be used for instances with equals and hashCode
+    // MultiSearchResponse does not override equals and hashCode.
+
+    private MultiSearchResponse createTestInstance() {
         int numItems = randomIntBetween(0, 128);
         MultiSearchResponse.Item[] items = new MultiSearchResponse.Item[numItems];
         for (int i = 0; i < numItems; i++) {
@@ -84,13 +86,11 @@ public class MultiSearchResponseTests extends AbstractXContentTestCase<MultiSear
         return new MultiSearchResponse(items, randomNonNegativeLong());
     }
 
-    @Override
-    protected MultiSearchResponse doParseInstance(XContentParser parser) throws IOException {
+    private MultiSearchResponse doParseInstance(XContentParser parser) throws IOException {
         return MultiSearchResponse.fromXContext(parser);
     }
 
-    @Override
-    protected void assertEqualInstances(MultiSearchResponse expected, MultiSearchResponse actual) {
+    private void assertEqualInstances(MultiSearchResponse expected, MultiSearchResponse actual) {
         assertThat(actual.getTook(), equalTo(expected.getTook()));
         assertThat(actual.getResponses().length, equalTo(expected.getResponses().length));
         for (int i = 0; i < expected.getResponses().length; i++) {
@@ -106,8 +106,7 @@ public class MultiSearchResponseTests extends AbstractXContentTestCase<MultiSear
         }
     }
 
-    @Override
-    protected boolean supportsUnknownFields() {
+    private boolean supportsUnknownFields() {
         return true;
     }
 
@@ -115,30 +114,31 @@ public class MultiSearchResponseTests extends AbstractXContentTestCase<MultiSear
         return field -> field.startsWith("responses");
     }
 
+    public final void testFromXContent() throws IOException {
+        chunkedXContentTester(this::createParser, t -> createTestInstance(), ToXContent.EMPTY_PARAMS, this::doParseInstance)
+            .numberOfTestRuns(20)
+            .supportsUnknownFields(supportsUnknownFields())
+            .assertEqualsConsumer(this::assertEqualInstances)
+            .test();
+    }
+
     /**
      * Test parsing {@link MultiSearchResponse} with inner failures as they don't support asserting on xcontent equivalence, given that
-     * exceptions are not parsed back as the same original class. We run the usual {@link AbstractXContentTestCase#testFromXContent()}
+     * exceptions are not parsed back as the same original class. We run the usual
+     * {@link org.elasticsearch.test.AbstractSerializationTestCase#testFromXContent()}
      * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end.
      */
     public void testFromXContentWithFailures() throws IOException {
-        Supplier<MultiSearchResponse> instanceSupplier = MultiSearchResponseTests::createTestInstanceWithFailures;
-        // with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata,
-        // but that does not bother our assertions, as we only want to test that we don't break.
-        boolean supportsUnknownFields = true;
-        // exceptions are not of the same type whenever parsed back
-        boolean assertToXContentEquivalence = false;
-        AbstractXContentTestCase.testFromXContent(
-            NUMBER_OF_TEST_RUNS,
-            instanceSupplier,
-            supportsUnknownFields,
-            Strings.EMPTY_ARRAY,
-            getRandomFieldsExcludeFilterWhenResultHasErrors(),
-            this::createParser,
-            this::doParseInstance,
-            this::assertEqualInstances,
-            assertToXContentEquivalence,
-            ToXContent.EMPTY_PARAMS
-        );
+        chunkedXContentTester(this::createParser, t -> createTestInstanceWithFailures(), ToXContent.EMPTY_PARAMS, this::doParseInstance)
+            .numberOfTestRuns(20)
+            .randomFieldsExcludeFilter(getRandomFieldsExcludeFilterWhenResultHasErrors())
+            // with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata,
+            // but that does not bother our assertions, as we only want to test that we don't break.
+            .supportsUnknownFields(true)
+            // exceptions are not of the same type whenever parsed back
+            .assertToXContentEquivalence(false)
+            .assertEqualsConsumer(this::assertEqualInstances)
+            .test();
     }
 
 }

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

@@ -15,6 +15,7 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.search.SearchHit;
@@ -159,7 +160,12 @@ public class SearchResponseTests extends ESTestCase {
         XContentType xcontentType = randomFrom(XContentType.values());
         boolean humanReadable = randomBoolean();
         final ToXContent.Params params = new ToXContent.MapParams(singletonMap(RestSearchAction.TYPED_KEYS_PARAM, "true"));
-        BytesReference originalBytes = toShuffledXContent(response, xcontentType, params, humanReadable);
+        BytesReference originalBytes = toShuffledXContent(
+            ChunkedToXContent.wrapAsToXContent(response),
+            xcontentType,
+            params,
+            humanReadable
+        );
         BytesReference mutated;
         if (addRandomFields) {
             mutated = insertRandomFields(xcontentType, originalBytes, null, random());
@@ -189,7 +195,12 @@ public class SearchResponseTests extends ESTestCase {
         SearchResponse response = createTestItem(failures);
         XContentType xcontentType = randomFrom(XContentType.values());
         final ToXContent.Params params = new ToXContent.MapParams(singletonMap(RestSearchAction.TYPED_KEYS_PARAM, "true"));
-        BytesReference originalBytes = toShuffledXContent(response, xcontentType, params, randomBoolean());
+        BytesReference originalBytes = toShuffledXContent(
+            ChunkedToXContent.wrapAsToXContent(response),
+            xcontentType,
+            params,
+            randomBoolean()
+        );
         try (XContentParser parser = createParser(xcontentType.xContent(), originalBytes)) {
             SearchResponse parsed = SearchResponse.fromXContent(parser);
             for (int i = 0; i < parsed.getShardFailures().length; i++) {

+ 10 - 4
server/src/test/java/org/elasticsearch/search/SearchHitsTests.java

@@ -15,11 +15,12 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.lucene.LuceneTests;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.index.shard.ShardId;
-import org.elasticsearch.test.AbstractXContentSerializingTestCase;
+import org.elasticsearch.test.AbstractChunkedSerializingTestCase;
 import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
@@ -29,7 +30,7 @@ import org.elasticsearch.xcontent.json.JsonXContent;
 import java.io.IOException;
 import java.util.function.Predicate;
 
-public class SearchHitsTests extends AbstractXContentSerializingTestCase<SearchHits> {
+public class SearchHitsTests extends AbstractChunkedSerializingTestCase<SearchHits> {
 
     public static SearchHits createTestItem(boolean withOptionalInnerHits, boolean withShardTarget) {
         return createTestItem(randomFrom(XContentType.values()).canonical(), withOptionalInnerHits, withShardTarget);
@@ -233,7 +234,7 @@ public class SearchHitsTests extends AbstractXContentSerializingTestCase<SearchH
         SearchHits searchHits = new SearchHits(hits, new TotalHits(totalHits, TotalHits.Relation.EQUAL_TO), maxScore);
         XContentBuilder builder = JsonXContent.contentBuilder();
         builder.startObject();
-        searchHits.toXContent(builder, ToXContent.EMPTY_PARAMS);
+        ChunkedToXContent.wrapAsToXContent(searchHits).toXContent(builder, ToXContent.EMPTY_PARAMS);
         builder.endObject();
         assertEquals(XContentHelper.stripWhitespace("""
             {
@@ -270,7 +271,12 @@ public class SearchHitsTests extends AbstractXContentSerializingTestCase<SearchH
             float maxScore = 1.5f;
             SearchHits searchHits = new SearchHits(hits, new TotalHits(totalHits, TotalHits.Relation.EQUAL_TO), maxScore);
             XContentType xContentType = randomFrom(XContentType.values()).canonical();
-            BytesReference bytes = toShuffledXContent(searchHits, xContentType, ToXContent.EMPTY_PARAMS, false);
+            BytesReference bytes = toShuffledXContent(
+                ChunkedToXContent.wrapAsToXContent(searchHits),
+                xContentType,
+                ToXContent.EMPTY_PARAMS,
+                false
+            );
             try (
                 XContentParser parser = xContentType.xContent()
                     .createParser(xContentRegistry(), LoggingDeprecationHandler.INSTANCE, bytes.streamInput())

+ 4 - 3
server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalTopHitsTests.java

@@ -21,6 +21,7 @@ import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.document.DocumentField;
 import org.elasticsearch.common.lucene.search.TopDocsAndMaxScore;
 import org.elasticsearch.common.util.Maps;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.search.SearchHit;
@@ -288,7 +289,7 @@ public class InternalTopHitsTests extends InternalAggregationTestCase<InternalTo
      * Assert that two objects are equals, calling {@link ToXContent#toXContent(XContentBuilder, ToXContent.Params)} to print out their
      * differences if they aren't equal.
      */
-    private static <T extends ToXContent> void assertEqualsWithErrorMessageFromXContent(T expected, T actual) {
+    private static <T extends ChunkedToXContent> void assertEqualsWithErrorMessageFromXContent(T expected, T actual) {
         if (Objects.equals(expected, actual)) {
             return;
         }
@@ -300,10 +301,10 @@ public class InternalTopHitsTests extends InternalAggregationTestCase<InternalTo
         }
         try (XContentBuilder actualJson = JsonXContent.contentBuilder(); XContentBuilder expectedJson = JsonXContent.contentBuilder()) {
             actualJson.startObject();
-            actual.toXContent(actualJson, ToXContent.EMPTY_PARAMS);
+            ChunkedToXContent.wrapAsToXContent(actual).toXContent(actualJson, ToXContent.EMPTY_PARAMS);
             actualJson.endObject();
             expectedJson.startObject();
-            expected.toXContent(expectedJson, ToXContent.EMPTY_PARAMS);
+            ChunkedToXContent.wrapAsToXContent(expected).toXContent(expectedJson, ToXContent.EMPTY_PARAMS);
             expectedJson.endObject();
             NotEqualMessageBuilder message = new NotEqualMessageBuilder();
             message.compareMaps(

+ 2 - 1
test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java

@@ -14,6 +14,7 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.document.DocumentField;
 import org.elasticsearch.common.geo.SpatialPoint;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.geometry.utils.Geohash;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.sort.SortBuilders;
@@ -236,7 +237,7 @@ public abstract class AbstractGeoTestCase extends ESIntegTestCase {
         assertSearchResponse(response);
         long totalHits = response.getHits().getTotalHits().value;
         XContentBuilder builder = XContentFactory.jsonBuilder();
-        response.toXContent(builder, ToXContent.EMPTY_PARAMS);
+        ChunkedToXContent.wrapAsToXContent(response).toXContent(builder, ToXContent.EMPTY_PARAMS);
         logger.info("Full high_card_idx Response Content:\n{ {} }", Strings.toString(builder));
         for (int i = 0; i < totalHits; i++) {
             SearchHit searchHit = response.getHits().getAt(i);

+ 2 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncSearchResponse.java

@@ -12,6 +12,7 @@ import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.common.xcontent.StatusToXContentObject;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.rest.RestStatus;
@@ -194,7 +195,7 @@ public class AsyncSearchResponse extends ActionResponse implements StatusToXCont
 
         if (searchResponse != null) {
             builder.field("response");
-            searchResponse.toXContent(builder, params);
+            ChunkedToXContent.wrapAsToXContent(searchResponse).toXContent(builder, params);
         }
         if (error != null) {
             builder.startObject("error");

+ 5 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/Payload.java

@@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.watcher.watch;
 
 import org.elasticsearch.common.collect.MapBuilder;
 import org.elasticsearch.common.util.CollectionUtils;
+import org.elasticsearch.common.xcontent.ChunkedToXContentObject;
 import org.elasticsearch.xcontent.ToXContentObject;
 import org.elasticsearch.xcontent.XContentBuilder;
 
@@ -79,5 +80,9 @@ public interface Payload extends ToXContentObject {
         public XContent(ToXContentObject response, Params params) throws IOException {
             super(responseToData(response, params));
         }
+
+        public XContent(ChunkedToXContentObject response, Params params) throws IOException {
+            this(ChunkedToXContentObject.wrapAsToXContentObject(response), params);
+        }
     }
 }

+ 2 - 2
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RestQuerySearchApplicationAction.java

@@ -12,7 +12,7 @@ import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
-import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.xpack.application.EnterpriseSearch;
 
 import java.io.IOException;
@@ -46,6 +46,6 @@ public class RestQuerySearchApplicationAction extends BaseRestHandler {
             request = new SearchApplicationSearchRequest(searchAppName);
         }
         final SearchApplicationSearchRequest finalRequest = request;
-        return channel -> client.execute(QuerySearchApplicationAction.INSTANCE, finalRequest, new RestToXContentListener<>(channel));
+        return channel -> client.execute(QuerySearchApplicationAction.INSTANCE, finalRequest, new RestChunkedToXContentListener<>(channel));
     }
 }

+ 2 - 2
x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java

@@ -18,7 +18,7 @@ import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.action.RestCancellableNodeClient;
-import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.rest.action.search.RestMultiSearchAction;
 import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.usage.SearchUsageHolder;
@@ -112,7 +112,7 @@ public class RestFleetMultiSearchAction extends BaseRestHandler {
 
         return channel -> {
             final RestCancellableNodeClient cancellableClient = new RestCancellableNodeClient(client, request.getHttpChannel());
-            cancellableClient.execute(MultiSearchAction.INSTANCE, multiSearchRequest, new RestToXContentListener<>(channel));
+            cancellableClient.execute(MultiSearchAction.INSTANCE, multiSearchRequest, new RestChunkedToXContentListener<>(channel));
         };
     }
 

+ 2 - 2
x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetSearchAction.java

@@ -18,7 +18,7 @@ import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.action.RestCancellableNodeClient;
-import org.elasticsearch.rest.action.RestStatusToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.usage.SearchUsageHolder;
 
@@ -96,7 +96,7 @@ public class RestFleetSearchAction extends BaseRestHandler {
 
         return channel -> {
             RestCancellableNodeClient cancelClient = new RestCancellableNodeClient(client, request.getHttpChannel());
-            cancelClient.execute(SearchAction.INSTANCE, searchRequest, new RestStatusToXContentListener<>(channel));
+            cancelClient.execute(SearchAction.INSTANCE, searchRequest, new RestChunkedToXContentListener<>(channel));
         };
     }
 

+ 2 - 2
x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/rest/RestRollupSearchAction.java

@@ -10,7 +10,7 @@ import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
-import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.rest.action.RestChunkedToXContentListener;
 import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction;
 
@@ -48,7 +48,7 @@ public class RestRollupSearchAction extends BaseRestHandler {
             )
         );
         RestSearchAction.validateSearchRequest(restRequest, searchRequest);
-        return channel -> client.execute(RollupSearchAction.INSTANCE, searchRequest, new RestToXContentListener<>(channel));
+        return channel -> client.execute(RollupSearchAction.INSTANCE, searchRequest, new RestChunkedToXContentListener<>(channel));
     }
 
     @Override

+ 2 - 1
x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java

@@ -19,6 +19,7 @@ import org.elasticsearch.common.geo.GeoPoint;
 import org.elasticsearch.common.geo.GeoUtils;
 import org.elasticsearch.common.io.Streams;
 import org.elasticsearch.common.io.stream.BytesStream;
+import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.geometry.Rectangle;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -414,7 +415,7 @@ public class RestVectorTileAction extends BaseRestHandler {
             final Rectangle tile = request.getBoundingBox();
             featureBuilder.mergeFrom(featureFactory.box(tile.getMinLon(), tile.getMaxLon(), tile.getMinLat(), tile.getMaxLat()));
         }
-        VectorTileUtils.addToXContentToFeature(featureBuilder, layerProps, response);
+        VectorTileUtils.addToXContentToFeature(featureBuilder, layerProps, ChunkedToXContent.wrapAsToXContent(response));
         metaLayerBuilder.addFeatures(featureBuilder);
         VectorTileUtils.addPropertiesToLayer(metaLayerBuilder, layerProps);
         return metaLayerBuilder;