Skip to content

Commit 5e8cc7e

Browse files
committed
WIP - Needs review and cleanup before being merged into main branch
1 parent c428345 commit 5e8cc7e

21 files changed

+632
-194
lines changed

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
elasticsearch:
3-
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.5
3+
image: docker.elastic.co/elasticsearch/elasticsearch:9.2.1
44
environment:
55
discovery.type: single-node
66
xpack.security.enabled: 'false'
@@ -19,7 +19,7 @@ services:
1919
depends_on:
2020
elasticsearch:
2121
condition: service_healthy
22-
image: docker.elastic.co/kibana/kibana:8.19.5
22+
image: docker.elastic.co/kibana/kibana:9.2.1
2323
ports:
2424
- 5601:5601
2525
networks:

src/Foundatio.Parsers.ElasticQueries/AggregationMap.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using Elastic.Clients.Elasticsearch.Aggregations;
23

34
namespace Foundatio.Parsers.ElasticQueries;
45

@@ -8,4 +9,93 @@ public record AggregationMap(string Name, object Value)
89
public object Value { get; set; } = Value;
910
public List<AggregationMap> Aggregations { get; } = new();
1011
public Dictionary<string, object> Meta { get; } = new();
12+
13+
public IDictionary<string, Aggregation> ToDictionary()
14+
{
15+
var result = new Dictionary<string, Aggregation>();
16+
17+
// If this is a root container (Value is null), process all sub-aggregations
18+
if (Value == null)
19+
{
20+
foreach (var subAgg in Aggregations)
21+
{
22+
var subAggregation = CreateAggregation(subAgg);
23+
if (subAggregation != null)
24+
result[subAgg.Name] = subAggregation;
25+
}
26+
}
27+
else
28+
{
29+
AddToDictionary(this, result);
30+
}
31+
32+
return result;
33+
}
34+
35+
private static void AddToDictionary(AggregationMap map, Dictionary<string, Aggregation> result)
36+
{
37+
if (map?.Value == null)
38+
return;
39+
40+
var aggregation = CreateAggregation(map);
41+
if (aggregation != null)
42+
result[map.Name] = aggregation;
43+
}
44+
45+
private static Aggregation CreateAggregation(AggregationMap map)
46+
{
47+
if (map?.Value == null)
48+
return null;
49+
50+
var aggregation = map.Value switch
51+
{
52+
TermsAggregation terms => new Aggregation { Terms = terms },
53+
DateHistogramAggregation dateHistogram => new Aggregation { DateHistogram = dateHistogram },
54+
HistogramAggregation histogram => new Aggregation { Histogram = histogram },
55+
MinAggregation min => new Aggregation { Min = min },
56+
MaxAggregation max => new Aggregation { Max = max },
57+
AverageAggregation avg => new Aggregation { Avg = avg },
58+
SumAggregation sum => new Aggregation { Sum = sum },
59+
StatsAggregation stats => new Aggregation { Stats = stats },
60+
ExtendedStatsAggregation extendedStats => new Aggregation { ExtendedStats = extendedStats },
61+
CardinalityAggregation cardinality => new Aggregation { Cardinality = cardinality },
62+
MissingAggregation missing => new Aggregation { Missing = missing },
63+
TopHitsAggregation topHits => new Aggregation { TopHits = topHits },
64+
PercentilesAggregation percentiles => new Aggregation { Percentiles = percentiles },
65+
GeohashGridAggregation geohashGrid => new Aggregation { GeohashGrid = geohashGrid },
66+
_ => null
67+
};
68+
69+
if (aggregation == null)
70+
return null;
71+
72+
// Add sub-aggregations
73+
if (map.Aggregations.Count > 0)
74+
{
75+
aggregation.Aggregations = new Dictionary<string, Aggregation>();
76+
foreach (var subAgg in map.Aggregations)
77+
{
78+
var subAggregation = CreateAggregation(subAgg);
79+
if (subAggregation != null)
80+
aggregation.Aggregations[subAgg.Name] = subAggregation;
81+
}
82+
}
83+
84+
// Add meta (exclude null values)
85+
if (map.Meta.Count > 0)
86+
{
87+
aggregation.Meta = new Dictionary<string, object>();
88+
foreach (var kvp in map.Meta)
89+
{
90+
if (kvp.Value != null)
91+
aggregation.Meta[kvp.Key] = kvp.Value;
92+
}
93+
94+
// Remove meta if empty after filtering nulls
95+
if (aggregation.Meta.Count == 0)
96+
aggregation.Meta = null;
97+
}
98+
99+
return aggregation;
100+
}
11101
}

