Skip to content

Commit d19790d

Browse files
authored
CSHARP-5420: Driver Support for Search Sequential Pagination (#1558)
1 parent 5edf5ba commit d19790d

File tree

6 files changed

+159
-2
lines changed

6 files changed

+159
-2
lines changed

src/MongoDB.Driver/IAggregateFluent.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ IAggregateFluent<TNewResult> Lookup<TForeignDocument, TAsElement, TAs, TNewResul
404404
IAggregateFluent<BsonDocument> SetWindowFields<TWindowFields>(
405405
AggregateExpressionDefinition<ISetWindowFieldsPartition<TResult>, TWindowFields> output);
406406

407+
//TODO If I add a parameter here, then this would be a binary breaking change
407408
/// <summary>
408409
/// Appends a $search stage to the pipeline.
409410
/// </summary>

src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,8 @@ public static PipelineStageDefinition<TInput, TInput> Search<TInput>(
13861386
renderedSearchDefinition.Add("returnStoredSource", searchOptions.ReturnStoredSource, searchOptions.ReturnStoredSource);
13871387
renderedSearchDefinition.Add("scoreDetails", searchOptions.ScoreDetails, searchOptions.ScoreDetails);
13881388
renderedSearchDefinition.Add("tracking", () => searchOptions.Tracking.Render(), searchOptions.Tracking != null);
1389+
renderedSearchDefinition.Add("searchAfter", () => searchOptions.SearchAfter, searchOptions.SearchAfter != null);
1390+
renderedSearchDefinition.Add("searchBefore", () => searchOptions.SearchBefore, searchOptions.SearchBefore != null);
13891391

13901392
var document = new BsonDocument(operatorName, renderedSearchDefinition);
13911393
return new RenderedPipelineStageDefinition<TInput>(operatorName, document, args.DocumentSerializer);

src/MongoDB.Driver/ProjectionDefinitionBuilder.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,40 @@ public static ProjectionDefinition<TDocument> MetaSearchScoreDetails<TDocument>(
257257
return builder.Combine(projection, builder.MetaSearchScoreDetails(field));
258258
}
259259

260+
/// <summary>
261+
/// Combines an existing projection with a search sequence token projection.
262+
/// </summary>
263+
/// <typeparam name="TDocument">The type of the document.</typeparam>
264+
/// <param name="projection">The projection.</param>
265+
/// <param name="field">The field.</param>
266+
/// <returns>
267+
/// A combined projection.
268+
/// </returns>
269+
public static ProjectionDefinition<TDocument> MetaSearchSequenceToken<TDocument>(
270+
this ProjectionDefinition<TDocument> projection,
271+
FieldDefinition<TDocument> field)
272+
{
273+
var builder = Builders<TDocument>.Projection;
274+
return builder.Combine(projection, builder.MetaSearchSequenceToken(field));
275+
}
276+
277+
/// <summary>
278+
/// Combines an existing projection with a search sequence token projection.
279+
/// </summary>
280+
/// <typeparam name="TDocument">The type of the document.</typeparam>
281+
/// <param name="projection">The projection.</param>
282+
/// <param name="field">The field.</param>
283+
/// <returns>
284+
/// A combined projection.
285+
/// </returns>
286+
public static ProjectionDefinition<TDocument> MetaSearchSequenceToken<TDocument>(
287+
this ProjectionDefinition<TDocument> projection,
288+
Expression<Func<TDocument, object>> field)
289+
{
290+
var builder = Builders<TDocument>.Projection;
291+
return builder.Combine(projection, builder.MetaSearchSequenceToken(field));
292+
}
293+
260294
/// <summary>
261295
/// Combines an existing projection with a text score projection.
262296
/// </summary>
@@ -576,7 +610,7 @@ public ProjectionDefinition<TSource> Include(Expression<Func<TSource, object>> f
576610
/// <param name="field">The field.</param>
577611
/// <param name="metaFieldName">The meta field name.</param>
578612
/// <returns>
579-
/// A text score projection.
613+
/// A meta projection.
580614
/// </returns>
581615
public ProjectionDefinition<TSource> Meta(FieldDefinition<TSource> field, string metaFieldName)
582616
{
@@ -655,6 +689,30 @@ public ProjectionDefinition<TSource> MetaSearchScoreDetails<TField>(Expression<F
655689
return MetaSearchScoreDetails(new ExpressionFieldDefinition<TSource>(field));
656690
}
657691

692+
/// <summary>
693+
/// Creates a search sequence token projection.
694+
/// </summary>
695+
/// <param name="field">The field.</param>
696+
/// <returns>
697+
/// A search sequence token projection.
698+
/// </returns>
699+
public ProjectionDefinition<TSource> MetaSearchSequenceToken(FieldDefinition<TSource> field)
700+
{
701+
return Meta(field, "searchSequenceToken");
702+
}
703+
704+
/// <summary>
705+
/// Creates a search sequence token projection.
706+
/// </summary>
707+
/// <param name="field">The field.</param>
708+
/// <returns>
709+
/// A search sequence token projection.
710+
/// </returns>
711+
public ProjectionDefinition<TSource> MetaSearchSequenceToken<TField>(Expression<Func<TSource, TField>> field)
712+
{
713+
return MetaSearchSequenceToken(new ExpressionFieldDefinition<TSource>(field));
714+
}
715+
658716
/// <summary>
659717
/// Creates a text score projection.
660718
/// </summary>

src/MongoDB.Driver/Search/SearchOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,17 @@ public sealed class SearchOptions<TDocument>
5757
/// Gets or sets the options for tracking search terms.
5858
/// </summary>
5959
public SearchTrackingOptions Tracking { get; set; }
60+
61+
/// <summary>
62+
/// Gets or sets the "after" reference point for pagination.
63+
/// When set, the search retrieves documents starting immediately after the specified reference point.
64+
/// </summary>
65+
public string SearchAfter { get; set; }
66+
67+
/// <summary>
68+
/// Gets or sets the "before" reference point for pagination.
69+
/// When set, the search retrieves documents starting immediately before the specified reference point.
70+
/// </summary>
71+
public string SearchBefore { get; set; }
6072
}
6173
}

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

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,6 @@ public void Phrase()
288288
{
289289
// This test case exercises the indexName and returnStoredSource arguments. The
290290
// remaining test cases omit them.
291-
var coll = GetTestCollection();
292291
var results = GetTestCollection().Aggregate()
293292
.Search(Builders.Search.Phrase(x => x.Body, "life, liberty, and the pursuit of happiness"),
294293
new SearchHighlightOptions<HistoricalDocument>(x => x.Body),
@@ -381,6 +380,78 @@ public void Range()
381380
results.Should().ContainSingle().Which.Name.Should().Be("House close to station & direct to opera house....");
382381
}
383382

383+
[Fact]
384+
public void SearchSequenceToken()
385+
{
386+
const int limitVal = 10;
387+
var titles = new[]
388+
{
389+
"Equinox Flower",
390+
"Flower Drum Song",
391+
"Cactus Flower",
392+
"The Flower of My Secret",
393+
};
394+
395+
var searchDefinition = Builders<Movie>.Search.Text(t => t.Title, "flower");
396+
var searchOptions = new SearchOptions<Movie>
397+
{
398+
IndexName = "default",
399+
Sort = Builders<Movie>.Sort.Ascending("year")
400+
};
401+
var projection = Builders<Movie>.Projection
402+
.Include(x => x.Title)
403+
.MetaSearchSequenceToken(x => x.PaginationToken);
404+
405+
// Base search
406+
var baseSearchResults = GetSynonymTestCollection()
407+
.Aggregate()
408+
.Search(searchDefinition, searchOptions)
409+
.Project<Movie>(projection)
410+
.Limit(limitVal)
411+
.ToList();
412+
413+
baseSearchResults.Count.Should().Be(limitVal);
414+
baseSearchResults.ForEach( m => m.PaginationToken.Should().NotBeNullOrEmpty());
415+
baseSearchResults[0].Title.Should().Be(titles[0]);
416+
baseSearchResults[1].Title.Should().Be(titles[1]);
417+
baseSearchResults[2].Title.Should().Be(titles[2]);
418+
baseSearchResults[3].Title.Should().Be(titles[3]);
419+
420+
// Testing SearchAfter
421+
// We're searching after the 2nd result of the base search
422+
searchOptions.SearchAfter = baseSearchResults[1].PaginationToken;
423+
var searchAfterResults = GetSynonymTestCollection()
424+
.Aggregate()
425+
.Search(searchDefinition, searchOptions)
426+
.Project<Movie>(projection)
427+
.Limit(limitVal)
428+
.ToList();
429+
430+
searchAfterResults.Count.Should().Be(limitVal);
431+
searchAfterResults.ForEach( m => m.PaginationToken.Should().NotBeNullOrEmpty());
432+
searchAfterResults[0].Title.Should().Be(titles[2]);
433+
searchAfterResults[1].Title.Should().Be(titles[3]);
434+
435+
// Testing SearchBefore
436+
// We're searching before the 4th result of the base search
437+
searchOptions.SearchAfter = null;
438+
searchOptions.SearchBefore = baseSearchResults[3].PaginationToken;
439+
var searchBeforeResults = GetSynonymTestCollection()
440+
.Aggregate()
441+
.Search(searchDefinition, searchOptions)
442+
.Project<Movie>(projection)
443+
.Limit(limitVal)
444+
.ToList();
445+
446+
// We only get the first 3 elements of the base search
447+
searchBeforeResults.Count.Should().Be(3);
448+
searchBeforeResults.ForEach( m => m.PaginationToken.Should().NotBeNullOrEmpty());
449+
// With searchBefore the results are reversed
450+
searchBeforeResults[0].Title.Should().Be(titles[2]);
451+
searchBeforeResults[1].Title.Should().Be(titles[1]);
452+
searchBeforeResults[2].Title.Should().Be(titles[0]);
453+
}
454+
384455
[Fact]
385456
public void Search_count_lowerBound()
386457
{
@@ -686,6 +757,9 @@ public class Movie
686757

687758
[BsonElement("score")]
688759
public double Score { get; set; }
760+
761+
[BsonElement("paginationToken")]
762+
public string PaginationToken { get; set; }
689763
}
690764

691765
[BsonIgnoreExtraElements]

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ public void MetaSearchScoreDetails()
5353
AssertRendered(subjectTyped.MetaSearchScoreDetails(p => p.MetaField), "{ mf : { $meta: 'searchScoreDetails' } }");
5454
}
5555

56+
[Fact]
57+
public void MetaSearchSequenceToken()
58+
{
59+
var subject = CreateSubject<BsonDocument>();
60+
AssertRendered(subject.MetaSearchSequenceToken("a"), "{ a : { $meta: 'searchSequenceToken' } }");
61+
62+
var subjectTyped = CreateSubject<SimplestPerson>();
63+
AssertRendered(subjectTyped.MetaSearchSequenceToken(p => p.MetaField), "{ mf : { $meta: 'searchSequenceToken' } }");
64+
}
65+
5666
[Fact]
5767
public void MetaVectorSearchScore()
5868
{

0 commit comments

Comments
 (0)