From 73ad40383e8718c3eb1d79ac7812a3e357649186 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 7 Dec 2022 13:37:36 +0000 Subject: [PATCH 1/3] Introduce Iterators#flatMap Various places that implement response chunking involving some kind of nested structure (e.g. a list of lists) need to call something like `flatMap`, and today this means they must must convert between iterators and streams to make use of `Stream#flatMap`. This is noisy and awkward, so with this commit we introduce a way to `flatMap` directly on iterators. Relates #89838 --- .../node/tasks/list/ListTasksResponse.java | 14 ++- .../status/SnapshotsStatusResponse.java | 14 +-- .../cluster/metadata/IndexGraveyard.java | 2 +- .../cluster/metadata/Metadata.java | 22 ++--- .../common/collect/Iterators.java | 91 +++++++++++++++---- .../xcontent/ChunkedToXContentHelper.java | 4 +- .../org/elasticsearch/health/Diagnosis.java | 27 ++---- .../health/HealthIndicatorResult.java | 18 +--- .../elasticsearch/ingest/IngestMetadata.java | 2 +- .../PersistentTasksCustomMetadata.java | 2 +- .../common/collect/IteratorsTests.java | 30 ++++++ 11 files changed, 136 insertions(+), 90 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java index 2d65e128a657f..1937effdaeda6 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java @@ -32,10 +32,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; @@ -166,9 +164,9 @@ public ChunkedToXContent groupedByNode(Supplier nodesInCluster) toXContentCommon(builder, params); builder.startObject("nodes"); return builder; - }), getPerNodeTasks().entrySet().stream().flatMap(entry -> { + }), Iterators.flatMap(getPerNodeTasks().entrySet().iterator(), entry -> { DiscoveryNode node = discoveryNodes.get(entry.getKey()); - return Stream.>of(Stream.of((builder, params) -> { + return Iterators.concat(Iterators.single((builder, params) -> { builder.startObject(entry.getKey()); if (node != null) { // If the node is no longer part of the cluster, oh well, we'll just skip its useful information. @@ -193,17 +191,17 @@ public ChunkedToXContent groupedByNode(Supplier nodesInCluster) } builder.startObject(TASKS); return builder; - }), entry.getValue().stream().map(task -> (builder, params) -> { + }), Iterators.flatMap(entry.getValue().iterator(), task -> Iterators.single((builder, params) -> { builder.startObject(task.taskId().toString()); task.toXContent(builder, params); builder.endObject(); return builder; - }), Stream.of((builder, params) -> { + })), Iterators.single((builder, params) -> { builder.endObject(); builder.endObject(); return builder; - })).flatMap(Function.identity()); - }).iterator(), Iterators.single((builder, params) -> { + })); + }), Iterators.single((builder, params) -> { builder.endObject(); builder.endObject(); return builder; diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java index 3ac90f882ab25..1ddc9e895e91e 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java @@ -13,6 +13,7 @@ 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.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; @@ -22,9 +23,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.StreamSupport; import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; @@ -90,14 +88,6 @@ public int hashCode() { @Override public Iterator toXContentChunked(ToXContent.Params params) { - return Iterators.concat( - Iterators.single((ToXContent) (b, p) -> b.startObject().startArray("snapshots")), - snapshots.stream() - .flatMap( - s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(params), Spliterator.ORDERED), false) - ) - .iterator(), - Iterators.single((b, p) -> b.endArray().endObject()) - ); + return ChunkedToXContentHelper.array("snapshots", Iterators.flatMap(snapshots.iterator(), s -> s.toXContentChunked(params))); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java index 900c4ff80f00e..bf628c7f5ce80 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java @@ -127,7 +127,7 @@ public boolean containsIndex(final Index index) { @Override public Iterator toXContentChunked(ToXContent.Params ignored) { - return ChunkedToXContentHelper.array(TOMBSTONES_FIELD.getPreferredName(), tombstones); + return ChunkedToXContentHelper.array(TOMBSTONES_FIELD.getPreferredName(), tombstones.iterator()); } public static IndexGraveyard fromXContent(final XContentParser parser) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 46670b84d42ca..4eb9ced4f4357 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -75,8 +75,6 @@ import java.util.Optional; import java.util.Set; import java.util.SortedMap; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.TreeMap; import java.util.function.BiPredicate; import java.util.function.Consumer; @@ -84,7 +82,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.common.settings.Settings.readSettingsFromStream; @@ -1340,7 +1337,7 @@ public Iterator toXContentChunked(ToXContent.Params p) { ? ChunkedToXContentHelper.wrapWithObject("indices", indices().values().iterator()) : Collections.emptyIterator(); - return Iterators.concat(start, Iterators.single((builder, params) -> { + return Iterators.concat(start, Iterators.single((builder, params) -> { builder.field("cluster_uuid", clusterUUID); builder.field("cluster_uuid_committed", clusterUUIDCommitted); builder.startObject("cluster_coordination"); @@ -1362,17 +1359,12 @@ public Iterator toXContentChunked(ToXContent.Params p) { .iterator() ), indices, - customs().entrySet() - .stream() - .filter(cursor -> cursor.getValue().context().contains(context)) - .map( - cursor -> (ChunkedToXContent) params -> ChunkedToXContentHelper.wrapWithObject( - cursor.getKey(), - cursor.getValue().toXContentChunked(params) - ) - ) - .flatMap(s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(p), Spliterator.ORDERED), false)) - .iterator(), + Iterators.flatMap( + customs.entrySet().iterator(), + entry -> entry.getValue().context().contains(context) + ? ChunkedToXContentHelper.wrapWithObject(entry.getKey(), entry.getValue().toXContentChunked(p)) + : Collections.emptyIterator() + ), ChunkedToXContentHelper.wrapWithObject("reserved_state", reservedStateMetadata().values().iterator()), ChunkedToXContentHelper.endObject() ); diff --git a/server/src/main/java/org/elasticsearch/common/collect/Iterators.java b/server/src/main/java/org/elasticsearch/common/collect/Iterators.java index a2629ffd0556c..2f78ba70529cd 100644 --- a/server/src/main/java/org/elasticsearch/common/collect/Iterators.java +++ b/server/src/main/java/org/elasticsearch/common/collect/Iterators.java @@ -8,9 +8,13 @@ package org.elasticsearch.common.collect; +import org.elasticsearch.core.Nullable; + +import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.function.Function; public class Iterators { @@ -43,36 +47,33 @@ public static Iterator concat(Iterator... iterators) { throw new NullPointerException("iterators"); } - // explicit generic type argument needed for type inference - return new ConcatenatedIterator(iterators); + for (int i = 0; i < iterators.length; i++) { + if (iterators[i].hasNext()) { + // explicit generic type argument needed for type inference + return new ConcatenatedIterator(iterators, i); + } + } + + return Collections.emptyIterator(); } - static class ConcatenatedIterator implements Iterator { + private static class ConcatenatedIterator implements Iterator { private final Iterator[] iterators; - private int index = 0; + private int index; - @SafeVarargs - @SuppressWarnings("varargs") - ConcatenatedIterator(Iterator... iterators) { - if (iterators == null) { - throw new NullPointerException("iterators"); - } - for (int i = 0; i < iterators.length; i++) { + ConcatenatedIterator(Iterator[] iterators, int startIndex) { + for (int i = startIndex; i < iterators.length; i++) { if (iterators[i] == null) { throw new NullPointerException("iterators[" + i + "]"); } } this.iterators = iterators; + this.index = startIndex; } @Override public boolean hasNext() { - boolean hasNext = false; - while (index < iterators.length && (hasNext = iterators[index].hasNext()) == false) { - index++; - } - - return hasNext; + return index < iterators.length; } @Override @@ -80,7 +81,11 @@ public T next() { if (hasNext() == false) { throw new NoSuchElementException(); } - return iterators[index].next(); + final T value = iterators[index].next(); + while (index < iterators.length && iterators[index].hasNext() == false) { + index++; + } + return value; } } @@ -110,4 +115,54 @@ public T next() { return array[index++]; } } + + public static Iterator flatMap(Iterator input, Function> fn) { + while (input.hasNext()) { + final var value = fn.apply(input.next()); + if (value.hasNext()) { + return new FlatMapIterator<>(input, fn, value); + } + } + + return Collections.emptyIterator(); + } + + private static final class FlatMapIterator implements Iterator { + + private final Iterator input; + private final Function> fn; + + @Nullable // if finished, otherwise currentOutput.hasNext() is true + private Iterator currentOutput; + + FlatMapIterator(Iterator input, Function> fn, Iterator firstOutput) { + this.input = input; + this.fn = fn; + this.currentOutput = firstOutput; + } + + @Override + public boolean hasNext() { + return currentOutput != null; + } + + @Override + public U next() { + if (hasNext() == false) { + throw new NoSuchElementException(); + } + // noinspection ConstantConditions this is for documentation purposes + assert currentOutput != null && currentOutput.hasNext(); + final U value = currentOutput.next(); + while (currentOutput != null && currentOutput.hasNext() == false) { + if (input.hasNext()) { + currentOutput = fn.apply(input.next()); + } else { + currentOutput = null; + } + } + return value; + } + } + } diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java index 282fadac236bb..19ebbe350a53f 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java @@ -62,8 +62,8 @@ public static Iterator field(String name, boolean value) { return Iterators.single(((builder, params) -> builder.field(name, value))); } - public static Iterator array(String name, Iterable iterable) { - return Iterators.concat(ChunkedToXContentHelper.startArray(name), iterable.iterator(), ChunkedToXContentHelper.endArray()); + public static Iterator array(String name, Iterator contents) { + return Iterators.concat(ChunkedToXContentHelper.startArray(name), contents, ChunkedToXContentHelper.endArray()); } public static Iterator wrapWithObject(String name, Iterator iterator) { diff --git a/server/src/main/java/org/elasticsearch/health/Diagnosis.java b/server/src/main/java/org/elasticsearch/health/Diagnosis.java index b3adddd1b6cc1..343fe86d87456 100644 --- a/server/src/main/java/org/elasticsearch/health/Diagnosis.java +++ b/server/src/main/java/org/elasticsearch/health/Diagnosis.java @@ -11,6 +11,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.xcontent.ChunkedToXContent; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ToXContent; @@ -19,9 +20,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.StreamSupport; import static org.elasticsearch.health.HealthService.HEALTH_API_ID_PREFIX; @@ -78,7 +76,7 @@ public Resource(Collection nodes) { @Override public Iterator toXContentChunked(ToXContent.Params outerParams) { - Iterator valuesIterator; + final Iterator valuesIterator; if (nodes != null) { valuesIterator = nodes.stream().map(node -> (ToXContent) (builder, params) -> { builder.startObject(); @@ -92,11 +90,7 @@ public Iterator toXContentChunked(ToXContent.Params outerP } else { valuesIterator = values.stream().map(value -> (ToXContent) (builder, params) -> builder.value(value)).iterator(); } - return Iterators.concat( - Iterators.single((ToXContent) (builder, params) -> builder.startArray(type.displayValue)), - valuesIterator, - Iterators.single((builder, params) -> builder.endArray()) - ); + return ChunkedToXContentHelper.array(type.displayValue, valuesIterator); } @Override @@ -148,16 +142,11 @@ public String getUniqueId() { @Override public Iterator toXContentChunked(ToXContent.Params outerParams) { - Iterator resourcesIterator = Collections.emptyIterator(); - if (affectedResources != null && affectedResources.size() > 0) { - resourcesIterator = affectedResources.stream() - .flatMap( - s -> StreamSupport.stream( - Spliterators.spliteratorUnknownSize(s.toXContentChunked(outerParams), Spliterator.ORDERED), - false - ) - ) - .iterator(); + final Iterator resourcesIterator; + if (affectedResources == null) { + resourcesIterator = Collections.emptyIterator(); + } else { + resourcesIterator = Iterators.flatMap(affectedResources.iterator(), s -> s.toXContentChunked(outerParams)); } return Iterators.concat(Iterators.single((ToXContent) (builder, params) -> { builder.startObject(); diff --git a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java index 8ce2818c371c9..e05a88e0e6139 100644 --- a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java +++ b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java @@ -15,9 +15,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.StreamSupport; public record HealthIndicatorResult( String name, @@ -29,16 +26,11 @@ public record HealthIndicatorResult( ) implements ChunkedToXContent { @Override public Iterator toXContentChunked(ToXContent.Params outerParams) { - Iterator diagnosisIterator = Collections.emptyIterator(); - if (diagnosisList != null && diagnosisList.isEmpty() == false) { - diagnosisIterator = diagnosisList.stream() - .flatMap( - s -> StreamSupport.stream( - Spliterators.spliteratorUnknownSize(s.toXContentChunked(outerParams), Spliterator.ORDERED), - false - ) - ) - .iterator(); + final Iterator diagnosisIterator; + if (diagnosisList.isEmpty()) { + diagnosisIterator = Collections.emptyIterator(); + } else { + diagnosisIterator = Iterators.flatMap(diagnosisList.iterator(), s -> s.toXContentChunked(outerParams)); } return Iterators.concat(Iterators.single((ToXContent) (builder, params) -> { builder.startObject(); diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java b/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java index eff4be93e78ad..3816ade205dff 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java @@ -98,7 +98,7 @@ public static IngestMetadata fromXContent(XContentParser parser) throws IOExcept @Override public Iterator toXContentChunked(ToXContent.Params ignored) { - return ChunkedToXContentHelper.array(PIPELINES_FIELD.getPreferredName(), pipelines.values()); + return ChunkedToXContentHelper.array(PIPELINES_FIELD.getPreferredName(), pipelines.values().iterator()); } @Override diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java index 515540a0d79d3..00f7fa031e125 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java @@ -558,7 +558,7 @@ public static NamedDiff readDiffFrom(StreamInput in) throws IOE public Iterator toXContentChunked(ToXContent.Params ignored) { return Iterators.concat( Iterators.single((builder, params) -> builder.field("last_allocation_id", lastAllocationId)), - ChunkedToXContentHelper.array("tasks", tasks.values()) + ChunkedToXContentHelper.array("tasks", tasks.values().iterator()) ); } diff --git a/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java b/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java index 3a750419c2090..8d3bcdd614396 100644 --- a/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java +++ b/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; public class IteratorsTests extends ESTestCase { public void testConcatentation() { @@ -153,6 +154,35 @@ public void testArrayIteratorOnNull() { expectThrows(NullPointerException.class, "Unable to iterate over a null array", () -> Iterators.forArray(null)); } + public void testFlatMap() { + final var array = randomIntegerArray(); + assertEmptyIterator(Iterators.flatMap(Iterators.forArray(array), i -> Iterators.concat())); + assertEmptyIterator(Iterators.flatMap(Iterators.concat(), i -> Iterators.single("foo"))); + + final var index = new AtomicInteger(); + Iterators.flatMap(Iterators.forArray(array), Iterators::single) + .forEachRemaining(i -> assertEquals(array[index.getAndIncrement()], i)); + assertEquals(array.length, index.get()); + + index.set(0); + Iterators.flatMap(Iterators.forArray(array), i -> Iterators.forArray(new Integer[] { i, i, i })) + .forEachRemaining(i -> assertEquals(array[(index.getAndIncrement() / 3)], i)); + assertEquals(array.length * 3, index.get()); + + final var input = new ArrayList<>(); + for (int i = 1; i <= 4; i++) { + IntStream.range(0, between(0, 2)).forEachOrdered(ignored -> input.add(0)); + input.add(i); + IntStream.range(0, between(0, 2)).forEachOrdered(ignored -> input.add(0)); + } + + index.set(0); + final var expectedArray = new Integer[] { 0, 0, 1, 0, 1, 2, 0, 1, 2, 3 }; + Iterators.flatMap(input.listIterator(), i -> IntStream.range(0, (int) i).iterator()) + .forEachRemaining(i -> assertEquals(expectedArray[(index.getAndIncrement())], i)); + assertEquals(expectedArray.length, index.get()); + } + private static Integer[] randomIntegerArray() { return Randomness.get().ints(randomIntBetween(0, 1000)).boxed().toArray(Integer[]::new); } From cf3f3e66e55d64e942839b6766a085824d957bca Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 7 Dec 2022 14:42:09 +0000 Subject: [PATCH 2/3] Reinstate lost object wrapper --- .../cluster/snapshots/status/SnapshotsStatusResponse.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java index 1ddc9e895e91e..abd1c0b9068e7 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java @@ -13,7 +13,6 @@ 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.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; @@ -88,6 +87,10 @@ public int hashCode() { @Override public Iterator toXContentChunked(ToXContent.Params params) { - return ChunkedToXContentHelper.array("snapshots", Iterators.flatMap(snapshots.iterator(), s -> s.toXContentChunked(params))); + return Iterators.concat( + Iterators.single((b, p) -> b.startObject().startArray("snapshots")), + Iterators.flatMap(snapshots.iterator(), s -> s.toXContentChunked(params)), + Iterators.single((b, p) -> b.endArray().endObject()) + ); } } From e33421902bf0271a08122d506a0fb895beb76f51 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 7 Dec 2022 15:08:40 +0000 Subject: [PATCH 3/3] Missing null check --- .../java/org/elasticsearch/health/HealthIndicatorResult.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java index e05a88e0e6139..51985cc7395ba 100644 --- a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java +++ b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java @@ -27,7 +27,7 @@ public record HealthIndicatorResult( @Override public Iterator toXContentChunked(ToXContent.Params outerParams) { final Iterator diagnosisIterator; - if (diagnosisList.isEmpty()) { + if (diagnosisList == null) { diagnosisIterator = Collections.emptyIterator(); } else { diagnosisIterator = Iterators.flatMap(diagnosisList.iterator(), s -> s.toXContentChunked(outerParams));