src/Foundatio.Parsers.ElasticQueries/ElasticMappingResolver.cs

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Runtime.CompilerServices;
56
using Elastic.Clients.Elasticsearch;
67
using Elastic.Clients.Elasticsearch.IndexManagement;
78
using Elastic.Clients.Elasticsearch.Mapping;
@@ -18,6 +19,7 @@ public class ElasticMappingResolver
1819
private readonly TypeMapping _codeMapping;
1920
private readonly Inferrer _inferrer;
2021
private readonly ConcurrentDictionary<string, FieldMapping> _mappingCache = new();
22+
private readonly ConditionalWeakTable<IProperty, ConcurrentDictionary<string, object>> _propertyMetadata = new();
2123
private readonly ILogger _logger;
2224

2325
public static ElasticMappingResolver NullInstance = new(() => null);
@@ -85,15 +87,28 @@ public FieldMapping GetMapping(string field, bool followAlias = false)
8587
{
8688
string fieldPart = fieldParts[depth];
8789
IProperty fieldMapping = null;
90+
PropertyName foundPropertyName = null;
8891
if (currentProperties == null || !currentProperties.TryGetProperty(fieldPart, out fieldMapping))
8992
{
90-
// check to see if there is a name match
93+
// check to see if there is a name match by iterating through the dictionary keys
9194
if (currentProperties != null)
92-
fieldMapping = ((IDictionary<PropertyName, IProperty>)currentProperties).Values.FirstOrDefault(m =>
95+
{
96+
foreach (var kvp in (IDictionary<PropertyName, IProperty>)currentProperties)
9397
{
94-
string propertyName = _inferrer.PropertyName(m?.TryGetName());
95-
return propertyName != null && propertyName.Equals(fieldPart, StringComparison.OrdinalIgnoreCase);
96-
});
98+
string propertyName = null;
99+
if (_inferrer != null && kvp.Key?.Name != null)
100+
propertyName = _inferrer.PropertyName(kvp.Key);
101+
else if (kvp.Key?.Name != null)
102+
propertyName = kvp.Key.Name;
103+
104+
if (propertyName != null && propertyName.Equals(fieldPart, StringComparison.OrdinalIgnoreCase))
105+
{
106+
fieldMapping = kvp.Value;
107+
foundPropertyName = kvp.Key;
108+
break;
109+
}
110+
}
111+
}
97112

98113
// no mapping found, call GetServerMapping again in case it hasn't been called recently and there are possibly new mappings
99114
if (fieldMapping == null && GetServerMapping())
@@ -122,17 +137,32 @@ public FieldMapping GetMapping(string field, bool followAlias = false)
122137
break;
123138
}
124139
}
140+
else
141+
{
142+
// TryGetProperty succeeded, store the PropertyName used
143+
foreach (var kvp in (IDictionary<PropertyName, IProperty>)currentProperties)
144+
{
145+
if (kvp.Value == fieldMapping)
146+
{
147+
foundPropertyName = kvp.Key;
148+
break;
149+
}
150+
}
151+
}
125152

126-
// coded properties sometimes have null Name properties
127-
string name = fieldMapping.TryGetName();
128-
// TODO: ?
129-
// if (name == null && fieldMapping is IPropertyWithClrOrigin clrOrigin && clrOrigin.ClrOrigin != null)
130-
// name = new PropertyName(clrOrigin.ClrOrigin);
153+
// Determine the property name - use foundPropertyName if available, otherwise fall back to fieldPart
154+
string resolvedName;
155+
if (foundPropertyName != null && _inferrer != null && foundPropertyName.Name != null)
156+
resolvedName = _inferrer.PropertyName(foundPropertyName);
157+
else if (foundPropertyName != null && foundPropertyName.Name != null)
158+
resolvedName = foundPropertyName.Name;
159+
else
160+
resolvedName = fieldPart;
131161

