diff --git a/src/Nest/Aggregations/Bucket/Composite/CompositeAggregationSource.cs b/src/Nest/Aggregations/Bucket/Composite/CompositeAggregationSource.cs
index 23754272e33..cb736b1f704 100644
--- a/src/Nest/Aggregations/Bucket/Composite/CompositeAggregationSource.cs
+++ b/src/Nest/Aggregations/Bucket/Composite/CompositeAggregationSource.cs
@@ -37,6 +37,13 @@ public interface ICompositeAggregationSource
///
[JsonProperty("order")]
SortOrder? Order { get; set; }
+
+ ///
+ /// By default documents without a value for a given source are ignored. It is possible to include
+ /// them in the response as null by setting this to true
+ ///
+ [JsonProperty("missing_bucket")]
+ bool? MissingBucket { get; set; }
}
///
@@ -59,6 +66,9 @@ protected CompositeAggregationSourceBase(string name) =>
///
public SortOrder? Order { get; set; }
+
+ ///
+ public bool? MissingBucket { get; set; }
}
///
@@ -93,6 +103,7 @@ public abstract class CompositeAggregationSourceDescriptorBase _sourceType;
Field ICompositeAggregationSource.Field { get; set; }
SortOrder? ICompositeAggregationSource.Order { get; set; }
+ bool? ICompositeAggregationSource.MissingBucket { get; set; }
protected CompositeAggregationSourceDescriptorBase(string name, string sourceType)
{
@@ -108,6 +119,9 @@ protected CompositeAggregationSourceDescriptorBase(string name, string sourceTyp
///
public TDescriptor Order(SortOrder? order) => Assign(a => a.Order = order);
+
+ ///
+ public TDescriptor MissingBucket(bool? includeMissing = true) => Assign(a => a.MissingBucket = includeMissing);
}
internal class CompositeAggregationSourceConverter : ReserializeJsonConverter
diff --git a/src/Tests/Tests.Domain/Project.cs b/src/Tests/Tests.Domain/Project.cs
index 233a9248712..62e20b06cc9 100644
--- a/src/Tests/Tests.Domain/Project.cs
+++ b/src/Tests/Tests.Domain/Project.cs
@@ -57,7 +57,7 @@ public class Project
.RuleFor(p => p.NumberOfCommits, f => Gimme.Random.Number(1, 1000))
.RuleFor(p => p.NumberOfContributors, f => Gimme.Random.Number(1, 200))
.RuleFor(p => p.Ranges, f => Ranges.Generator.Generate())
- .RuleFor(p => p.Branches, f => Gimme.Random.ListItems(new List { "master", "dev", "release", "qa", "test" }, 2))
+ .RuleFor(p => p.Branches, f => Gimme.Random.ListItems(new List { "master", "dev", "release", "qa", "test" }))
.RuleFor(p => p.SourceOnly, f =>
TestConfiguration.Instance.Random.SourceSerializer ? new SourceOnlyObject() : null
)
diff --git a/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs b/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs
index 0f22d0ebcf7..de33f936b45 100644
--- a/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs
+++ b/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs
@@ -155,7 +155,7 @@ public CompositeAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) :
};
/**==== Handling Responses
- * Each Composite aggregation bucket key is an `CompositeKey`, a specialized
+ * Each Composite aggregation bucket key is a `CompositeKey` type, a specialized
* `IReadOnlyDictionary` type with methods to convert values to supported types
*/
protected override void ExpectResponse(ISearchResponse response)
@@ -196,6 +196,130 @@ protected override void ExpectResponse(ISearchResponse response)
}
}
+ /**[float]
+ * == Missing buckets
+ * By default documents without a value for a given source are ignored.
+ * It is possible to include them in the response by setting missing_bucket to `true` (defaults to `false`):
+ *
+ * NOTE: Only available in Elasticsearch 6.4.0+
+ */
+ [SkipVersion("<6.4.0", "Missing buckets added to Composite Aggregation Elasticsearch 6.4.0+")]
+ public class CompositeAggregationMissingBucketUsageTests : ProjectsOnlyAggregationUsageTestBase
+ {
+ public CompositeAggregationMissingBucketUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
+
+ protected override object AggregationJson => new
+ {
+ my_buckets = new
+ {
+ composite = new
+ {
+ sources = new object[]
+ {
+ new
+ {
+ branches = new
+ {
+ terms = new
+ {
+ field = "branches.keyword",
+ order = "asc",
+ missing_bucket = true
+ }
+ }
+ },
+ }
+ },
+ aggs = new
+ {
+ project_tags = new
+ {
+ nested = new { path = "tags" },
+ aggs = new
+ {
+ tags = new { terms = new {field = "tags.name"} }
+ }
+ }
+ }
+ }
+ };
+
+ protected override Func, IAggregationContainer> FluentAggs => a => a
+ .Composite("my_buckets", date => date
+ .Sources(s => s
+ .Terms("branches", t => t
+ .Field(f => f.Branches.Suffix("keyword"))
+ .MissingBucket()
+ .Order(SortOrder.Ascending)
+ )
+ )
+ .Aggregations(childAggs => childAggs
+ .Nested("project_tags", n => n
+ .Path(p => p.Tags)
+ .Aggregations(nestedAggs => nestedAggs
+ .Terms("tags", avg => avg.Field(p => p.Tags.First().Name))
+ )
+ )
+ )
+ );
+
+ protected override AggregationDictionary InitializerAggs =>
+ new CompositeAggregation("my_buckets")
+ {
+ Sources = new List
+ {
+ new TermsCompositeAggregationSource("branches")
+ {
+ Field = Infer.Field(f => f.Branches.Suffix("keyword")),
+ MissingBucket = true,
+ Order = SortOrder.Ascending
+ }
+ },
+ Aggregations = new NestedAggregation("project_tags")
+ {
+ Path = Field(p => p.Tags),
+ Aggregations = new TermsAggregation("tags")
+ {
+ Field = Field(p => p.Tags.First().Name)
+ }
+ }
+ };
+
+ /**==== Handling Responses
+ * Each Composite aggregation bucket key is an `CompositeKey`, a specialized
+ * `IReadOnlyDictionary` type with methods to convert values to supported types
+ */
+ protected override void ExpectResponse(ISearchResponse response)
+ {
+ response.ShouldBeValid();
+
+ var composite = response.Aggregations.Composite("my_buckets");
+ composite.Should().NotBeNull();
+ composite.Buckets.Should().NotBeNullOrEmpty();
+ composite.AfterKey.Should().NotBeNull();
+
+ if (TestConfiguration.Instance.InRange(">=6.3.0"))
+ composite.AfterKey.Should().HaveCount(1).And.ContainKeys("branches");
+
+ var i = 0;
+ foreach (var item in composite.Buckets)
+ {
+ var key = item.Key;
+ key.Should().NotBeNull();
+
+ key.TryGetValue("branches", out string branches).Should().BeTrue("expected to find 'branches' in composite bucket");
+ if (i == 0) branches.Should().BeNull("First key should be null as we expect to have some projects with no branches");
+ else branches.Should().NotBeNullOrEmpty();
+
+ var nested = item.Nested("project_tags");
+ nested.Should().NotBeNull();
+
+ var nestedTerms = nested.Terms("tags");
+ nestedTerms.Buckets.Count.Should().BeGreaterThan(0);
+ i++;
+ }
+ }
+ }
//hide
[SkipVersion("<6.3.0", "Date histogram source only supports format starting from Elasticsearch 6.3.0+")]
@@ -286,10 +410,6 @@ public DateFormatCompositeAggregationUsageTests(ReadOnlyCluster i, EndpointUsage
}
};
- /**==== Handling Responses
- * Each Composite aggregation bucket key is an `CompositeKey`, a specialized
- * `IReadOnlyDictionary` type with methods to convert values to supported types
- */
protected override void ExpectResponse(ISearchResponse response)
{
response.ShouldBeValid();