Skip to content

Commit adc68c8

Browse files
authored
CSHARP-4778: Add synonym support to text operator for Atlas Search (#1186)
1 parent cb90413 commit adc68c8

File tree

4 files changed

+112
-3
lines changed

4 files changed

+112
-3
lines changed

src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,23 +399,27 @@ internal sealed class TextSearchDefinition<TDocument> : OperatorSearchDefinition
399399
{
400400
private readonly SearchFuzzyOptions _fuzzy;
401401
private readonly SearchQueryDefinition _query;
402+
private readonly string _synonyms;
402403

403404
public TextSearchDefinition(
404405
SearchPathDefinition<TDocument> path,
405406
SearchQueryDefinition query,
406407
SearchFuzzyOptions fuzzy,
407-
SearchScoreDefinition<TDocument> score)
408+
SearchScoreDefinition<TDocument> score,
409+
string synonyms)
408410
: base(OperatorType.Text, path, score)
409411
{
410412
_query = Ensure.IsNotNull(query, nameof(query));
411413
_fuzzy = fuzzy;
414+
_synonyms = synonyms;
412415
}
413416

414417
private protected override BsonDocument RenderArguments(SearchDefinitionRenderContext<TDocument> renderContext) =>
415418
new()
416419
{
417420
{ "query", _query.Render() },
418421
{ "fuzzy", () => _fuzzy.Render(), _fuzzy != null },
422+
{ "synonyms", _synonyms, _synonyms != null }
419423
};
420424
}
421425

src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,23 @@ public SearchDefinition<TDocument> Text(
627627
SearchQueryDefinition query,
628628
SearchFuzzyOptions fuzzy = null,
629629
SearchScoreDefinition<TDocument> score = null) =>
630-
new TextSearchDefinition<TDocument>(path, query, fuzzy, score);
630+
new TextSearchDefinition<TDocument>(path, query, fuzzy, score, null);
631+
632+
/// <summary>
633+
/// Creates a search definition that performs full-text search with synonyms using the analyzer specified
634+
/// in the index configuration.
635+
/// </summary>
636+
/// <param name="path">The indexed field or fields to search.</param>
637+
/// <param name="query">The string or strings to search for.</param>
638+
/// <param name="synonyms">The name of the synonym mapping definition in the index definition</param>
639+
/// <param name="score">The score modifier.</param>
640+
/// <returns>A text search definition.</returns>
641+
public SearchDefinition<TDocument> Text(
642+
SearchPathDefinition<TDocument> path,
643+
SearchQueryDefinition query,
644+
string synonyms,
645+
SearchScoreDefinition<TDocument> score = null) =>
646+
new TextSearchDefinition<TDocument>(path, query, null, score, synonyms);
631647

632648
/// <summary>
633649
/// Creates a search definition that performs full-text search using the analyzer specified
@@ -646,6 +662,23 @@ public SearchDefinition<TDocument> Text<TField>(
646662
SearchScoreDefinition<TDocument> score = null) =>
647663
Text(new ExpressionFieldDefinition<TDocument>(path), query, fuzzy, score);
648664

665+
/// <summary>
666+
/// Creates a search definition that performs full-text search with synonyms using the analyzer specified
667+
/// in the index configuration.
668+
/// </summary>
669+
/// <typeparam name="TField">The type of the field.</typeparam>
670+
/// <param name="path">The indexed field or field to search.</param>
671+
/// <param name="query">The string or strings to search for.</param>
672+
/// <param name="synonyms">The name of the synonym mapping definition in the index definition</param>
673+
/// <param name="score">The score modifier.</param>
674+
/// <returns>A text search definition.</returns>
675+
public SearchDefinition<TDocument> Text<TField>(
676+
Expression<Func<TDocument, TField>> path,
677+
SearchQueryDefinition query,
678+
string synonyms,
679+
SearchScoreDefinition<TDocument> score = null) =>
680+
Text(new ExpressionFieldDefinition<TDocument>(path), query, synonyms, score);
681+
649682
/// <summary>
650683
/// Creates a search definition that uses special characters in the search string that can
651684
/// match any character.

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,51 @@ public void Text()
477477
result.Title.Should().Be("Declaration of Independence");
478478
}
479479