132162
if (depth == 0)
133-
resolvedFieldName += _inferrer.PropertyName(name);
163+
resolvedFieldName += resolvedName;
134164
else
135-
resolvedFieldName += "." + _inferrer.PropertyName(name);
165+
resolvedFieldName += "." + resolvedName;
136166

137167
if (depth == fieldParts.Length - 1)
138168
{
@@ -150,6 +180,10 @@ public FieldMapping GetMapping(string field, bool followAlias = false)
150180
{
151181
currentProperties = objectProperty.Properties;
152182
}
183+
else if (fieldMapping is NestedProperty nestedProperty)
184+
{
185+
currentProperties = nestedProperty.Properties;
186+
}
153187
else
154188
{
155189
if (fieldMapping is TextProperty textProperty)
@@ -415,12 +449,13 @@ private Properties MergeProperties(Properties codeProperties, Properties serverP
415449
if (kvp.Value is not FieldAliasProperty aliasProperty)
416450
continue;
417451

418-
mergedCodeProperties[kvp.Key] = new FieldAliasProperty
452+
var newAliasProperty = new FieldAliasProperty
419453
{
420-
//LocalMetadata = aliasProperty.LocalMetadata,
421454
Path = _inferrer?.Field(aliasProperty.Path) ?? aliasProperty.Path,
422455
// Name = aliasProperty.Name
423456
};
457+
CopyPropertyMetadata(aliasProperty, newAliasProperty);
458+
mergedCodeProperties[kvp.Key] = newAliasProperty;
424459
}
425460
}
426461
}
@@ -433,11 +468,12 @@ private Properties MergeProperties(Properties codeProperties, Properties serverP
433468
foreach (var serverProperty in serverProperties)
434469
{
435470
var merged = serverProperty.Value;
436-
// if (mergedCodeProperties.TryGetProperty(serverProperty.Key, out var codeProperty))
437-
// merged.LocalMetadata = codeProperty.LocalMetadata;
438471

439472
if (mergedCodeProperties.TryGetProperty(serverProperty.Key, out var codeProperty))
440473
{
474+
// Copy local metadata from code property to merged property
475+
CopyPropertyMetadata(codeProperty, merged);
476+
441477
switch (merged)
442478
{
443479
case ObjectProperty objectProperty:
@@ -491,7 +527,7 @@ private bool GetServerMapping()
491527
}
492528
}
493529

494-
public static ElasticMappingResolver Create<T>(Func<TypeMappingDescriptor<T>, TypeMapping> mappingBuilder, ElasticsearchClient client, ILogger logger = null) where T : class
530+
public static ElasticMappingResolver Create<T>(Action<TypeMappingDescriptor<T>> mappingBuilder, ElasticsearchClient client, ILogger logger = null) where T : class
495531
{
496532
logger ??= NullLogger.Instance;
497533

@@ -506,7 +542,7 @@ public static ElasticMappingResolver Create<T>(Func<TypeMappingDescriptor<T>, Ty
506542
}, logger);
507543
}
508544

509-
public static ElasticMappingResolver Create<T>(Func<TypeMappingDescriptor<T>, TypeMapping> mappingBuilder, ElasticsearchClient client, string index, ILogger logger = null) where T : class
545+
public static ElasticMappingResolver Create<T>(Action<TypeMappingDescriptor<T>> mappingBuilder, ElasticsearchClient client, string index, ILogger logger = null) where T : class
510546
{
511547
logger ??= NullLogger.Instance;
512548

@@ -521,10 +557,11 @@ public static ElasticMappingResolver Create<T>(Func<TypeMappingDescriptor<T>, Ty
521557
}, logger);
522558
}
523559

524-
public static ElasticMappingResolver Create<T>(Func<TypeMappingDescriptor<T>, TypeMapping> mappingBuilder, Inferrer inferrer, Func<TypeMapping> getMapping, ILogger logger = null) where T : class
560+
public static ElasticMappingResolver Create<T>(Action<TypeMappingDescriptor<T>> mappingBuilder, Inferrer inferrer, Func<TypeMapping> getMapping, ILogger logger = null) where T : class
525561
{
526-
var codeMapping = mappingBuilder(new TypeMappingDescriptor<T>());
527-
return new ElasticMappingResolver(codeMapping, inferrer, getMapping, logger: logger);
562+
var descriptor = new TypeMappingDescriptor<T>();
563+
mappingBuilder(descriptor);
564+
return new ElasticMappingResolver(descriptor, inferrer, getMapping, logger: logger);
528565
}
529566

