Browse Source

Add `keyed` param to allow named filters agg return buckets as an array of objects (#89256)

Adds a new `keyed` param for `filters` aggs to come back with their `key` attached rather than as a json object. So that sorting them is meaningful.
QY 2 years ago
parent
commit
2306f78ca9
15 changed files with 289 additions and 46 deletions
  1. 6 0
      docs/changelog/89256.yaml
  2. 65 0
      docs/reference/aggregations/bucket/filters-aggregation.asciidoc
  3. 39 0
      modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/filters_bucket.yml
  4. 1 0
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregationBuilder.java
  5. 16 2
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java
  6. 31 0
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregationBuilder.java
  7. 33 8
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregator.java
  8. 4 0
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorFactory.java
  9. 37 16
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilters.java
  10. 12 3
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/ParsedFilters.java
  11. 1 1
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java
  12. 1 0
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregatorFromFilters.java
  13. 34 12
      server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java
  14. 3 0
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/FiltersTests.java
  15. 6 4
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFiltersTests.java

+ 6 - 0
docs/changelog/89256.yaml

@@ -0,0 +1,6 @@
+pr: 89256
+summary: Add `keyed` parameter to filters agg, allowing the user to get non-keyed buckets of named filters agg
+area: Aggregations
+type: enhancement
+issues:
+  - 83957

+ 65 - 0
docs/reference/aggregations/bucket/filters-aggregation.asciidoc

@@ -66,6 +66,7 @@ Response:
 // TESTRESPONSE[s/"_shards": \.\.\./"_shards": $body._shards/]
 // TESTRESPONSE[s/"hits": \.\.\./"hits": $body.hits/]
 
+[[anonymous-filters]]
 ==== Anonymous filters
 
 The filters field can also be provided as an array of filters, as in the
@@ -187,3 +188,67 @@ The response would be something like the following:
 // TESTRESPONSE[s/"took": 3/"took": $body.took/]
 // TESTRESPONSE[s/"_shards": \.\.\./"_shards": $body._shards/]
 // TESTRESPONSE[s/"hits": \.\.\./"hits": $body.hits/]
+
+[[non-keyed-response]]
+==== Non-keyed Response
+
+By default, the named filters aggregation returns the buckets as an object. But in some sorting cases, such as
+<<search-aggregations-pipeline-bucket-sort-aggregation,bucket sort>>, the JSON doesn't guarantee the order of elements
+in the object. You can use the `keyed` parameter to specify the buckets as an array of objects. The value of this
+parameter can be as follows:
+
+`true`::        (Default) Returns the buckets as an object
+`false`::       Returns the buckets as an array of objects
+
+NOTE: This parameter is ignored by <<anonymous-filters,Anonymous filters>>.
+
+Example:
+
+[source,console,id=filters-aggregation-sortable-example]
+----
+POST /sales/_search?size=0&filter_path=aggregations
+{
+  "aggs": {
+    "the_filter": {
+      "filters": {
+        "keyed": false,
+        "filters": {
+          "t-shirt": { "term": { "type": "t-shirt" } },
+          "hat": { "term": { "type": "hat" } }
+        }
+      },
+      "aggs": {
+        "avg_price": { "avg": { "field": "price" } },
+        "sort_by_avg_price": {
+          "bucket_sort": { "sort": { "avg_price": "asc" } }
+        }
+      }
+    }
+  }
+}
+----
+// TEST[setup:sales]
+
+Response:
+
+[source,console-result]
+----
+{
+  "aggregations": {
+    "the_filter": {
+      "buckets": [
+        {
+          "key": "t-shirt",
+          "doc_count": 3,
+          "avg_price": { "value": 128.33333333333334 }
+        },
+        {
+          "key": "hat",
+          "doc_count": 3,
+          "avg_price": { "value": 150.0 }
+        }
+      ]
+    }
+  }
+}
+----

+ 39 - 0
modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/filters_bucket.yml

@@ -137,6 +137,45 @@ setup:
   - match: { aggregations.the_filter.buckets.1.doc_count: 1 }
   - match: { aggregations.the_filter.buckets.1.the_avg.value: 151.0 }
 
+---
+"named filters with bucket sort test":
+  - skip:
+      version: " - 8.7.99"
+      reason: introduced in 8.8.0
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        body:
+          size: 0
+          aggs:
+            the_filter:
+              filters:
+                keyed: false
+                filters:
+                  first_filter:
+                    match:
+                      int_field: 101
+                  second_filter:
+                    match:
+                      int_field: 151
+              aggs:
+                the_max:
+                  max:
+                    field: int_field
+                sort_by_the_max:
+                  bucket_sort:
+                    sort:
+                      - the_max: desc
+
+  - match: { hits.total: 4 }
+  - length: { hits.hits: 0 }
+  - match: { aggregations.the_filter.buckets.0.key: second_filter }
+  - match: { aggregations.the_filter.buckets.0.doc_count: 1 }
+  - match: { aggregations.the_filter.buckets.0.the_max.value: 151.0 }
+  - match: { aggregations.the_filter.buckets.1.key: first_filter }
+  - match: { aggregations.the_filter.buckets.1.doc_count: 1 }
+  - match: { aggregations.the_filter.buckets.1.the_max.value: 101.0 }
+
 ---
 "Only aggs test":
 

+ 1 - 0
server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregationBuilder.java

@@ -180,6 +180,7 @@ public class FilterAggregationBuilder extends AbstractAggregationBuilder<FilterA
                 List.of(filter),
                 false,
                 null,
+                true,
                 context,
                 parent,
                 cardinality,

+ 16 - 2
server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java

@@ -46,6 +46,7 @@ public class FilterByFilterAggregator extends FiltersAggregator {
         private final String name;
         private final List<QueryToFilterAdapter> filters = new ArrayList<>();
         private final boolean keyed;
+        private final boolean keyedBucket;
         private final AggregationContext aggCtx;
         private final Aggregator parent;
         private final CardinalityUpperBound cardinality;
@@ -56,6 +57,7 @@ public class FilterByFilterAggregator extends FiltersAggregator {
         public AdapterBuilder(
             String name,
             boolean keyed,
+            boolean keyedBucket,
             String otherBucketKey,
             AggregationContext aggCtx,
             Aggregator parent,
@@ -64,6 +66,7 @@ public class FilterByFilterAggregator extends FiltersAggregator {
         ) throws IOException {
             this.name = name;
             this.keyed = keyed;
+            this.keyedBucket = keyedBucket;
             this.aggCtx = aggCtx;
             this.parent = parent;
             this.cardinality = cardinality;
@@ -139,7 +142,17 @@ public class FilterByFilterAggregator extends FiltersAggregator {
 
                 @Override
                 public FilterByFilterAggregator apply(AggregatorFactories subAggregators) throws IOException {
-                    agg = new FilterByFilterAggregator(name, subAggregators, filters, keyed, aggCtx, parent, cardinality, metadata);
+                    agg = new FilterByFilterAggregator(
+                        name,
+                        subAggregators,
+                        filters,
+                        keyed,
+                        keyedBucket,
+                        aggCtx,
+                        parent,
+                        cardinality,
+                        metadata
+                    );
                     return agg;
                 }
             }
@@ -201,12 +214,13 @@ public class FilterByFilterAggregator extends FiltersAggregator {
         AggregatorFactories factories,
         List<QueryToFilterAdapter> filters,
         boolean keyed,
+        boolean keyedBucket,
         AggregationContext aggCtx,
         Aggregator parent,
         CardinalityUpperBound cardinality,
         Map<String, Object> metadata
     ) throws IOException {
-        super(name, factories, filters, keyed, null, aggCtx, parent, cardinality, metadata);
+        super(name, factories, filters, keyed, keyedBucket, null, aggCtx, parent, cardinality, metadata);
     }
 
     /**

+ 31 - 0
server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregationBuilder.java

@@ -42,11 +42,13 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
     private static final ParseField FILTERS_FIELD = new ParseField("filters");
     private static final ParseField OTHER_BUCKET_FIELD = new ParseField("other_bucket");
     private static final ParseField OTHER_BUCKET_KEY_FIELD = new ParseField("other_bucket_key");
+    private static final ParseField KEYED_FIELD = new ParseField("keyed");
 
     private final List<KeyedFilter> filters;
     private final boolean keyed;
     private boolean otherBucket = false;
     private String otherBucketKey = "_other_";
+    private boolean keyedBucket = true;
 
     /**
      * @param name
@@ -92,6 +94,7 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
         this.keyed = clone.keyed;
         this.otherBucket = clone.otherBucket;
         this.otherBucketKey = clone.otherBucketKey;
+        this.keyedBucket = clone.keyedBucket;
     }
 
     @Override
@@ -123,6 +126,7 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
         }
         otherBucket = in.readBoolean();
         otherBucketKey = in.readString();
+        keyedBucket = in.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0) ? in.readBoolean() : true;
     }
 
     @Override
@@ -131,6 +135,9 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
         out.writeCollection(filters, keyed ? (o, v) -> v.writeTo(o) : (o, v) -> o.writeNamedWriteable(v.filter()));
         out.writeBoolean(otherBucket);
         out.writeString(otherBucketKey);
+        if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
+            out.writeBoolean(keyedBucket);
+        }
     }
 
     /**
@@ -182,6 +189,21 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
         return otherBucketKey;
     }
 
+    /**
+     * Set whether to return keyed bucket in array
+     */
+    public FiltersAggregationBuilder keyedBucket(boolean keyedBucket) {
+        this.keyedBucket = keyedBucket;
+        return this;
+    }
+
+    /**
+     * Get whether to return keyed bucket in array
+     */
+    public boolean keyedBucket() {
+        return keyedBucket;
+    }
+
     @Override
     public BucketCardinality bucketCardinality() {
         return BucketCardinality.MANY;
@@ -202,6 +224,7 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
             FiltersAggregationBuilder rewritten = new FiltersAggregationBuilder(getName(), rewrittenFilters, this.keyed);
             rewritten.otherBucket(otherBucket);
             rewritten.otherBucketKey(otherBucketKey);
+            rewritten.keyedBucket(keyedBucket);
             return rewritten;
         } else {
             return this;
@@ -217,6 +240,7 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
             keyed,
             otherBucket,
             otherBucketKey,
+            keyedBucket,
             context,
             parent,
             subFactoriesBuilder,
@@ -242,6 +266,7 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
         }
         builder.field(FiltersAggregator.OTHER_BUCKET_FIELD.getPreferredName(), otherBucket);
         builder.field(FiltersAggregator.OTHER_BUCKET_KEY_FIELD.getPreferredName(), otherBucketKey);
+        builder.field(FiltersAggregator.KEYED_FIELD.getPreferredName(), keyedBucket);
         builder.endObject();
         return builder;
     }
@@ -254,6 +279,7 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
         String currentFieldName = null;
         String otherBucketKey = null;
         Boolean otherBucket = null;
+        Boolean keyedBucket = null;
         boolean keyed = false;
         while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
             if (token == XContentParser.Token.FIELD_NAME) {
@@ -261,6 +287,8 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
             } else if (token == XContentParser.Token.VALUE_BOOLEAN) {
                 if (OTHER_BUCKET_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                     otherBucket = parser.booleanValue();
+                } else if (KEYED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                    keyedBucket = parser.booleanValue();
                 } else {
                     throw new ParsingException(
                         parser.getTokenLocation(),
@@ -334,6 +362,9 @@ public class FiltersAggregationBuilder extends AbstractAggregationBuilder<Filter
         if (otherBucketKey != null) {
             factory.otherBucketKey(otherBucketKey);
         }
+        if (keyed && keyedBucket != null) {
+            factory.keyedBucket(keyedBucket);
+        }
         return factory;
     }
 

+ 33 - 8
server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregator.java

@@ -53,6 +53,7 @@ public abstract class FiltersAggregator extends BucketsAggregator {
     public static final ParseField FILTERS_FIELD = new ParseField("filters");
     public static final ParseField OTHER_BUCKET_FIELD = new ParseField("other_bucket");
     public static final ParseField OTHER_BUCKET_KEY_FIELD = new ParseField("other_bucket_key");
+    public static final ParseField KEYED_FIELD = new ParseField("keyed");
 
     public static class KeyedFilter implements Writeable, ToXContentFragment {
         private final String key;
@@ -128,6 +129,7 @@ public abstract class FiltersAggregator extends BucketsAggregator {
         List<QueryToFilterAdapter> filters,
         boolean keyed,
         String otherBucketKey,
+        boolean keyedBucket,
         AggregationContext context,
         Aggregator parent,
         CardinalityUpperBound cardinality,
@@ -137,6 +139,7 @@ public abstract class FiltersAggregator extends BucketsAggregator {
             new FilterByFilterAggregator.AdapterBuilder<FilterByFilterAggregator>(
                 name,
                 keyed,
+                keyedBucket,
                 otherBucketKey,
                 context,
                 parent,
@@ -157,11 +160,23 @@ public abstract class FiltersAggregator extends BucketsAggregator {
         if (filterByFilter != null) {
             return filterByFilter;
         }
-        return new FiltersAggregator.Compatible(name, factories, filters, keyed, otherBucketKey, context, parent, cardinality, metadata);
+        return new FiltersAggregator.Compatible(
+            name,
+            factories,
+            filters,
+            keyed,
+            keyedBucket,
+            otherBucketKey,
+            context,
+            parent,
+            cardinality,
+            metadata
+        );
     }
 
     private final List<QueryToFilterAdapter> filters;
     private final boolean keyed;
+    private final boolean keyedBucket;
     protected final String otherBucketKey;
 
     FiltersAggregator(
@@ -169,6 +184,7 @@ public abstract class FiltersAggregator extends BucketsAggregator {
         AggregatorFactories factories,
         List<QueryToFilterAdapter> filters,
         boolean keyed,
+        boolean keyedBucket,
         String otherBucketKey,
         AggregationContext aggCtx,
         Aggregator parent,
@@ -178,6 +194,7 @@ public abstract class FiltersAggregator extends BucketsAggregator {
         super(name, factories, aggCtx, parent, cardinality.multiply(filters.size() + (otherBucketKey == null ? 0 : 1)), metadata);
         this.filters = List.copyOf(filters);
         this.keyed = keyed;
+        this.keyedBucket = keyedBucket;
         this.otherBucketKey = otherBucketKey;
     }
 
@@ -196,12 +213,13 @@ public abstract class FiltersAggregator extends BucketsAggregator {
                         filters.get(offsetInOwningOrd).key().toString(),
                         docCount,
                         subAggregationResults,
-                        keyed
+                        keyed,
+                        keyedBucket
                     );
                 }
-                return new InternalFilters.InternalBucket(otherBucketKey, docCount, subAggregationResults, keyed);
+                return new InternalFilters.InternalBucket(otherBucketKey, docCount, subAggregationResults, keyed, keyedBucket);
             },
-            buckets -> new InternalFilters(name, buckets, keyed, metadata())
+            buckets -> new InternalFilters(name, buckets, keyed, keyedBucket, metadata())
         );
     }
 
@@ -210,16 +228,22 @@ public abstract class FiltersAggregator extends BucketsAggregator {
         InternalAggregations subAggs = buildEmptySubAggregations();
         List<InternalFilters.InternalBucket> buckets = new ArrayList<>(filters.size() + (otherBucketKey == null ? 0 : 1));
         for (QueryToFilterAdapter filter : filters) {
-            InternalFilters.InternalBucket bucket = new InternalFilters.InternalBucket(filter.key().toString(), 0, subAggs, keyed);
+            InternalFilters.InternalBucket bucket = new InternalFilters.InternalBucket(
+                filter.key().toString(),
+                0,
+                subAggs,
+                keyed,
+                keyedBucket
+            );
             buckets.add(bucket);
         }
 
         if (otherBucketKey != null) {
-            InternalFilters.InternalBucket bucket = new InternalFilters.InternalBucket(otherBucketKey, 0, subAggs, keyed);
+            InternalFilters.InternalBucket bucket = new InternalFilters.InternalBucket(otherBucketKey, 0, subAggs, keyed, keyedBucket);
             buckets.add(bucket);
         }
 
-        return new InternalFilters(name, buckets, keyed, metadata());
+        return new InternalFilters(name, buckets, keyed, keyedBucket, metadata());
     }
 
     @Override
@@ -248,13 +272,14 @@ public abstract class FiltersAggregator extends BucketsAggregator {
             AggregatorFactories factories,
             List<QueryToFilterAdapter> filters,
             boolean keyed,
+            boolean keyedBucket,
             String otherBucketKey,
             AggregationContext context,
             Aggregator parent,
             CardinalityUpperBound cardinality,
             Map<String, Object> metadata
         ) throws IOException {
-            super(name, factories, filters, keyed, otherBucketKey, context, parent, cardinality, metadata);
+            super(name, factories, filters, keyed, keyedBucket, otherBucketKey, context, parent, cardinality, metadata);
             if (otherBucketKey == null) {
                 this.totalNumKeys = filters.size();
             } else {

+ 4 - 0
server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorFactory.java

@@ -26,6 +26,7 @@ public class FiltersAggregatorFactory extends AggregatorFactory {
     private final boolean keyed;
     private final boolean otherBucket;
     private final String otherBucketKey;
+    private final boolean keyedBucket;
 
     public FiltersAggregatorFactory(
         String name,
@@ -33,6 +34,7 @@ public class FiltersAggregatorFactory extends AggregatorFactory {
         boolean keyed,
         boolean otherBucket,
         String otherBucketKey,
+        boolean keyedBucket,
         AggregationContext context,
         AggregatorFactory parent,
         AggregatorFactories.Builder subFactories,
@@ -42,6 +44,7 @@ public class FiltersAggregatorFactory extends AggregatorFactory {
         this.keyed = keyed;
         this.otherBucket = otherBucket;
         this.otherBucketKey = otherBucketKey;
+        this.keyedBucket = keyedBucket;
         this.filters = new ArrayList<>(filters.size());
         for (KeyedFilter f : filters) {
             this.filters.add(QueryToFilterAdapter.build(context.searcher(), f.key(), context.buildQuery(f.filter())));
@@ -57,6 +60,7 @@ public class FiltersAggregatorFactory extends AggregatorFactory {
             filters,
             keyed,
             otherBucket ? otherBucketKey : null,
+            keyedBucket,
             context,
             parent,
             cardinality,

+ 37 - 16
server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilters.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.search.aggregations.bucket.filter;
 
+import org.elasticsearch.TransportVersion;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.util.Maps;
@@ -28,12 +29,14 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
     public static class InternalBucket extends InternalMultiBucketAggregation.InternalBucket implements Filters.Bucket {
 
         private final boolean keyed;
+        private final boolean keyedBucket;
         private final String key;
         private long docCount;
         InternalAggregations aggregations;
 
-        public InternalBucket(String key, long docCount, InternalAggregations aggregations, boolean keyed) {
+        public InternalBucket(String key, long docCount, InternalAggregations aggregations, boolean keyed, boolean keyedBucket) {
             this.key = key;
+            this.keyedBucket = keyedBucket;
             this.docCount = docCount;
             this.aggregations = aggregations;
             this.keyed = keyed;
@@ -42,8 +45,9 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
         /**
          * Read from a stream.
          */
-        public InternalBucket(StreamInput in, boolean keyed) throws IOException {
+        public InternalBucket(StreamInput in, boolean keyed, boolean keyedBucket) throws IOException {
             this.keyed = keyed;
+            this.keyedBucket = keyedBucket;
             key = in.readOptionalString();
             docCount = in.readVLong();
             aggregations = InternalAggregations.readFrom(in);
@@ -78,11 +82,14 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
 
         @Override
         public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-            if (keyed) {
+            if (keyed && keyedBucket) {
                 builder.startObject(key);
             } else {
                 builder.startObject();
             }
+            if (keyed && keyedBucket == false) {
+                builder.field(CommonFields.KEY.getPreferredName(), key);
+            }
             builder.field(CommonFields.DOC_COUNT.getPreferredName(), docCount);
             aggregations.toXContentInternal(builder, params);
             builder.endObject();
@@ -100,13 +107,14 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
             InternalBucket that = (InternalBucket) other;
             return Objects.equals(key, that.key)
                 && Objects.equals(keyed, that.keyed)
+                && Objects.equals(keyedBucket, that.keyedBucket)
                 && Objects.equals(docCount, that.docCount)
                 && Objects.equals(aggregations, that.aggregations);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(getClass(), key, keyed, docCount, aggregations);
+            return Objects.hash(getClass(), key, keyed, keyedBucket, docCount, aggregations);
         }
 
         InternalBucket finalizeSampling(SamplingContext samplingContext) {
@@ -114,20 +122,23 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
                 key,
                 samplingContext.scaleUp(docCount),
                 InternalAggregations.finalizeSampling(aggregations, samplingContext),
-                keyed
+                keyed,
+                keyedBucket
             );
         }
     }
 
     private final List<InternalBucket> buckets;
     private final boolean keyed;
+    private final boolean keyedBucket;
     // bucketMap gets lazily initialized from buckets in getBucketByKey()
     private transient Map<String, InternalBucket> bucketMap;
 
-    public InternalFilters(String name, List<InternalBucket> buckets, boolean keyed, Map<String, Object> metadata) {
+    public InternalFilters(String name, List<InternalBucket> buckets, boolean keyed, boolean keyedBucket, Map<String, Object> metadata) {
         super(name, metadata);
         this.buckets = buckets;
         this.keyed = keyed;
+        this.keyedBucket = keyedBucket;
     }
 
     /**
@@ -136,10 +147,11 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
     public InternalFilters(StreamInput in) throws IOException {
         super(in);
         keyed = in.readBoolean();
+        keyedBucket = in.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0) ? in.readBoolean() : true;
         int size = in.readVInt();
         List<InternalBucket> buckets = new ArrayList<>(size);
         for (int i = 0; i < size; i++) {
-            buckets.add(new InternalBucket(in, keyed));
+            buckets.add(new InternalBucket(in, keyed, keyedBucket));
         }
         this.buckets = buckets;
         this.bucketMap = null;
@@ -148,6 +160,9 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
     @Override
     protected void doWriteTo(StreamOutput out) throws IOException {
         out.writeBoolean(keyed);
+        if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
+            out.writeBoolean(keyedBucket);
+        }
         out.writeList(buckets);
     }
 
@@ -158,12 +173,12 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
 
     @Override
     public InternalFilters create(List<InternalBucket> buckets) {
-        return new InternalFilters(name, buckets, keyed, metadata);
+        return new InternalFilters(name, buckets, keyed, keyedBucket, metadata);
     }
 
     @Override
     public InternalBucket createBucket(InternalAggregations aggregations, InternalBucket prototype) {
-        return new InternalBucket(prototype.key, prototype.docCount, aggregations, prototype.keyed);
+        return new InternalBucket(prototype.key, prototype.docCount, aggregations, prototype.keyed, keyedBucket);
     }
 
     @Override
@@ -203,7 +218,7 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
         }
 
         reduceContext.consumeBucketsAndMaybeBreak(bucketsList.size());
-        InternalFilters reduced = new InternalFilters(name, new ArrayList<>(bucketsList.size()), keyed, getMetadata());
+        InternalFilters reduced = new InternalFilters(name, new ArrayList<>(bucketsList.size()), keyed, keyedBucket, getMetadata());
         for (List<InternalBucket> sameRangeList : bucketsList) {
             reduced.buckets.add(reduceBucket(sameRangeList, reduceContext));
         }
@@ -212,7 +227,13 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
 
     @Override
     public InternalAggregation finalizeSampling(SamplingContext samplingContext) {
-        return new InternalFilters(name, buckets.stream().map(b -> b.finalizeSampling(samplingContext)).toList(), keyed, getMetadata());
+        return new InternalFilters(
+            name,
+            buckets.stream().map(b -> b.finalizeSampling(samplingContext)).toList(),
+            keyed,
+            keyedBucket,
+            getMetadata()
+        );
     }
 
     @Override
@@ -222,7 +243,7 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
         List<InternalAggregations> aggregationsList = new ArrayList<>(buckets.size());
         for (InternalBucket bucket : buckets) {
             if (reduced == null) {
-                reduced = new InternalBucket(bucket.key, bucket.docCount, bucket.aggregations, bucket.keyed);
+                reduced = new InternalBucket(bucket.key, bucket.docCount, bucket.aggregations, bucket.keyed, keyedBucket);
             } else {
                 reduced.docCount += bucket.docCount;
             }
@@ -234,7 +255,7 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
 
     @Override
     public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
-        if (keyed) {
+        if (keyed && keyedBucket) {
             builder.startObject(CommonFields.BUCKETS.getPreferredName());
         } else {
             builder.startArray(CommonFields.BUCKETS.getPreferredName());
@@ -242,7 +263,7 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
         for (InternalBucket bucket : buckets) {
             bucket.toXContent(builder, params);
         }
-        if (keyed) {
+        if (keyed && keyedBucket) {
             builder.endObject();
         } else {
             builder.endArray();
@@ -252,7 +273,7 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), buckets, keyed);
+        return Objects.hash(super.hashCode(), buckets, keyed, keyedBucket);
     }
 
     @Override
@@ -262,7 +283,7 @@ public class InternalFilters extends InternalMultiBucketAggregation<InternalFilt
         if (super.equals(obj) == false) return false;
 
         InternalFilters that = (InternalFilters) obj;
-        return Objects.equals(buckets, that.buckets) && Objects.equals(keyed, that.keyed);
+        return Objects.equals(buckets, that.buckets) && Objects.equals(keyed, that.keyed) && Objects.equals(keyedBucket, that.keyedBucket);
     }
 
 }

+ 12 - 3
server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/ParsedFilters.java

@@ -69,9 +69,10 @@ public class ParsedFilters extends ParsedMultiBucketAggregation<ParsedFilters.Pa
         if (aggregation.keyed == false) {
             int i = 0;
             for (ParsedBucket bucket : aggregation.buckets) {
-                assert bucket.key == null;
-                bucket.key = String.valueOf(i);
-                i++;
+                if (bucket.key == null) {
+                    bucket.key = String.valueOf(i);
+                    i++;
+                }
             }
         }
         return aggregation;
@@ -81,6 +82,8 @@ public class ParsedFilters extends ParsedMultiBucketAggregation<ParsedFilters.Pa
 
         private String key;
 
+        private boolean keyedBucket = true;
+
         @Override
         public String getKey() {
             return key;
@@ -98,6 +101,9 @@ public class ParsedFilters extends ParsedMultiBucketAggregation<ParsedFilters.Pa
             } else {
                 builder.startObject();
             }
+            if (isKeyed() == false && keyedBucket == false) {
+                builder.field(CommonFields.KEY.getPreferredName(), key);
+            }
             builder.field(CommonFields.DOC_COUNT.getPreferredName(), getDocCount());
             getAggregations().toXContentInternal(builder, params);
             builder.endObject();
@@ -122,6 +128,9 @@ public class ParsedFilters extends ParsedMultiBucketAggregation<ParsedFilters.Pa
                 } else if (token.isValue()) {
                     if (CommonFields.DOC_COUNT.getPreferredName().equals(currentFieldName)) {
                         bucket.setDocCount(parser.longValue());
+                    } else if (CommonFields.KEY.getPreferredName().equals(currentFieldName)) {
+                        bucket.key = parser.text();
+                        bucket.keyedBucket = false;
                     }
                 } else if (token == XContentParser.Token.START_OBJECT) {
                     XContentParserUtils.parseTypedKeysObject(

+ 1 - 1
server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java

@@ -392,7 +392,7 @@ public abstract class RangeAggregator extends BucketsAggregator {
         }
         boolean wholeNumbersOnly = false == ((ValuesSource.Numeric) valuesSourceConfig.getValuesSource()).isFloatingPoint();
         FilterByFilterAggregator.AdapterBuilder<FromFilters<?>> filterByFilterBuilder = new FilterByFilterAggregator.AdapterBuilder<
-            FromFilters<?>>(name, false, null, context, parent, cardinality, metadata) {
+            FromFilters<?>>(name, false, false, null, context, parent, cardinality, metadata) {
             @Override
             protected FromFilters<?> adapt(CheckedFunction<AggregatorFactories, FilterByFilterAggregator, IOException> delegate)
                 throws IOException {

+ 1 - 0
server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregatorFromFilters.java

@@ -72,6 +72,7 @@ public class StringTermsAggregatorFromFilters extends AdaptingAggregator {
             new FilterByFilterAggregator.AdapterBuilder<StringTermsAggregatorFromFilters>(
                 name,
                 false,
+                false,
                 null,
                 context,
                 parent,

+ 34 - 12
server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java

@@ -86,8 +86,14 @@ public class InternalAggregationsTests extends ESTestCase {
 
     InternalAggregations toReduce(AtomicLong f1Reduced, AtomicLong f2Reduced, int k1, int k2, int k1k1, int k1k2, int k2k1, int k2k2) {
         class InternalFiltersForF2 extends InternalFilters {
-            InternalFiltersForF2(String name, List<InternalBucket> buckets, boolean keyed, Map<String, Object> metadata) {
-                super(name, buckets, keyed, metadata);
+            InternalFiltersForF2(
+                String name,
+                List<InternalBucket> buckets,
+                boolean keyed,
+                boolean keyedBucket,
+                Map<String, Object> metadata
+            ) {
+                super(name, buckets, keyed, keyedBucket, metadata);
             }
 
             public InternalAggregation reduce(List<InternalAggregation> aggregations, AggregationReduceContext reduceContext) {
@@ -98,8 +104,14 @@ public class InternalAggregationsTests extends ESTestCase {
             }
         }
         class InternalFiltersForF1 extends InternalFilters {
-            InternalFiltersForF1(String name, List<InternalBucket> buckets, boolean keyed, Map<String, Object> metadata) {
-                super(name, buckets, keyed, metadata);
+            InternalFiltersForF1(
+                String name,
+                List<InternalBucket> buckets,
+                boolean keyed,
+                boolean keyedBucket,
+                Map<String, Object> metadata
+            ) {
+                super(name, buckets, keyed, keyedBucket, metadata);
             }
 
             public InternalAggregation reduce(List<InternalAggregation> aggregations, AggregationReduceContext reduceContext) {
@@ -122,14 +134,16 @@ public class InternalAggregationsTests extends ESTestCase {
                                     new InternalFiltersForF2(
                                         "f2",
                                         List.of(
-                                            new InternalFilters.InternalBucket("f2k1", k1k1, InternalAggregations.EMPTY, true),
-                                            new InternalFilters.InternalBucket("f2k2", k1k2, InternalAggregations.EMPTY, true)
+                                            new InternalFilters.InternalBucket("f2k1", k1k1, InternalAggregations.EMPTY, true, true),
+                                            new InternalFilters.InternalBucket("f2k2", k1k2, InternalAggregations.EMPTY, true, true)
                                         ),
                                         true,
+                                        true,
                                         null
                                     )
                                 )
                             ),
+                            true,
                             true
                         ),
                         new InternalFilters.InternalBucket(
@@ -140,18 +154,21 @@ public class InternalAggregationsTests extends ESTestCase {
                                     new InternalFiltersForF2(
                                         "f2",
                                         List.of(
-                                            new InternalFilters.InternalBucket("f2k1", k2k1, InternalAggregations.EMPTY, true),
-                                            new InternalFilters.InternalBucket("f2k2", k2k2, InternalAggregations.EMPTY, true)
+                                            new InternalFilters.InternalBucket("f2k1", k2k1, InternalAggregations.EMPTY, true, true),
+                                            new InternalFilters.InternalBucket("f2k2", k2k2, InternalAggregations.EMPTY, true, true)
                                         ),
                                         true,
+                                        true,
                                         null
                                     )
                                 )
                             ),
+                            true,
                             true
                         )
                     ),
                     true,
+                    true,
                     null
                 )
             )
@@ -172,14 +189,16 @@ public class InternalAggregationsTests extends ESTestCase {
                                     new InternalFilters(
                                         "f2",
                                         List.of(
-                                            new InternalFilters.InternalBucket("f2k1", k1k1, InternalAggregations.EMPTY, true),
-                                            new InternalFilters.InternalBucket("f2k2", k1k2, InternalAggregations.EMPTY, true)
+                                            new InternalFilters.InternalBucket("f2k1", k1k1, InternalAggregations.EMPTY, true, true),
+                                            new InternalFilters.InternalBucket("f2k2", k1k2, InternalAggregations.EMPTY, true, true)
                                         ),
                                         true,
+                                        true,
                                         null
                                     )
                                 )
                             ),
+                            true,
                             true
                         ),
                         new InternalFilters.InternalBucket(
@@ -190,18 +209,21 @@ public class InternalAggregationsTests extends ESTestCase {
                                     new InternalFilters(
                                         "f2",
                                         List.of(
-                                            new InternalFilters.InternalBucket("f2k1", k2k1, InternalAggregations.EMPTY, true),
-                                            new InternalFilters.InternalBucket("f2k2", k2k2, InternalAggregations.EMPTY, true)
+                                            new InternalFilters.InternalBucket("f2k1", k2k1, InternalAggregations.EMPTY, true, true),
+                                            new InternalFilters.InternalBucket("f2k2", k2k2, InternalAggregations.EMPTY, true, true)
                                         ),
                                         true,
+                                        true,
                                         null
                                     )
                                 )
                             ),
+                            true,
                             true
                         )
                     ),
                     true,
+                    true,
                     null
                 )
             )

+ 3 - 0
server/src/test/java/org/elasticsearch/search/aggregations/bucket/FiltersTests.java

@@ -61,6 +61,9 @@ public class FiltersTests extends BaseAggregationTestCase<FiltersAggregationBuil
         if (randomBoolean()) {
             factory.otherBucketKey(randomAlphaOfLengthBetween(1, 20));
         }
+        if (randomBoolean()) {
+            factory.keyedBucket(randomBoolean());
+        }
         return factory;
     }
 

+ 6 - 4
server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFiltersTests.java

@@ -30,6 +30,7 @@ import static org.hamcrest.Matchers.sameInstance;
 public class InternalFiltersTests extends InternalMultiBucketAggregationTestCase<InternalFilters> {
 
     private boolean keyed;
+    private boolean keyedBucket;
     private List<String> keys;
 
     @Override
@@ -41,6 +42,7 @@ public class InternalFiltersTests extends InternalMultiBucketAggregationTestCase
     public void setUp() throws Exception {
         super.setUp();
         keyed = randomBoolean();
+        keyedBucket = randomBoolean();
         keys = new ArrayList<>();
         int numBuckets = randomNumberOfBuckets();
         for (int i = 0; i < numBuckets; i++) {
@@ -59,9 +61,9 @@ public class InternalFiltersTests extends InternalMultiBucketAggregationTestCase
         for (int i = 0; i < keys.size(); ++i) {
             String key = keys.get(i);
             int docCount = randomIntBetween(0, 1000);
-            buckets.add(new InternalFilters.InternalBucket(key, docCount, aggregations, keyed));
+            buckets.add(new InternalFilters.InternalBucket(key, docCount, aggregations, keyed, keyedBucket));
         }
-        return new InternalFilters(name, buckets, keyed, metadata);
+        return new InternalFilters(name, buckets, keyed, keyedBucket, metadata);
     }
 
     @Override
@@ -96,7 +98,7 @@ public class InternalFiltersTests extends InternalMultiBucketAggregationTestCase
             case 0 -> name += randomAlphaOfLength(5);
             case 1 -> {
                 buckets = new ArrayList<>(buckets);
-                buckets.add(new InternalBucket("test", randomIntBetween(0, 1000), InternalAggregations.EMPTY, keyed));
+                buckets.add(new InternalBucket("test", randomIntBetween(0, 1000), InternalAggregations.EMPTY, keyed, keyedBucket));
             }
             default -> {
                 if (metadata == null) {
@@ -107,7 +109,7 @@ public class InternalFiltersTests extends InternalMultiBucketAggregationTestCase
                 metadata.put(randomAlphaOfLength(15), randomInt());
             }
         }
-        return new InternalFilters(name, buckets, keyed, metadata);
+        return new InternalFilters(name, buckets, keyed, keyedBucket, metadata);
     }
 
     public void testReducePipelinesReturnsSameInstanceWithoutPipelines() {