Skip to content

Commit 4000f45

Browse files
authored
CSHARP-5429: Support strings with range operator for atlas search (#1588)
1 parent 8bdee88 commit 4000f45

File tree

7 files changed

+327
-64
lines changed

7 files changed

+327
-64
lines changed

src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -358,31 +358,43 @@ private protected override BsonDocument RenderArguments(RenderArgs<TDocument> ar
358358
}
359359

360360
internal sealed class RangeSearchDefinition<TDocument, TField> : OperatorSearchDefinition<TDocument>
361-
where TField : struct, IComparable<TField>
362361
{
363-
private readonly SearchRange<TField> _range;
364-
private readonly BsonValue _min;
365-
private readonly BsonValue _max;
366-
362+
private readonly SearchRangeV2<TField> _range;
363+
367364
public RangeSearchDefinition(
368365
SearchPathDefinition<TDocument> path,
369-
SearchRange<TField> range,
366+
SearchRangeV2<TField> range,
370367
SearchScoreDefinition<TDocument> score)
371368
: base(OperatorType.Range, path, score)
372369
{
373370
_range = range;
374-
_min = ToBsonValue(_range.Min);
375-
_max = ToBsonValue(_range.Max);
376371
}
377372

378-
private protected override BsonDocument RenderArguments(RenderArgs<TDocument> args) =>
379-
new()
373+
private protected override BsonDocument RenderArguments(RenderArgs<TDocument> args)
374+
{
375+
BsonValue min = null, max = null;
376+
bool minInclusive = false, maxInclusive = false;
377+
378+
if (_range.Min != null)
379+
{
380+
min = ToBsonValue(_range.Min.Value);
381+
minInclusive = _range.Min.Inclusive;
382+
}
383+
384+
if (_range.Max != null)
380385
{
381-
{ _range.IsMinInclusive ? "gte" : "gt", _min, _min != null },
382-
{ _range.IsMaxInclusive ? "lte" : "lt", _max, _max != null },
386+
max = ToBsonValue(_range.Max.Value);
387+
maxInclusive = _range.Max.Inclusive;
388+
}
389+
390+
return new()
391+
{
392+
{ minInclusive ? "gte" : "gt", min, min != null },
393+
{ maxInclusive ? "lte" : "lt", max, max != null }
383394
};
384-
385-
private static BsonValue ToBsonValue(TField? value) =>
395+
}
396+
397+
private static BsonValue ToBsonValue(TField value) =>
386398
value switch
387399
{
388400
sbyte v => (BsonInt32)v,
@@ -396,6 +408,7 @@ private static BsonValue ToBsonValue(TField? value) =>
396408
double v => (BsonDouble)v,
397409
DateTime v => (BsonDateTime)v,
398410
DateTimeOffset v => (BsonDateTime)v.UtcDateTime,
411+
string v => (BsonString)v,
399412
null => null,
400413
_ => throw new InvalidCastException()
401414
};

src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,54 @@ public SearchDefinition<TDocument> Range<TField>(
639639
SearchRange<TField> range,
640640
SearchScoreDefinition<TDocument> score = null)
641641
where TField : struct, IComparable<TField> =>
642-
new RangeSearchDefinition<TDocument, TField>(path, range, score);
642+
Range(
643+
path,
644+
new SearchRangeV2<TField>(
645+
range.Min.HasValue ? new(range.Min.Value, range.IsMinInclusive) : null,
646+
range.Max.HasValue ? new(range.Max.Value, range.IsMaxInclusive) : null),
647+
score);
648+
649+
/// <summary>
650+
/// Creates a search definition that queries for documents where a field is in the specified range.
651+
/// </summary>
652+
/// <typeparam name="TField">The type of the field.</typeparam>
653+
/// <param name="path">The indexed field or fields to search.</param>
654+
/// <param name="range">The field range.</param>
655+
/// <param name="score">The score modifier.</param>
656+
/// <returns>A range search definition.</returns>
657+
public SearchDefinition<TDocument> Range<TField>(
658+
Expression<Func<TDocument, TField>> path,
659+
SearchRangeV2<TField> range,
660+
SearchScoreDefinition<TDocument> score = null) =>
661+
Range(new ExpressionFieldDefinition<TDocument>(path), range, score);
662+
663+
/// <summary>
664+
/// Creates a search definition that queries for documents where a field is in the specified range.
665+
/// </summary>
666+
/// <typeparam name="TField">The type of the field.</typeparam>
667+
/// <param name="path">The indexed field or fields to search.</param>
668+
/// <param name="range">The field range.</param>
669+
/// <param name="score">The score modifier.</param>
670+
/// <returns>A range search definition.</returns>
671+
public SearchDefinition<TDocument> Range<TField>(
672+
Expression<Func<TDocument, IEnumerable<TField>>> path,
673+
SearchRangeV2<TField> range,
674+
SearchScoreDefinition<TDocument> score = null) =>
675+
Range(new ExpressionFieldDefinition<TDocument>(path), range, score);
676+
677+
/// <summary>
678+
/// Creates a search definition that queries for documents where a field is in the specified range.
679+
/// </summary>
680+
/// <typeparam name="TField">The type of the field.</typeparam>
681+
/// <param name="path">The indexed field or fields to search.</param>
682+
/// <param name="range">The field range.</param>
683+
/// <param name="score">The score modifier.</param>
684+
/// <returns>A range search definition.</returns>
685+
public SearchDefinition<TDocument> Range<TField>(
686+
SearchPathDefinition<TDocument> path,
687+
SearchRangeV2<TField> range,
688+
SearchScoreDefinition<TDocument> score = null) =>
689+
new RangeSearchDefinition<TDocument, TField>(path, range, score);
643690

644691
/// <summary>
645692
/// Creates a search definition that interprets the query as a regular expression.

src/MongoDB.Driver/Search/SearchRange.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public SearchRange(TValue? min, TValue? max, bool isMinInclusive, bool isMaxIncl
5555
/// <summary>Gets the value that indicates whether the lower bound of the range is inclusive.</summary>
5656
public bool IsMinInclusive { get; }
5757

58-
/// <summary>Gets the lower bound of the range.</summary>
58+
/// <summary>Gets the upper bound of the range.</summary>
5959
public TValue? Max { get; }
6060

6161
/// <summary>Gets the lower bound of the range.</summary>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace MongoDB.Driver.Search
17+
{
18+
/// <summary>
19+
/// Object that specifies the boundaries for a range query.
20+
/// </summary>
21+
/// <typeparam name="TValue">The type of the range value.</typeparam>
22+
public struct SearchRangeV2<TValue>
23+
{
24+
#region static
25+
/// <summary>Empty range.</summary>
26+
internal static SearchRangeV2<TValue> Empty { get; } = new(null, null);
27+
#endregion
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="SearchRangeV2{TValue}"/> class.
31+
/// </summary>
32+
/// <param name="min">The lower bound of the range.</param>
33+
/// <param name="max">The upper bound of the range.</param>
34+
public SearchRangeV2(Bound<TValue> min, Bound<TValue> max)
35+
{
36+
Min = min;
37+
Max = max;
38+
}
39+
40+
/// <summary>Gets the upper bound of the range.</summary>
41+
public Bound<TValue> Max { get; }
42+
43+
/// <summary>Gets the lower bound of the range.</summary>
44+
public Bound<TValue> Min { get; }
45+
}
46+
47+
/// <summary>
48+
/// Represents a bound value.
49+
/// </summary>
50+
/// <typeparam name="TValue">The type of the bound value.</typeparam>
51+
public sealed class Bound<TValue>
52+
{
53+
/// <summary>
54+
/// Gets the bound value.
55+
/// </summary>
56+
public TValue Value { get; }
57+
58+
/// <summary>
59+
/// Gets whether the bound is inclusive or not.
60+
/// </summary>
61+
public bool Inclusive { get; }
62+
63+
/// <summary>
64+
/// Initializes a new instance of the <see cref="Bound{TValue}"/> class.
65+
/// </summary>
66+
/// <param name="value">The value of the bound.</param>
67+
/// <param name="inclusive">Indicates whether the bound is inclusive or not.</param>
68+
public Bound(TValue value, bool inclusive = false)
69+
{
70+
Value = value;
71+
Inclusive = inclusive;
72+
}
73+
}
74+
75+
/// <summary>
76+
/// A builder for a SearchRangeV2.
77+
/// </summary>
78+
public static class SearchRangeV2Builder
79+
{
80+
/// <summary>
81+
/// Creates a greater than search range.
82+
/// </summary>
83+
/// <typeparam name="TValue">The type of the value.</typeparam>
84+
/// <param name="value">The value.</param>
85+
/// <returns>Search range.</returns>
86+
public static SearchRangeV2<TValue> Gt<TValue>(TValue value) => SearchRangeV2<TValue>.Empty.Gt(value);
87+
88+
/// <summary>
89+
/// Adds a greater than value to a search range.
90+
/// </summary>
91+
/// <typeparam name="TValue">The type of the value.</typeparam>
92+
/// <param name="searchRange">Search range.</param>
93+
/// <param name="value">The value.</param>
94+
/// <returns>Search range.</returns>
95+
public static SearchRangeV2<TValue> Gt<TValue>(this SearchRangeV2<TValue> searchRange, TValue value)
96+
=> new(min: new (value), searchRange.Max);
97+
98+
/// <summary>
99+
/// Creates a greater or equal than search range.
100+
/// </summary>
101+
/// <typeparam name="TValue">The type of the value.</typeparam>
102+
/// <param name="value">The value.</param>
103+
/// <returns>Search range.</returns>
104+
public static SearchRangeV2<TValue> Gte<TValue>(TValue value)
105+
=> SearchRangeV2<TValue>.Empty.Gte(value);
106+
107+
/// <summary>
108+
/// Adds a greater or equal than value to a search range.
109+
/// </summary>
110+
/// <typeparam name="TValue">The type of the value.</typeparam>
111+
/// <param name="searchRange">Search range.</param>
112+
/// <param name="value">The value.</param>
113+
/// <returns>Search range.</returns>
114+
public static SearchRangeV2<TValue> Gte<TValue>(this SearchRangeV2<TValue> searchRange, TValue value)
115+
=> new(min: new(value, inclusive: true), searchRange.Max);
116+
117+
/// <summary>
118+
/// Creates a less than search range.
119+
/// </summary>
120+
/// <typeparam name="TValue">The type of the value.</typeparam>
121+
/// <param name="value">The value.</param>
122+
/// <returns>Search range.</returns>
123+
public static SearchRangeV2<TValue> Lt<TValue>(TValue value)
124+
=> SearchRangeV2<TValue>.Empty.Lt(value);
125+
126+
/// <summary>
127+
/// Adds a less than value to a search range.
128+
/// </summary>
129+
/// <typeparam name="TValue">The type of the value.</typeparam>
130+
/// <param name="searchRange">Search range.</param>
131+
/// <param name="value">The value.</param>
132+
/// <returns>Search range.</returns>
133+
public static SearchRangeV2<TValue> Lt<TValue>(this SearchRangeV2<TValue> searchRange, TValue value)
134+
=> new(searchRange.Min, max: new(value));
135+
136+
/// <summary>
137+
/// Creates a less than or equal search range.
138+
/// </summary>
139+
/// <typeparam name="TValue">The type of the value.</typeparam>
140+
/// <param name="value">The value.</param>
141+
/// <returns>search range.</returns>
142+
public static SearchRangeV2<TValue> Lte<TValue>(TValue value)
143+
=> SearchRangeV2<TValue>.Empty.Lte(value);
144+
145+
/// <summary>
146+
/// Adds a less than or equal value to a search range.
147+
/// </summary>
148+
/// <typeparam name="TValue">The type of the value.</typeparam>
149+
/// <param name="searchRange">Search range.</param>
150+
/// <param name="value">The value.</param>
151+
/// <returns>search range.</returns>
152+
public static SearchRangeV2<TValue> Lte<TValue>(this SearchRangeV2<TValue> searchRange, TValue value)
153+
=> new(searchRange.Min, max: new(value, inclusive: true));
154+
}
155+
}

tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,11 +444,27 @@ public void Range()
444444
{
445445
var results = GeoSearch(
446446
GeoBuilders.Search.Compound().Must(
447-
GeoBuilders.Search.Range(x => x.Bedrooms, SearchRangeBuilder.Gt(2).Lt(4)),
448-
GeoBuilders.Search.Range(x => x.Beds, SearchRangeBuilder.Gte(14).Lte(14))));
447+
GeoBuilders.Search.Range(x => x.Bedrooms, SearchRangeV2Builder.Gt(2).Lt(4)),
448+
GeoBuilders.Search.Range(x => x.Beds, SearchRangeV2Builder.Gte(14).Lte(14))));
449449

450450
results.Should().ContainSingle().Which.Name.Should().Be("House close to station & direct to opera house....");
451451
}
452+
453+
[Fact]
454+
public void RangeString()
455+
{
456+
var results = GetSynonymTestCollection().Aggregate()
457+
.Search(Builders<Movie>.Search.Range(p => p.Title, SearchRangeV2Builder.Gt("city").Lt("country")))
458+
.Limit(5)
459+
.Project<Movie>(Builders<Movie>.Projection.Include(p => p.Title))
460+
.ToList();
461+
462+
results[0].Title.Should().Be("Civilization");
463+
results[1].Title.Should().Be("Clash of the Wolves");
464+
results[2].Title.Should().Be("City Lights");
465+
results[3].Title.Should().Be("Comradeship");
466+
results[4].Title.Should().Be("Come and Get It");
467+
}
452468

453469
[Fact]
454470
public void SearchSequenceToken()

0 commit comments

Comments
 (0)