530567
public static ElasticMappingResolver Create<T>(ElasticsearchClient client, ILogger logger = null)
@@ -562,6 +599,59 @@ public static ElasticMappingResolver Create(Func<TypeMapping> getMapping, Inferr
562599
{
563600
return new ElasticMappingResolver(getMapping, inferrer, logger: logger);
564601
}
602+
603+
#region Property Metadata
604+
605+
public IDictionary<string, object> GetPropertyMetadata(IProperty property)
606+
{
607+
if (property == null)
608+
return null;
609+
610+
return _propertyMetadata.GetOrCreateValue(property);
611+
}
612+
613+
public T GetPropertyMetadataValue<T>(IProperty property, string key, T defaultValue = default)
614+
{
615+
var metadata = GetPropertyMetadata(property);
616+
if (metadata == null || !metadata.TryGetValue(key, out var value))
617+
return defaultValue;
618+
619+
if (value is T typedValue)
620+
return typedValue;
621+
622+
try
623+
{
624+
return (T)Convert.ChangeType(value, typeof(T));
625+
}
626+
catch
627+
{
628+
return defaultValue;
629+
}
630+
}
631+
632+
public void SetPropertyMetadataValue(IProperty property, string key, object value)
633+
{
634+
if (property == null)
635+
return;
636+
637+
var metadata = _propertyMetadata.GetOrCreateValue(property);
638+
metadata[key] = value;
639+
}
640+
641+
public void CopyPropertyMetadata(IProperty source, IProperty target)
642+
{
643+
if (source == null || target == null)
644+
return;
645+
646+
if (!_propertyMetadata.TryGetValue(source, out var sourceMetadata))
647+
return;
648+
649+
var targetMetadata = _propertyMetadata.GetOrCreateValue(target);
650+
foreach (var kvp in sourceMetadata)
651+
targetMetadata[kvp.Key] = kvp.Value;
652+
}
653+
654+
#endregion
565655
}
566656

567657
public class FieldMapping

src/Foundatio.Parsers.ElasticQueries/ElasticQueryParser.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ private void SetupQueryVisitorContextDefaults(IQueryVisitorContext context)
100100
if (Configuration.ValidationOptions != null && !context.HasValidationOptions())
101101
context.SetValidationOptions(Configuration.ValidationOptions);
102102

103+
if (context is IElasticQueryVisitorContext elasticVisitorContext)
104+
{
105+
if (Configuration.GeoLocationResolver != null && elasticVisitorContext.GeoLocationResolver == null)
106+
elasticVisitorContext.GeoLocationResolver = Configuration.GeoLocationResolver;
107+
}
108+
103109
if (context.QueryType == QueryTypes.Query)
104110
{
105111
context.SetDefaultFields(Configuration.DefaultFields);
@@ -177,10 +183,22 @@ public async Task<Query> BuildQueryAsync(IQueryNode query, IElasticQueryVisitorC
177183
var q = await query.GetQueryAsync() ?? new MatchAllQuery();
178184
if (context?.UseScoring == false)
179185
{
180-
q = new BoolQuery
186+
// If the query is already a filter-only BoolQuery, don't wrap it again
187+
// CombineQueriesVisitor now builds filter queries directly for filter mode
188+
var boolQuery = q.Bool;
189+
bool isFilterOnlyBoolQuery = boolQuery != null
190+
&& boolQuery.Filter?.Count > 0
191+
&& (boolQuery.Must == null || boolQuery.Must.Count == 0)
192+
&& (boolQuery.Should == null || boolQuery.Should.Count == 0)
193+
&& (boolQuery.MustNot == null || boolQuery.MustNot.Count == 0);
194+
195+
if (!isFilterOnlyBoolQuery)
181196
{
182-
Filter = [q]
183-
};
197+
q = new BoolQuery
198+
{
199+
Filter = [q]
200+
};
201+
}
184202
}
185203

186204
return q;

0 commit comments

Comments
 (0)