diff --git a/docs/aggregations.asciidoc b/docs/aggregations.asciidoc index ed4fe30d36d..09eca89e21f 100644 --- a/docs/aggregations.asciidoc +++ b/docs/aggregations.asciidoc @@ -138,6 +138,8 @@ In addition to the buckets themselves, the bucket aggregations also compute and * <> +* <> + * <> * <> @@ -193,6 +195,8 @@ include::aggregations/bucket/missing/missing-aggregation-usage.asciidoc[] include::aggregations/bucket/nested/nested-aggregation-usage.asciidoc[] +include::aggregations/bucket/parent/parent-aggregation-usage.asciidoc[] + include::aggregations/bucket/range/range-aggregation-usage.asciidoc[] include::aggregations/bucket/reverse-nested/reverse-nested-aggregation-usage.asciidoc[] diff --git a/docs/aggregations/bucket/parent/parent-aggregation-usage.asciidoc b/docs/aggregations/bucket/parent/parent-aggregation-usage.asciidoc new file mode 100644 index 00000000000..778412f1f09 --- /dev/null +++ b/docs/aggregations/bucket/parent/parent-aggregation-usage.asciidoc @@ -0,0 +1,98 @@ +:ref_current: https://www.elastic.co/guide/en/elasticsearch/reference/6.5 + +:github: https://github.com/elastic/elasticsearch-net + +:nuget: https://www.nuget.org/packages + +//// +IMPORTANT NOTE +============== +This file has been generated from https://github.com/elastic/elasticsearch-net/tree/6.x/src/Tests/Tests/Aggregations/Bucket/Parent/ParentAggregationUsageTests.cs. +If you wish to submit a PR for any spelling mistakes, typos or grammatical errors for this file, +please modify the original csharp file found at the link and submit the PR with that change. Thanks! +//// + +[[parent-aggregation-usage]] +=== Parent Aggregation Usage + +A special single bucket aggregation that selects parent documents that have the specified type, as defined in a `join` field. + +Be sure to read the Elasticsearch documentation on {ref_current}/search-aggregations-bucket-parent-aggregation.html[Parent Aggregation]. + +==== Fluent DSL example + +[source,csharp] +---- +a => a +.Parent("name_of_parent_agg", parent => parent <1> + .Aggregations(parentAggs => parentAggs + .Average("average_commits", avg => avg.Field(p => p.NumberOfCommits)) + .Max("max_commits", avg => avg.Field(p => p.NumberOfCommits)) + .Min("min_commits", avg => avg.Field(p => p.NumberOfCommits)) + ) +) +---- +<1> sub-aggregations are on the type determined from the generic type parameter. In this example, the search is against `CommitActivity` type and `Project` is a parent of `CommitActivity` + +==== Object Initializer syntax example + +[source,csharp] +---- +new ParentAggregation("name_of_parent_agg", typeof(CommitActivity)) <1> +{ + Aggregations = + new AverageAggregation("average_commits", Field(f => f.NumberOfCommits)) <2> + && new MaxAggregation("max_commits", Field(f => f.NumberOfCommits)) + && new MinAggregation("min_commits", Field(f => f.NumberOfCommits)) +} +---- +<1> `join` field is determined from the _child_ type. In this example, it is `CommitActivity` + +<2> sub-aggregations are on the type determined from the `join` field. In this example, a `Project` is a parent of `CommitActivity` + +[source,javascript] +.Example json output +---- +{ + "size": 0, + "aggs": { + "name_of_parent_agg": { + "parent": { + "type": "commits" + }, + "aggs": { + "average_commits": { + "avg": { + "field": "numberOfCommits" + } + }, + "max_commits": { + "max": { + "field": "numberOfCommits" + } + }, + "min_commits": { + "min": { + "field": "numberOfCommits" + } + } + } + } + } +} +---- + +==== Handling Responses + +[source,csharp] +---- +response.ShouldBeValid(); + +var parentAgg = response.Aggregations.Parent("name_of_parent_agg"); +parentAgg.Should().NotBeNull(); +parentAgg.DocCount.Should().BeGreaterThan(0); +parentAgg.Min("average_commits").Should().NotBeNull(); +parentAgg.Min("min_commits").Should().NotBeNull(); +parentAgg.Max("max_commits").Should().NotBeNull(); +---- + diff --git a/src/Nest/Aggregations/AggregateDictionary.cs b/src/Nest/Aggregations/AggregateDictionary.cs index 26c0f3e7bff..fb277383377 100644 --- a/src/Nest/Aggregations/AggregateDictionary.cs +++ b/src/Nest/Aggregations/AggregateDictionary.cs @@ -113,6 +113,8 @@ public FiltersAggregate Filters(string key) public SingleBucketAggregate Children(string key) => TryGet(key); + public SingleBucketAggregate Parent(string key) => TryGet(key); + public SingleBucketAggregate Sampler(string key) => TryGet(key); public GeoCentroidAggregate GeoCentroid(string key) => TryGet(key); diff --git a/src/Nest/Aggregations/AggregationContainer.cs b/src/Nest/Aggregations/AggregationContainer.cs index b591f53270f..a65e9ec5773 100644 --- a/src/Nest/Aggregations/AggregationContainer.cs +++ b/src/Nest/Aggregations/AggregationContainer.cs @@ -177,6 +177,9 @@ public interface IAggregationContainer [JsonProperty("nested")] INestedAggregation Nested { get; set; } + [JsonProperty("parent")] + IParentAggregation Parent { get; set; } + [JsonProperty("percentile_ranks")] IPercentileRanksAggregation PercentileRanks { get; set; } @@ -304,6 +307,8 @@ public class AggregationContainer : IAggregationContainer public INestedAggregation Nested { get; set; } + public IParentAggregation Parent { get; set; } + public IPercentileRanksAggregation PercentileRanks { get; set; } public IPercentilesAggregation Percentiles { get; set; } @@ -440,6 +445,8 @@ public class AggregationContainerDescriptor : DescriptorBase _SetInnerAggregation(name, selector, (a, d) => a.Nested = d); + public AggregationContainerDescriptor Parent(string name, + Func, IParentAggregation> selector + ) where TParent : class => + _SetInnerAggregation(name, selector, (a, d) => a.Parent = d); + public AggregationContainerDescriptor ReverseNested(string name, Func, IReverseNestedAggregation> selector ) => diff --git a/src/Nest/Aggregations/Bucket/Parent/ParentAggregation.cs b/src/Nest/Aggregations/Bucket/Parent/ParentAggregation.cs new file mode 100644 index 00000000000..c94d2b69d5e --- /dev/null +++ b/src/Nest/Aggregations/Bucket/Parent/ParentAggregation.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace Nest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + [ContractJsonConverter(typeof(AggregationJsonConverter))] + public interface IParentAggregation : IBucketAggregation + { + /// + /// The type for the child in the parent/child relationship + /// + [JsonProperty("type")] + RelationName Type { get; set; } + } + + public class ParentAggregation : BucketAggregationBase, IParentAggregation + { + internal ParentAggregation() { } + + public ParentAggregation(string name, RelationName type) : base(name) => Type = type; + + public RelationName Type { get; set; } + + internal override void WrapInContainer(AggregationContainer c) => c.Parent = this; + } + + public class ParentAggregationDescriptor + : BucketAggregationDescriptorBase, IParentAggregation, TParent>, IParentAggregation + where T : class + where TParent : class + { + RelationName IParentAggregation.Type { get; set; } = typeof(T); + + public ParentAggregationDescriptor Type(RelationName type) => + Assign(a => a.Type = type); + + public ParentAggregationDescriptor Type() => + Assign(a => a.Type = typeof(TOtherParent)); + } +} diff --git a/src/Nest/Aggregations/Visitor/AggregationVisitor.cs b/src/Nest/Aggregations/Visitor/AggregationVisitor.cs index ac046d2c1e0..609f77606f4 100644 --- a/src/Nest/Aggregations/Visitor/AggregationVisitor.cs +++ b/src/Nest/Aggregations/Visitor/AggregationVisitor.cs @@ -66,6 +66,8 @@ public interface IAggregationVisitor void Visit(INestedAggregation aggregation); + void Visit(IParentAggregation aggregation); + void Visit(IReverseNestedAggregation aggregation); void Visit(IRangeAggregation aggregation); @@ -149,6 +151,7 @@ public class AggregationVisitor : IAggregationVisitor public virtual void Visit(ITermsAggregation aggregation) { } + // TODO: make virtual public void Visit(ISignificantTextAggregation aggregation) { } public virtual void Visit(IPercentileRanksAggregation aggregation) { } @@ -195,6 +198,8 @@ public class AggregationVisitor : IAggregationVisitor public virtual void Visit(INestedAggregation aggregation) { } + public virtual void Visit(IParentAggregation aggregation) { } + public virtual void Visit(ICardinalityAggregation aggregation) { } public virtual void Visit(IGlobalAggregation aggregation) { } diff --git a/src/Nest/Aggregations/Visitor/AggregationWalker.cs b/src/Nest/Aggregations/Visitor/AggregationWalker.cs index ee506161b34..2e1f75933b6 100644 --- a/src/Nest/Aggregations/Visitor/AggregationWalker.cs +++ b/src/Nest/Aggregations/Visitor/AggregationWalker.cs @@ -111,6 +111,11 @@ public void Walk(IAggregationContainer aggregation, IAggregationVisitor visitor) v.Visit(d); Accept(v, d.Aggregations); }); + AcceptAggregation(aggregation.Parent, visitor, (v, d) => + { + v.Visit(d); + Accept(v, d.Aggregations); + }); AcceptAggregation(aggregation.PercentileRanks, visitor, (v, d) => v.Visit(d)); AcceptAggregation(aggregation.Percentiles, visitor, (v, d) => v.Visit(d)); AcceptAggregation(aggregation.Range, visitor, (v, d) => diff --git a/src/Tests/Tests/Aggregations/Bucket/Parent/ParentAggregationUsageTests.cs b/src/Tests/Tests/Aggregations/Bucket/Parent/ParentAggregationUsageTests.cs new file mode 100644 index 00000000000..43b950264ca --- /dev/null +++ b/src/Tests/Tests/Aggregations/Bucket/Parent/ParentAggregationUsageTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading.Tasks; +using Elastic.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; +using Tests.Core.Client; +using Tests.Core.Extensions; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Core.ManagedElasticsearch.NodeSeeders; +using Tests.Domain; +using Tests.Framework; +using Tests.Framework.Integration; +using static Nest.Infer; + +namespace Tests.Aggregations.Bucket.Parent +{ + /** + * A special single bucket aggregation that selects parent documents that have the specified type, as defined in a `join` field. + * + * Be sure to read the Elasticsearch documentation on {ref_current}/search-aggregations-bucket-parent-aggregation.html[Parent Aggregation]. + */ + public class ParentAggregationUsageTests : ApiIntegrationTestBase, ISearchRequest, SearchDescriptor, SearchRequest> + { + public ParentAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override bool ExpectIsValid => true; + + protected sealed override object ExpectJson => new + { + size = 0, + aggs = new + { + name_of_parent_agg = new + { + parent = new { type = "commits" }, + aggs = new + { + average_commits = new + { + avg = new { field = "numberOfCommits" } + }, + max_commits = new + { + max = new { field = "numberOfCommits" } + }, + min_commits = new + { + min = new { field = "numberOfCommits" } + } + } + } + } + }; + + protected override int ExpectStatusCode => 200; + + // hide + protected override Func, ISearchRequest> Fluent => s => s + .Size(0) + .Index(DefaultSeeder.CommitsAliasFilter) + .Type() + .TypedKeys(TestClient.Configuration.Random.TypedKeys) + .Aggregations(FluentAggs); + + protected override HttpMethod HttpMethod => HttpMethod.POST; + + // hide + protected override SearchRequest Initializer => + new SearchRequest(DefaultSeeder.CommitsAliasFilter, Type()) + { + Size = 0, + TypedKeys = TestClient.Configuration.Random.TypedKeys, + Aggregations = InitializerAggs + }; + + protected override string UrlPath => $"/commits-only/doc/_search"; + + // https://youtrack.jetbrains.com/issue/RIDER-19912 + [U] protected override Task HitsTheCorrectUrl() => base.HitsTheCorrectUrl(); + + [U] protected override Task UsesCorrectHttpMethod() => base.UsesCorrectHttpMethod(); + + [U] protected override void SerializesInitializer() => base.SerializesInitializer(); + + [U] protected override void SerializesFluent() => base.SerializesFluent(); + + [I] public override Task ReturnsExpectedStatusCode() => base.ReturnsExpectedResponse(); + + [I] public override Task ReturnsExpectedIsValid() => base.ReturnsExpectedIsValid(); + + [I] public override Task ReturnsExpectedResponse() => base.ReturnsExpectedResponse(); + + protected override LazyResponses ClientUsage() => Calls( + (client, f) => client.Search(f), + (client, f) => client.SearchAsync(f), + (client, r) => client.Search(r), + (client, r) => client.SearchAsync(r) + ); + + protected Func, IAggregationContainer> FluentAggs => a => a + .Parent("name_of_parent_agg", parent => parent // <1> sub-aggregations are on the type determined from the generic type parameter. In this example, the search is against `CommitActivity` type and `Project` is a parent of `CommitActivity` + .Aggregations(parentAggs => parentAggs + .Average("average_commits", avg => avg.Field(p => p.NumberOfCommits)) + .Max("max_commits", avg => avg.Field(p => p.NumberOfCommits)) + .Min("min_commits", avg => avg.Field(p => p.NumberOfCommits)) + ) + ); + + protected AggregationDictionary InitializerAggs => + new ParentAggregation("name_of_parent_agg", typeof(CommitActivity)) // <1> `join` field is determined from the _child_ type. In this example, it is `CommitActivity` + { + Aggregations = + new AverageAggregation("average_commits", Field(f => f.NumberOfCommits)) // <2> sub-aggregations are on the type determined from the `join` field. In this example, a `Project` is a parent of `CommitActivity` + && new MaxAggregation("max_commits", Field(f => f.NumberOfCommits)) + && new MinAggregation("min_commits", Field(f => f.NumberOfCommits)) + }; + + protected override void ExpectResponse(ISearchResponse response) + { + response.ShouldBeValid(); + + var parentAgg = response.Aggregations.Parent("name_of_parent_agg"); + parentAgg.Should().NotBeNull(); + parentAgg.DocCount.Should().BeGreaterThan(0); + parentAgg.Min("average_commits").Should().NotBeNull(); + parentAgg.Min("min_commits").Should().NotBeNull(); + parentAgg.Max("max_commits").Should().NotBeNull(); + } + } +}