Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Foundatio.Parsers.ElasticQueries.Visitors;
using Foundatio.Parsers.LuceneQueries.Extensions;
Expand Down Expand Up @@ -42,6 +44,7 @@ public static QueryBase GetDefaultQuery(this TermNode node, IQueryVisitorContext
{
string[] fields = !String.IsNullOrEmpty(field) ? [field] : defaultFields;

// Handle wildcard query string case
if (!node.IsQuotedTerm && node.UnescapedTerm.EndsWith("*"))
{
query = new QueryStringQuery
Expand All @@ -51,42 +54,108 @@ public static QueryBase GetDefaultQuery(this TermNode node, IQueryVisitorContext
AnalyzeWildcard = true,
Query = node.UnescapedTerm
};
return query;
}
else

// If a single field, use single Match or MatchPhrase query
if (fields != null && fields.Length == 1)
{
if (fields != null && fields.Length == 1)
if (node.IsQuotedTerm)
{
if (node.IsQuotedTerm)
{
query = new MatchPhraseQuery
{
Field = fields[0],
Query = node.UnescapedTerm
};
}
else
query = new MatchPhraseQuery
{
query = new MatchQuery
{
Field = fields[0],
Query = node.UnescapedTerm
};
}
Field = fields[0],
Query = node.UnescapedTerm
};
}
else
{
query = new MultiMatchQuery
query = new MatchQuery
{
Fields = fields,
Field = fields[0],
Query = node.UnescapedTerm
};
if (node.IsQuotedTerm)
((MultiMatchQuery)query).Type = TextQueryType.Phrase;
}
return query;
}

// Multiple fields: split into nested groups and non-nested
var shouldQueries = new List<QueryBase>();

// Group nested fields by prefix (before dot)
var nestedGroups = fields
.Where(f => !String.IsNullOrEmpty(f) && f.Contains('.'))
.GroupBy(f => f.Substring(0, f.IndexOf('.')));

// Non-nested fields (no dot)
string[] nonNestedFields = [.. fields.Where(f => String.IsNullOrEmpty(f) || !f.Contains('.'))];

// Add non-nested query if any
if (nonNestedFields.Length == 1)
{
if (node.IsQuotedTerm)
{
shouldQueries.Add(new MatchPhraseQuery
{
Field = nonNestedFields[0],
Query = node.UnescapedTerm
});
}
else
{
shouldQueries.Add(new MatchQuery
{
Field = nonNestedFields[0],
Query = node.UnescapedTerm
});
}
}
else if (nonNestedFields.Length > 1)
{
var mmq = new MultiMatchQuery
{
Fields = nonNestedFields,
Query = node.UnescapedTerm
};
if (node.IsQuotedTerm)
mmq.Type = TextQueryType.Phrase;

shouldQueries.Add(mmq);
}

// Add nested queries per nested group
foreach (var group in nestedGroups)
{
string nestedPath = group.Key;

var nestedShouldQueries = group.Select(f => (QueryBase)(node.IsQuotedTerm
? new MatchPhraseQuery { Field = f, Query = node.UnescapedTerm }
: new MatchQuery { Field = f, Query = node.UnescapedTerm })).ToList();

QueryBase nestedInnerQuery;
if (nestedShouldQueries.Count == 1)
nestedInnerQuery = nestedShouldQueries[0];
else
nestedInnerQuery = new BoolQuery { Should = nestedShouldQueries.Select(q => (QueryContainer)q), MinimumShouldMatch = 1 };

var nestedQuery = new NestedQuery
{
Path = nestedPath,
Query = nestedInnerQuery
};

shouldQueries.Add(nestedQuery);
}

query = new BoolQuery
{
Should = shouldQueries.Select(q => (QueryContainer)q),
MinimumShouldMatch = 1
};
}
else
{
// Non-analyzed field path: handle prefix and term queries
if (!node.IsQuotedTerm && node.UnescapedTerm.EndsWith("*"))
{
query = new PrefixQuery
Expand Down
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,108 @@ 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.OrderBy(x => x.Key))
{
((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)
{
var fieldName = agg.Terms.Field.Name;

// For text fields, use the keyword sub-field if it's not already specified
// This helps handle the common case where a text field needs to be aggregated
bool isTextFieldWithoutKeyword = agg.Meta != null &&
agg.Meta.TryGetValue("@field_type", out var fieldType) &&
fieldType?.ToString() == "text" &&
!fieldName.EndsWith(".keyword");

if (isTextFieldWithoutKeyword)
{
// Use the keyword sub-field for text field aggregations
t.Field($"{fieldName}.keyword");
}
else
{
t.Field(agg.Terms.Field);
}
}

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

// Copy include
if (agg.Terms.Include != null)
{
if (agg.Terms.Include.Values != null && agg.Terms.Include.Values.Any())
{
t.Include([.. agg.Terms.Include.Values.OrderBy(v => v)]);
}
}

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

return t;
});
}
else if (agg.Max != null)
{
target.Max(name, m =>
{
// Copy field
if (agg.Max.Field != null)
m.Field(agg.Max.Field);

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

return m;
});
}
}

return target;
}

public static SearchDescriptor<T> Sort<T>(this SearchDescriptor<T> descriptor, IEnumerable<ISort> sorts) where T : class
Expand Down
Loading