Skip to content

Commit 5a26d29

Browse files
authored
Add range query support (#7311)
1 parent 4ff277e commit 5a26d29

20 files changed

+1751
-6
lines changed

Elasticsearch.sln

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
3737
Directory.Build.props = Directory.Build.props
3838
Directory.Build.targets = Directory.Build.targets
3939
dotnet-tools.json = dotnet-tools.json
40+
exclusion.dic = exclusion.dic
4041
global.json = global.json
4142
issue_template.md = issue_template.md
4243
LICENSE.txt = LICENSE.txt

exclusion.dic

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ json
33
async
44
inferrer
55
elasticsearch
6-
asciidocs
6+
asciidocs
7+
yyyy

src/Elastic.Clients.Elasticsearch/Core/Exceptions/ThrowHelper.cs

+7
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,39 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Runtime.CompilerServices;
78
using System.Text.Json;
89

910
namespace Elastic.Clients.Elasticsearch;
1011

1112
internal static class ThrowHelper
1213
{
14+
[DoesNotReturn]
1315
[MethodImpl(MethodImplOptions.NoInlining)]
1416
internal static void ThrowJsonException(string? message = null) => throw new JsonException(message);
1517

18+
[DoesNotReturn]
1619
[MethodImpl(MethodImplOptions.NoInlining)]
1720
internal static void ThrowUnknownTaggedUnionVariantJsonException(string variantTag, Type interfaceType) =>
1821
throw new JsonException($"Encountered an unsupported variant tag '{variantTag}' on '{SimplifiedFullName(interfaceType)}', which could not be deserialized.");
1922

23+
[DoesNotReturn]
2024
[MethodImpl(MethodImplOptions.NoInlining)]
2125
internal static void ThrowInvalidOperationException(string message) =>
2226
throw new InvalidOperationException(message);
2327

28+
[DoesNotReturn]
2429
[MethodImpl(MethodImplOptions.NoInlining)]
2530
#pragma warning disable IDE0057 // Use range operator
2631
private static string SimplifiedFullName(Type type) => type.FullName.Substring(30);
2732
#pragma warning restore IDE0057 // Use range operator
2833

34+
[DoesNotReturn]
2935
[MethodImpl(MethodImplOptions.NoInlining)]
3036
internal static void ThrowJsonExceptionForMissingSettings() => throw new JsonException("Unable to retrieve client settings for JsonSerializerOptions.");
3137

38+
[DoesNotReturn]
3239
[MethodImpl(MethodImplOptions.NoInlining)]
3340
internal static void ThrowInvalidOperationForBulkWhenNotIStreamSerializable() => throw new InvalidOperationException("Operation must implement IStreamSerializable.");
3441
}

src/Elastic.Clients.Elasticsearch/Serialization/TermsAggregateSerializationHelper.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ internal static class TermsAggregateSerializationHelper
1515
private static readonly byte[] s_key = Encoding.UTF8.GetBytes("key");
1616
private static readonly byte s_period = (byte)'.';
1717

18-
public static bool TryDeserialiseTermsAggregate(string aggregateName, ref Utf8JsonReader reader, JsonSerializerOptions options, out IAggregate? aggregate)
18+
public static bool TryDeserializeTermsAggregate(string aggregateName, ref Utf8JsonReader reader, JsonSerializerOptions options, out IAggregate? aggregate)
1919
{
2020
aggregate = null;
2121

22-
// We take a copy here so we can read forward to establish the term key type before we resume with final deserialisation.
22+
// We take a copy here so we can read forward to establish the term key type before we resume with final deserialization.
2323
var readerCopy = reader;
2424

2525
if (JsonHelper.TryReadUntilStringPropertyValue(ref readerCopy, s_buckets))

src/Elastic.Clients.Elasticsearch/Types/Aggregations/AggregateDictionaryConverter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static void ReadAggregate(ref Utf8JsonReader reader, JsonSerializerOption
5252
case "lterms":
5353
case "dterms":
5454
{
55-
if (TermsAggregateSerializationHelper.TryDeserialiseTermsAggregate(aggregateName, ref reader, options, out var agg))
55+
if (TermsAggregateSerializationHelper.TryDeserializeTermsAggregate(aggregateName, ref reader, options, out var agg))
5656
{
5757
dictionary.Add(nameParts[1], agg);
5858
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
using Elastic.Clients.Elasticsearch.Fluent;
10+
11+
namespace Elastic.Clients.Elasticsearch.QueryDsl;
12+
13+
public partial class Query
14+
{
15+
public bool TryGet<T>([NotNullWhen(true)]out T? query)
16+
{
17+
query = default(T);
18+
19+
if (Variant is T variant)
20+
{
21+
query = variant;
22+
return true;
23+
}
24+
25+
return false;
26+
}
27+
}
28+
29+
[JsonConverter(typeof(RangeQueryConverter))]
30+
public class RangeQuery : SearchQuery
31+
{
32+
internal RangeQuery() { }
33+
}
34+
35+
internal sealed class RangeQueryConverter : JsonConverter<RangeQuery>
36+
{
37+
public override RangeQuery? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
38+
{
39+
var readerCopy = reader;
40+
41+
if (readerCopy.TokenType != JsonTokenType.StartObject)
42+
ThrowHelper.ThrowJsonException($"Unexpected JSON detected. Expected {JsonTokenType.StartObject} but read {readerCopy.TokenType}.");
43+
44+
readerCopy.Read(); // Read past the opening token
45+
readerCopy.Read(); // Read past the field name
46+
47+
using var jsonDoc = JsonDocument.ParseValue(ref readerCopy);
48+
49+
if (jsonDoc is null)
50+
return null;
51+
52+
// When either of these properties are present, we know we have a date range query
53+
if (jsonDoc.RootElement.TryGetProperty("format", out _) || jsonDoc.RootElement.TryGetProperty("time_zone", out _))
54+
{
55+
return JsonSerializer.Deserialize<DateRangeQuery>(ref reader, options);
56+
}
57+
58+
JsonElement? rangeElement = null;
59+
60+
if (jsonDoc.RootElement.TryGetProperty("gte", out var gte))
61+
{
62+
rangeElement = gte;
63+
}
64+
else if (jsonDoc.RootElement.TryGetProperty("gt", out var gt))
65+
{
66+
rangeElement = gt;
67+
}
68+
else if(jsonDoc.RootElement.TryGetProperty("lte", out var lte))
69+
{
70+
rangeElement = lte;
71+
}
72+
else if(jsonDoc.RootElement.TryGetProperty("lt", out var lt))
73+
{
74+
rangeElement = lt;
75+
}
76+
77+
if (!rangeElement.HasValue)
78+
{
79+
ThrowHelper.ThrowJsonException("Unable to determine type of range query.");
80+
}
81+
82+
switch (rangeElement.Value.ValueKind)
83+
{
84+
case JsonValueKind.String:
85+
return JsonSerializer.Deserialize<DateRangeQuery>(ref reader, options);
86+
case JsonValueKind.Number:
87+
return JsonSerializer.Deserialize<NumberRangeQuery>(ref reader, options);
88+
}
89+
90+
ThrowHelper.ThrowJsonException("Unable to deserialize range query.");
91+
92+
// We never reach here. I wish the flow analysis could infer that this isn't needed with the help of the DoesNotReturn attributes.
93+
return null;
94+
}
95+
96+
public override void Write(Utf8JsonWriter writer, RangeQuery value, JsonSerializerOptions options) =>
97+
JsonSerializer.Serialize(writer, value, value.GetType(), options);
98+
}
99+
100+
101+
public sealed class RangeQueryDescriptor<TDocument> : SerializableDescriptor<RangeQueryDescriptor<TDocument>>
102+
{
103+
private NumberRangeQueryDescriptor<TDocument> _numberRangeQueryDescriptor;
104+
private DateRangeQueryDescriptor<TDocument> _dateRangeQueryDescriptor;
105+
106+
private Action<NumberRangeQueryDescriptor<TDocument>> _numberRangeQueryDescriptorAction;
107+
private Action<DateRangeQueryDescriptor<TDocument>> _dateRangeQueryDescriptorAction;
108+
109+
public RangeQueryDescriptor<TDocument> DateRange(Action<DateRangeQueryDescriptor<TDocument>> configure)
110+
{
111+
_dateRangeQueryDescriptor = null;
112+
_dateRangeQueryDescriptorAction = configure;
113+
_numberRangeQueryDescriptor = null;
114+
_numberRangeQueryDescriptorAction = null;
115+
116+
return Self;
117+
}
118+
119+
public RangeQueryDescriptor<TDocument> NumberRange(Action<NumberRangeQueryDescriptor<TDocument>> configure)
120+
{
121+
_dateRangeQueryDescriptor = null;
122+
_dateRangeQueryDescriptorAction = null;
123+
_numberRangeQueryDescriptor = null;
124+
_numberRangeQueryDescriptorAction = configure;
125+
126+
return Self;
127+
}
128+
129+
public RangeQueryDescriptor<TDocument> DateRange(DateRangeQueryDescriptor<TDocument> descriptor)
130+
{
131+
_dateRangeQueryDescriptor = descriptor;
132+
_dateRangeQueryDescriptorAction = null;
133+
_numberRangeQueryDescriptor = null;
134+
_numberRangeQueryDescriptorAction = null;
135+
136+
return Self;
137+
}
138+
139+
public RangeQueryDescriptor<TDocument> NumberRange(NumberRangeQueryDescriptor<TDocument> descriptor)
140+
{
141+
_dateRangeQueryDescriptor = null;
142+
_dateRangeQueryDescriptorAction = null;
143+
_numberRangeQueryDescriptor = descriptor;
144+
_numberRangeQueryDescriptorAction = null;
145+
146+
return Self;
147+
}
148+
149+
protected override void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, IElasticsearchClientSettings settings)
150+
{
151+
if (_dateRangeQueryDescriptor is not null)
152+
{
153+
JsonSerializer.Serialize(writer, _dateRangeQueryDescriptor, options);
154+
}
155+
else if (_dateRangeQueryDescriptorAction is not null)
156+
{
157+
JsonSerializer.Serialize(writer, new DateRangeQueryDescriptor<TDocument>(_dateRangeQueryDescriptorAction), options);
158+
}
159+
else if (_numberRangeQueryDescriptor is not null)
160+
{
161+
JsonSerializer.Serialize(writer, _numberRangeQueryDescriptor, options);
162+
}
163+
else if (_numberRangeQueryDescriptorAction is not null)
164+
{
165+
JsonSerializer.Serialize(writer, new NumberRangeQueryDescriptor<TDocument>(_numberRangeQueryDescriptorAction), options);
166+
}
167+
}
168+
}
169+
170+
public sealed class RangeQueryDescriptor : SerializableDescriptor<RangeQueryDescriptor>
171+
{
172+
private NumberRangeQueryDescriptor _numberRangeQueryDescriptor;
173+
private DateRangeQueryDescriptor _dateRangeQueryDescriptor;
174+
175+
private Action<NumberRangeQueryDescriptor> _numberRangeQueryDescriptorAction;
176+
private Action<DateRangeQueryDescriptor> _dateRangeQueryDescriptorAction;
177+
178+
public RangeQueryDescriptor DateRange(Action<DateRangeQueryDescriptor> configure)
179+
{
180+
_dateRangeQueryDescriptor = null;
181+
_dateRangeQueryDescriptorAction = configure;
182+
_numberRangeQueryDescriptor = null;
183+
_numberRangeQueryDescriptorAction = null;
184+
185+
return Self;
186+
}
187+
188+
public RangeQueryDescriptor NumberRange(Action<NumberRangeQueryDescriptor> configure)
189+
{
190+
_dateRangeQueryDescriptor = null;
191+
_dateRangeQueryDescriptorAction = null;
192+
_numberRangeQueryDescriptor = null;
193+
_numberRangeQueryDescriptorAction = configure;
194+
195+
return Self;
196+
}
197+
198+
public RangeQueryDescriptor DateRange(DateRangeQueryDescriptor descriptor)
199+
{
200+
_dateRangeQueryDescriptor = descriptor;
201+
_dateRangeQueryDescriptorAction = null;
202+
_numberRangeQueryDescriptor = null;
203+
_numberRangeQueryDescriptorAction = null;
204+
205+
return Self;
206+
}
207+
208+
public RangeQueryDescriptor NumberRange(NumberRangeQueryDescriptor descriptor)
209+
{
210+
_dateRangeQueryDescriptor = null;
211+
_dateRangeQueryDescriptorAction = null;
212+
_numberRangeQueryDescriptor = descriptor;
213+
_numberRangeQueryDescriptorAction = null;
214+
215+
return Self;
216+
}
217+
218+
protected override void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, IElasticsearchClientSettings settings)
219+
{
220+
if (_dateRangeQueryDescriptor is not null)
221+
{
222+
JsonSerializer.Serialize(writer, _dateRangeQueryDescriptor, options);
223+
}
224+
else if (_dateRangeQueryDescriptorAction is not null)
225+
{
226+
JsonSerializer.Serialize(writer, new DateRangeQueryDescriptor(_dateRangeQueryDescriptorAction), options);
227+
}
228+
else if (_numberRangeQueryDescriptor is not null)
229+
{
230+
JsonSerializer.Serialize(writer, _numberRangeQueryDescriptor, options);
231+
}
232+
else if (_numberRangeQueryDescriptorAction is not null)
233+
{
234+
JsonSerializer.Serialize(writer, new NumberRangeQueryDescriptor(_numberRangeQueryDescriptorAction), options);
235+
}
236+
}
237+
}
238+

src/Elastic.Clients.Elasticsearch/_Generated/Types/Enums/Enums.QueryDsl.g.cs

+49
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,55 @@ public override void Write(Utf8JsonWriter writer, Operator value, JsonSerializer
453453
}
454454
}
455455

456+
[JsonConverter(typeof(RangeRelationConverter))]
457+
public enum RangeRelation
458+
{
459+
[EnumMember(Value = "within")]
460+
Within,
461+
[EnumMember(Value = "intersects")]
462+
Intersects,
463+
[EnumMember(Value = "contains")]
464+
Contains
465+
}
466+
467+
internal sealed class RangeRelationConverter : JsonConverter<RangeRelation>
468+
{
469+
public override RangeRelation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
470+
{
471+
var enumString = reader.GetString();
472+
switch (enumString)
473+
{
474+
case "within":
475+
return RangeRelation.Within;
476+
case "intersects":
477+
return RangeRelation.Intersects;
478+
case "contains":
479+
return RangeRelation.Contains;
480+
}
481+
482+
ThrowHelper.ThrowJsonException();
483+
return default;
484+
}
485+
486+
public override void Write(Utf8JsonWriter writer, RangeRelation value, JsonSerializerOptions options)
487+
{
488+
switch (value)
489+
{
490+
case RangeRelation.Within:
491+
writer.WriteStringValue("within");
492+
return;
493+
case RangeRelation.Intersects:
494+
writer.WriteStringValue("intersects");
495+
return;
496+
case RangeRelation.Contains:
497+
writer.WriteStringValue("contains");
498+
return;
499+
}
500+
501+
writer.WriteNullValue();
502+
}
503+
}
504+
456505
[JsonConverter(typeof(TextQueryTypeConverter))]
457506
public enum TextQueryType
458507
{

0 commit comments

Comments
 (0)