Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Nest;

namespace Foundatio.Parsers.ElasticQueries.Extensions;
Expand All @@ -7,13 +8,59 @@ public static class SearchDescriptorExtensions
{
public static SearchDescriptor<T> Aggregations<T>(this SearchDescriptor<T> descriptor, AggregationContainer aggregations) where T : class
{
descriptor.Aggregations(f =>
return descriptor.Aggregations(f => CopyAggregations(f, aggregations.Aggregations));
}

public static AggregationContainerDescriptor<T> CopyAggregations<T>(
AggregationContainerDescriptor<T> target,
IDictionary<string, IAggregationContainer> sourceAggregations
) where T : class
{
foreach (var kvp in sourceAggregations)
{
((IAggregationContainer)f).Aggregations = aggregations.Aggregations;
return f;
});
string name = kvp.Key;
var agg = kvp.Value;

return descriptor;
if (agg.Nested != null)
{
// Nested aggregation: copy nested path and inner aggregations recursively
target.Nested(name, n => n
.Path(agg.Nested.Path)
.Aggregations(a => CopyAggregations(a, agg.Nested.Aggregations)));
}
else if (agg.Terms != null)
{
target.Terms(name, t =>
{
// Copy field
if (agg.Terms.Field != null)
t.Field(agg.Terms.Field);

// Copy exclude
if (agg.Terms.Exclude != null)
{
if (agg.Terms.Exclude.Values != null && agg.Terms.Exclude.Values.Count() > 0)
{
t.Exclude([.. agg.Terms.Exclude.Values]);
}
}

// Copy Meta if present
if (agg.Meta != null)
{
t.Meta(d => {
foreach (var meta in agg.Terms.Meta)
d.Add(meta.Key, meta.Value);
return d;
});
}

return t;
});
}
}

return target;
}

public static SearchDescriptor<T> Sort<T>(this SearchDescriptor<T> descriptor, IEnumerable<ISort> sorts) where T : class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte
var container = await GetParentContainerAsync(node, context);
var termsAggregation = container as ITermsAggregation;

var parentBucket = container as BucketAggregationBase;
var childAggregations = new Dictionary<string, AggregationContainer>();

foreach (var child in node.Children.OfType<IFieldQueryNode>())
{
var aggregation = await child.GetAggregationAsync(() => child.GetDefaultAggregationAsync(context));
Expand All @@ -30,8 +33,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte
var termNode = child as TermNode;
if (termNode != null && termsAggregation != null)
{
// Look into this...
// TODO: Move these to the default aggs method using a visitor to walk down the tree to gather them but not going into any sub groups
// Accumulate @exclude values as a list
if (termNode.Field == "@exclude")
{
termsAggregation.Exclude = termsAggregation.Exclude.AddValue(termNode.UnescapedTerm);
Expand All @@ -46,11 +48,8 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte
}
else if (termNode.Field == "@min")
{
int? minCount = null;
if (!String.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMinCount))
minCount = parsedMinCount;

termsAggregation.MinimumDocumentCount = minCount;
if (!string.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMinCount))
termsAggregation.MinimumDocumentCount = parsedMinCount;
}
}

Expand Down Expand Up @@ -79,7 +78,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte
if (termNode.Field == "@missing")
{
DateTime? missingValue = null;
if (!String.IsNullOrEmpty(termNode.Term) && DateTime.TryParse(termNode.Term, out var parsedMissingDate))
if (!string.IsNullOrEmpty(termNode.Term) && DateTime.TryParse(termNode.Term, out var parsedMissingDate))
missingValue = parsedMissingDate;

dateHistogramAggregation.Missing = missingValue;
Expand All @@ -93,13 +92,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte
continue;
}

if (container is BucketAggregationBase bucketContainer)
{
if (bucketContainer.Aggregations == null)
bucketContainer.Aggregations = new AggregationDictionary();

bucketContainer.Aggregations[((IAggregation)aggregation).Name] = (AggregationContainer)aggregation;
}
childAggregations[((IAggregation)aggregation).Name] = (AggregationContainer)aggregation;

if (termsAggregation != null && (child.Prefix == "-" || child.Prefix == "+"))
{
Expand All @@ -114,6 +107,56 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte
}
}