480+
[Theory]
481+
[InlineData("automobile", "transportSynonyms", "Blue Car")]
482+
[InlineData("boat", "transportSynonyms", "And the Ship Sails On")]
483+
public void TextWithSynonymsReturnsCorrectResult(string query, string synonym, string expected)
484+
{
485+
var sortDefinition = Builders<Movie>.Sort.Ascending(x => x.Title);
486+
var result =
487+
GetSynonymTestCollection().Aggregate()
488+
.Search(Builders<Movie>.Search.Text(x => x.Title, query, synonym), indexName: "synonyms-tests")
489+
.Sort(sortDefinition)
490+
.Project<Movie>(Builders<Movie>.Projection.Include("Title").Exclude("_id"))
491+
.Limit(1)
492+
.Single();
493+
494+
result.Title.Should().Be(expected);
495+
}
496+
497+
[Fact]
498+
public void TextWithSynonymsMappings()
499+
{
500+
var automobileAndAttireSearchResults = SearchMultipleSynonymMapping(
501+
Builders<Movie>.Search.Text(x => x.Title, "automobile", "transportSynonyms"),
502+
Builders<Movie>.Search.Text(x => x.Title, "attire", "attireSynonyms"));
503+
504+
var vehicleAndDressSearchResults = SearchMultipleSynonymMapping(
505+
Builders<Movie>.Search.Text(x => x.Title, "vehicle", "transportSynonyms"),
506+
Builders<Movie>.Search.Text(x => x.Title, "dress", "attireSynonyms"));
507+
508+
var boatAndHatSearchResults = SearchMultipleSynonymMapping(
509+
Builders<Movie>.Search.Text(x => x.Title, "boat", "transportSynonyms"),
510+
Builders<Movie>.Search.Text(x => x.Title, "hat", "attireSynonyms"));
511+
512+
var vesselAndFedoraSearchResults = SearchMultipleSynonymMapping(
513+
Builders<Movie>.Search.Text(x => x.Title, "vessel", "transportSynonyms"),
514+
Builders<Movie>.Search.Text(x => x.Title, "fedora", "attireSynonyms"));
515+
516+
automobileAndAttireSearchResults.Should().NotBeNull();
517+
vehicleAndDressSearchResults.Should().NotBeNull();
518+
boatAndHatSearchResults.Should().NotBeNull();
519+
vesselAndFedoraSearchResults.Should().NotBeNull();
520+
521+
automobileAndAttireSearchResults.Should().BeEquivalentTo(vehicleAndDressSearchResults);
522+
boatAndHatSearchResults.Should().NotBeEquivalentTo(vesselAndFedoraSearchResults);
523+
}
524+
480525
[Fact]
481526
public void Wildcard()
482527
{
@@ -500,9 +545,15 @@ private HistoricalDocument SearchSingle(
500545
fluent = fluent.Project(projectionDefinition);
501546
}
502547

503-
return fluent.Limit(1).ToList().Single();
548+
return fluent.Limit(1).Single();
504549
}
505550

551+
private List<BsonDocument> SearchMultipleSynonymMapping(params SearchDefinition<Movie>[] clauses) =>
552+
GetSynonymTestCollection().Aggregate()
553+
.Search(Builders<Movie>.Search.Compound().Should(clauses), indexName: "synonyms-tests")
554+
.Project(Builders<Movie>.Projection.Include("Title").Exclude("_id"))
555+
.ToList();
556+
506557
private IMongoCollection<HistoricalDocument> GetTestCollection() => _disposableMongoClient
507558
.GetDatabase("sample_training")
508559
.GetCollection<HistoricalDocument>("posts");
@@ -511,6 +562,10 @@ private IMongoCollection<T> GetTestCollection<T>() => _disposableMongoClient
511562
.GetDatabase("sample_training")
512563
.GetCollection<T>("posts");
513564

565+
private IMongoCollection<Movie> GetSynonymTestCollection() => _disposableMongoClient
566+
.GetDatabase("sample_mflix")
567+
.GetCollection<Movie>("movies");
568+
514569
private IMongoCollection<AirbnbListing> GetGeoTestCollection() => _disposableMongoClient
515570
.GetDatabase("sample_airbnb")
516571
.GetCollection<AirbnbListing>("listingsAndReviews");
@@ -522,6 +577,13 @@ public class Comment
522577
public string Author { get; set; }
523578
}
524579

580+
[BsonIgnoreExtraElements]
581+
public class Movie
582+
{
583+
[BsonElement("title")]
584+
public string Title { get; set; }
585+
}
586+
525587
[BsonIgnoreExtraElements]
526588
public class HistoricalDocumentWithCommentsOnly
527589
{

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,10 @@ public void Text()
939939
subject.Text(new[] { "x", "y" }, new[] { "foo", "bar" }),
940940
"{ text: { query: ['foo', 'bar'], path: ['x', 'y'] } }");
941941

942+
AssertRendered(
943+
subject.Text(new[] { "x", "y" }, new[] { "foo", "bar" }, "testSynonyms"),
944+
"{ text: { query: ['foo', 'bar'], synonyms: 'testSynonyms', path: ['x', 'y'] } }");
945+
942946
AssertRendered(
943947
subject.Text("x", "foo", new SearchFuzzyOptions()),
944948
"{ text: { query: 'foo', path: 'x', fuzzy: {} } }");
@@ -955,6 +959,9 @@ public void Text()
955959
AssertRendered(
956960
subject.Text("x", "foo", score: scoreBuilder.Constant(1)),
957961
"{ text: { query: 'foo', path: 'x', score: { constant: { value: 1 } } } }");
962+
AssertRendered(
963+
subject.Text("x", "foo", "testSynonyms", scoreBuilder.Constant(1)),
964+
"{ text: { query: 'foo', synonyms: 'testSynonyms', path: 'x', score: { constant: { value: 1 } } } }");
958965
}
959966

960967
[Fact]
@@ -985,6 +992,9 @@ public void Text_typed()
985992
AssertRendered(
986993
subject.Text(x => x.FirstName, new[] { "foo", "bar" }),
987994
"{ text: { query: ['foo', 'bar'], path: 'fn' } }");
995+
AssertRendered(
996+
subject.Text(x => x.FirstName, new[] { "foo", "bar" }, "testSynonyms"),
997+
"{ text: { query: ['foo', 'bar'], synonyms: 'testSynonyms', path: 'fn' } }");
988998
AssertRendered(
989999
subject.Text("FirstName", new[] { "foo", "bar" }),
9901000
"{ text: { query: ['foo', 'bar'], path: 'fn' } }");

0 commit comments

Comments
 (0)