diff --git a/src/MongoDB.Driver/Search/MatchCriteria.cs b/src/MongoDB.Driver/Search/MatchCriteria.cs new file mode 100644 index 00000000000..4f7b74aaf42 --- /dev/null +++ b/src/MongoDB.Driver/Search/MatchCriteria.cs @@ -0,0 +1,32 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.Driver.Search +{ + /// + /// Represents the criteria used to match terms in a query for the Atlas Search Text operator. + /// + public enum MatchCriteria + { + /// + /// Match documents containing any of the terms from a query. + /// + Any, + /// + /// Match documents containing all the terms from a query. + /// + All + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs b/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs index b4686e3a815..28304fae5ad 100644 --- a/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs +++ b/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs @@ -328,16 +328,17 @@ internal sealed class PhraseSearchDefinition : OperatorSearchDefiniti { private readonly SearchQueryDefinition _query; private readonly int? _slop; + private readonly string _synonyms; public PhraseSearchDefinition( SearchPathDefinition path, SearchQueryDefinition query, - int? slop, - SearchScoreDefinition score) - : base(OperatorType.Phrase, path, score) + SearchPhraseOptions options) + : base(OperatorType.Phrase, path, options?.Score) { _query = Ensure.IsNotNull(query, nameof(query)); - _slop = slop; + _slop = options?.Slop; + _synonyms = options?.Synonyms; } private protected override BsonDocument RenderArguments( @@ -345,7 +346,8 @@ private protected override BsonDocument RenderArguments( IBsonSerializer fieldSerializer) => new() { { "query", _query.Render() }, - { "slop", _slop, _slop != null } + { "slop", _slop, _slop != null }, + { "synonyms", _synonyms, _synonyms != null } }; } @@ -461,29 +463,40 @@ private protected override BsonDocument RenderArguments( internal sealed class TextSearchDefinition : OperatorSearchDefinition { private readonly SearchFuzzyOptions _fuzzy; + private readonly string _matchCriteria; private readonly SearchQueryDefinition _query; private readonly string _synonyms; public TextSearchDefinition( SearchPathDefinition path, SearchQueryDefinition query, - SearchFuzzyOptions fuzzy, - SearchScoreDefinition score, - string synonyms) - : base(OperatorType.Text, path, score) + SearchTextOptions options) + : base(OperatorType.Text, path, options?.Score) { _query = Ensure.IsNotNull(query, nameof(query)); - _fuzzy = fuzzy; - _synonyms = synonyms; + _fuzzy = options?.Fuzzy; + _synonyms = options?.Synonyms; + _matchCriteria = options?.MatchCriteria switch + { + MatchCriteria.All => "all", + MatchCriteria.Any => "any", + null => null, + _ => throw new ArgumentException("Invalid match criteria set for Atlas Search text operator.") + }; } - private protected override BsonDocument RenderArguments(RenderArgs args, - IBsonSerializer fieldSerializer) => new() + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) + { + return new BsonDocument { { "query", _query.Render() }, { "fuzzy", () => _fuzzy.Render(), _fuzzy != null }, - { "synonyms", _synonyms, _synonyms != null } + { "synonyms", _synonyms, _synonyms != null }, + { "matchCriteria", _matchCriteria, _matchCriteria != null } }; + } } internal sealed class WildcardSearchDefinition : OperatorSearchDefinition diff --git a/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs b/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs index 09c5172a7f8..21f51069093 100644 --- a/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs +++ b/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs @@ -550,7 +550,21 @@ public SearchDefinition Phrase( SearchQueryDefinition query, int? slop = null, SearchScoreDefinition score = null) => - new PhraseSearchDefinition(path, query, slop, score); + new PhraseSearchDefinition(path, query, new SearchPhraseOptions { Slop = slop, Score = score }); + + /// + /// Creates a search definition that performs search for documents containing an ordered + /// sequence of terms. + /// + /// The indexed field or fields to search. + /// The string or strings to search for. + /// The options. + /// A phrase search definition. + public SearchDefinition Phrase( + SearchPathDefinition path, + SearchQueryDefinition query, + SearchPhraseOptions options) => + new PhraseSearchDefinition(path, query, options); /// /// Creates a search definition that performs search for documents containing an ordered @@ -569,6 +583,21 @@ public SearchDefinition Phrase( SearchScoreDefinition score = null) => Phrase(new ExpressionFieldDefinition(path), query, slop, score); + /// + /// Creates a search definition that performs search for documents containing an ordered + /// sequence of terms. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The string or strings to search for. + /// The options. + /// A phrase search definition. + public SearchDefinition Phrase( + Expression> path, + SearchQueryDefinition query, + SearchPhraseOptions options) => + Phrase(new ExpressionFieldDefinition(path), query, options); + /// /// Creates a search definition that queries a combination of indexed fields and values. /// @@ -732,6 +761,20 @@ public SearchDefinition Regex( public SearchDefinition Span(SearchSpanDefinition clause) => new SpanSearchDefinition(clause); + /// + /// Creates a search definition that performs full-text search using the analyzer specified + /// in the index configuration. + /// + /// The indexed field or fields to search. + /// The string or strings to search for. + /// The options. + /// A text search definition. + public SearchDefinition Text( + SearchPathDefinition path, + SearchQueryDefinition query, + SearchTextOptions options) => + new TextSearchDefinition(path, query, options); + /// /// Creates a search definition that performs full-text search using the analyzer specified /// in the index configuration. @@ -746,7 +789,7 @@ public SearchDefinition Text( SearchQueryDefinition query, SearchFuzzyOptions fuzzy = null, SearchScoreDefinition score = null) => - new TextSearchDefinition(path, query, fuzzy, score, null); + new TextSearchDefinition(path, query, new SearchTextOptions { Fuzzy = fuzzy, Score = score }); /// /// Creates a search definition that performs full-text search with synonyms using the analyzer specified @@ -762,7 +805,22 @@ public SearchDefinition Text( SearchQueryDefinition query, string synonyms, SearchScoreDefinition score = null) => - new TextSearchDefinition(path, query, null, score, synonyms); + new TextSearchDefinition(path, query, new SearchTextOptions { Score = score, Synonyms = synonyms }); + + /// + /// Creates a search definition that performs full-text search using the analyzer specified + /// in the index configuration. + /// + /// The type of the field. + /// The indexed field or field to search. + /// The string or strings to search for. + /// The options. + /// A text search definition. + public SearchDefinition Text( + Expression> path, + SearchQueryDefinition query, + SearchTextOptions options) => + Text(new ExpressionFieldDefinition(path), query, options); /// /// Creates a search definition that performs full-text search using the analyzer specified diff --git a/src/MongoDB.Driver/Search/SearchPhraseOptions.cs b/src/MongoDB.Driver/Search/SearchPhraseOptions.cs new file mode 100644 index 00000000000..8c239a1323e --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchPhraseOptions.cs @@ -0,0 +1,38 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.Driver.Search +{ + /// + /// Options for atlas search phrase operator. + /// + public sealed class SearchPhraseOptions + { + /// + /// The score modifier. + /// + public SearchScoreDefinition Score { get; set; } + + /// + /// The allowable distance between words in the query phrase. + /// + public int? Slop { get; set; } + + /// + /// The name of the synonym mapping definition in the index definition. Value can't be an empty string (e.g. ""). + /// + public string Synonyms { get; set; } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Search/SearchTextOptions.cs b/src/MongoDB.Driver/Search/SearchTextOptions.cs new file mode 100644 index 00000000000..ceb09e564a9 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchTextOptions.cs @@ -0,0 +1,44 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.Driver.Search +{ + /// + /// Options for atlas search text operator. + /// + public sealed class SearchTextOptions + { + /// + /// The options for fuzzy search. + /// + public SearchFuzzyOptions Fuzzy { get; set; } + + /// + /// The criteria to use to match the terms in the query. Value can be either "any" or "all". + /// Defaults to "all" if omitted. + /// + public MatchCriteria? MatchCriteria { get; set; } + + /// + /// The score modifier. + /// + public SearchScoreDefinition Score { get; set; } + + /// + /// The name of the synonym mapping definition in the index definition. Value can't be an empty string (e.g. ""). + /// + public string Synonyms { get; set; } + } +} \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs index 0ea49ce015e..b014e9152a8 100644 --- a/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs +++ b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs @@ -421,6 +421,26 @@ public void PhraseAnalyzerPath() result.Title.Should().Be("Declaration of Independence"); } + [Fact] + public void PhraseSynonym() + { + var result = + GetSynonymTestCollection().Aggregate() + .Search( + Builders.Search.Phrase("plot", "automobile race", new SearchPhraseOptions { Synonyms = "transportSynonyms" }), + indexName: "synonyms-tests") + .Project(Builders.Projection.Include("Title").Exclude("_id")) + .Limit(5) + .ToList(); + + result.Count.Should().Be(5); + result[0].Title.Should().Be("The Great Race"); + result[1].Title.Should().Be("The Cannonball Run"); + result[2].Title.Should().Be("National Mechanics"); + result[3].Title.Should().Be("Genevieve"); + result[4].Title.Should().Be("Speedway Junky"); + } + [Fact] public void PhraseWildcardPath() { @@ -723,6 +743,26 @@ public void Text() result.Title.Should().Be("Declaration of Independence"); } + [Fact] + public void TextMatchCriteria() + { + var result = + GetSynonymTestCollection().Aggregate() + .Search( + Builders.Search.Text("plot", "attire", new SearchTextOptions { Synonyms = "attireSynonyms", MatchCriteria = MatchCriteria.Any}), + indexName: "synonyms-tests") + .Project(Builders.Projection.Include("Title").Exclude("_id")) + .Limit(5) + .ToList(); + + result.Count.Should().Be(5); + result[0].Title.Should().Be("The Royal Tailor"); + result[1].Title.Should().Be("La guerre des tuques"); + result[2].Title.Should().Be("The Dress"); + result[3].Title.Should().Be("The Club"); + result[4].Title.Should().Be("The Triple Echo"); + } + [Theory] [InlineData("automobile", "transportSynonyms", "Blue Car")] [InlineData("boat", "transportSynonyms", "And the Ship Sails On")] diff --git a/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs index 7e06af49c7b..0afd28ea4ee 100644 --- a/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs @@ -949,10 +949,22 @@ public void Phrase() subject.Phrase("x", "foo", 5), "{ phrase: { query: 'foo', path: 'x', slop: 5 } }"); + AssertRendered( + subject.Phrase("x", "foo", new SearchPhraseOptions { Synonyms = "testSynonyms" }), + "{ phrase: { query: 'foo', path: 'x', synonyms: 'testSynonyms' } }"); + + AssertRendered( + subject.Phrase("x", "foo", 5), + "{ phrase: { query: 'foo', path: 'x', slop: 5 } }"); + var scoreBuilder = new SearchScoreDefinitionBuilder(); AssertRendered( subject.Phrase("x", "foo", score: scoreBuilder.Constant(1)), "{ phrase: { query: 'foo', path: 'x', score: { constant: { value: 1 } } } }"); + + AssertRendered( + subject.Phrase("x", "foo", new SearchPhraseOptions { Score = scoreBuilder.Constant(1), Slop = 5}), + "{ phrase: { query: 'foo', slop: 5, path: 'x', score: { constant: { value: 1 } } } }"); } [Fact] @@ -970,6 +982,10 @@ public void Phrase_typed() subject.Phrase(x => x.Hobbies, "foo"), "{ phrase: { query: 'foo', path: 'hobbies' } }"); + AssertRendered( + subject.Phrase(x => x.FirstName, "foo", new SearchPhraseOptions { Synonyms = "testSynonyms" }), + "{ phrase: { query: 'foo', synonyms: 'testSynonyms', path: 'fn' } }"); + AssertRendered( subject.Phrase( new FieldDefinition[] @@ -1308,6 +1324,10 @@ public void Text() subject.Text(new[] { "x", "y" }, new[] { "foo", "bar" }, "testSynonyms"), "{ text: { query: ['foo', 'bar'], synonyms: 'testSynonyms', path: ['x', 'y'] } }"); + AssertRendered( + subject.Text(new[] { "x", "y" }, new[] { "foo", "bar" }, new SearchTextOptions{ MatchCriteria = MatchCriteria.Any }), + "{ text: { query: ['foo', 'bar'], matchCriteria: 'any', path: ['x', 'y'] } }"); + AssertRendered( subject.Text("x", "foo", new SearchFuzzyOptions()), "{ text: { query: 'foo', path: 'x', fuzzy: {} } }"); @@ -1327,6 +1347,21 @@ public void Text() AssertRendered( subject.Text("x", "foo", "testSynonyms", scoreBuilder.Constant(1)), "{ text: { query: 'foo', synonyms: 'testSynonyms', path: 'x', score: { constant: { value: 1 } } } }"); + + AssertRendered( + subject.Text("x", "foo", new SearchTextOptions {Score = scoreBuilder.Constant(1), MatchCriteria = MatchCriteria.All}), + "{ text: { query: 'foo', matchCriteria: 'all', path: 'x', score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Text_should_throw_with_invalid_options() + { + var subject = CreateSubject(); + + Action act = () => + subject.Text("x", "foo", new SearchTextOptions { MatchCriteria = (MatchCriteria)3 }); + + act.ShouldThrow(); } [Fact] @@ -1337,6 +1372,9 @@ public void Text_typed() AssertRendered( subject.Text(x => x.FirstName, "foo"), "{ text: { query: 'foo', path: 'fn' } }"); + AssertRendered( + subject.Text(x => x.FirstName, "foo", new SearchTextOptions { MatchCriteria = MatchCriteria.All}), + "{ text: { query: 'foo', matchCriteria: 'all', path: 'fn' } }"); AssertRendered( subject.Text("FirstName", "foo"), "{ text: { query: 'foo', path: 'fn' } }");