if (parentBucket != null)
{
bool containsNestedField = node.Children
.OfType<IFieldQueryNode>()
.Any(c => !String.IsNullOrEmpty(c.Field) && c.Field.StartsWith("nested.", StringComparison.OrdinalIgnoreCase));

if (containsNestedField)
{
parentBucket.Aggregations ??= [];

bool nestedExists = parentBucket.Aggregations.Any(kvp => kvp.Key == "nested_nested");

if (!nestedExists)
{
var nestedAggregation = new NestedAggregation("nested_nested")
{
Path = "nested",
Aggregations = []
};

var nestedAggregationContainer = new AggregationContainer
{
Nested = nestedAggregation
};

parentBucket.Aggregations["nested_nested"] = nestedAggregationContainer;
}

var nestedAggregationContainerFromDict = parentBucket.Aggregations["nested_nested"];
var nestedAgg = nestedAggregationContainerFromDict.Nested;

nestedAgg.Aggregations ??= [];

foreach (var kvp in childAggregations)
{
nestedAgg.Aggregations[kvp.Key] = kvp.Value;
}
}
else
{
if (parentBucket.Aggregations == null)
parentBucket.Aggregations = [];

foreach (var kvp in childAggregations)
{
parentBucket.Aggregations[kvp.Key] = kvp.Value;
}
}
}

if (node.Parent == null)
node.SetAggregation(container);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,14 @@ await Client.IndexManyAsync([
var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings<MyNestedType>(Client).UseNested());

// Act
var result = await processor.BuildAggregationsAsync("terms:(nested.field1 @exclude:myexclude @include:myinclude @include:otherinclude @missing:mymissing @exclude:otherexclude @min:1)");

var result = await processor.BuildAggregationsAsync(
"terms:(nested.field1 @exclude:cherry @exclude:date) " +
"terms:(nested.field4 @exclude:3 @exclude:4)"
);

// Assert
var actualResponse = Client.Search<MyNestedType>(d => d.Index(index).Aggregations(result));
var actualResponse = Client.Search<MyNestedType>(d => d.Index(index).Aggregations(result));
string actualRequest = actualResponse.GetRequest();
_logger.LogInformation("Actual: {Request}", actualRequest);

Expand All @@ -461,13 +465,39 @@ await Client.IndexManyAsync([
.Aggregations(na => na
.Terms("terms_nested.field1", t => t
.Field("nested.field1.keyword")
.Exclude(["date"])
.Meta(m => m.Add("@field_type", "text")))
.Exclude(["cherry","date"])
.Meta(m => m.Add("@field_type", "keyword")))
.Terms("terms_nested.field4", t => t
.Field("nested.field4")
.Exclude(["4"])
.Exclude(["3","4"])
.Meta(m => m.Add("@field_type", "integer")))))));

// expected response buckets
var expectedNestedAgg = expectedResponse.Aggregations.Nested("nested_nested");
var expectedField1Terms = expectedNestedAgg.Terms("terms_nested.field1");
var expectedField4Terms = expectedNestedAgg.Terms("terms_nested.field4");

// actual response buckets
var actualNestedAgg = actualResponse.Aggregations.Nested("nested_nested");
var actualField1Terms = actualNestedAgg.Terms("terms_nested.field1");
var actualField4Terms = actualNestedAgg.Terms("terms_nested.field4");

// Add assertions for bucket counts and contents
Assert.Equal(expectedField1Terms.Buckets.Count, actualField1Terms.Buckets.Count);
Assert.Equal(expectedField4Terms.Buckets.Count, actualField4Terms.Buckets.Count);

// Verify field1 buckets (apple and banana should be included, cherry and date excluded)
Assert.Contains(actualField1Terms.Buckets, b => b.Key == "apple" && b.DocCount == 1);
Assert.Contains(actualField1Terms.Buckets, b => b.Key == "banana" && b.DocCount == 1);
Assert.DoesNotContain(actualField1Terms.Buckets, b => b.Key == "cherry");
Assert.DoesNotContain(actualField1Terms.Buckets, b => b.Key == "date");

// Verify field4 buckets (1 and 2 should be included, 3 and 4 excluded)
Assert.Contains(actualField4Terms.Buckets, b => b.Key == "1" && b.DocCount == 1);
Assert.Contains(actualField4Terms.Buckets, b => b.Key == "2" && b.DocCount == 1);
Assert.DoesNotContain(actualField4Terms.Buckets, b => b.Key == "3");
Assert.DoesNotContain(actualField4Terms.Buckets, b => b.Key == "4");

string expectedRequest = expectedResponse.GetRequest();
_logger.LogInformation("Expected: {Request}", expectedRequest);

Expand Down
Loading