diff --git a/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs b/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs index 9d9def7ebe4..3e91cb84a28 100644 --- a/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs +++ b/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs @@ -83,16 +83,18 @@ public static T IsGreaterThanOrEqualTo(T value, T comparand, string paramName } /// - /// Ensures that the value of a parameter is greater than or equal to zero. + /// Ensures that the value of a parameter is greater than a comparand. /// + /// Type type of the value. /// The value of the parameter. + /// The comparand. /// The name of the parameter. /// The value of the parameter. - public static int IsGreaterThanOrEqualToZero(int value, string paramName) + public static T IsGreaterThan(T value, T comparand, string paramName) where T : IComparable { - if (value < 0) + if (value.CompareTo(comparand) <= 0) { - var message = string.Format("Value is not greater than or equal to 0: {0}.", value); + var message = $"Value is not greater than {comparand}: {value}."; throw new ArgumentOutOfRangeException(paramName, message); } return value; @@ -104,15 +106,8 @@ public static int IsGreaterThanOrEqualToZero(int value, string paramName) /// The value of the parameter. /// The name of the parameter. /// The value of the parameter. - public static long IsGreaterThanOrEqualToZero(long value, string paramName) - { - if (value < 0) - { - var message = string.Format("Value is not greater than or equal to 0: {0}.", value); - throw new ArgumentOutOfRangeException(paramName, message); - } - return value; - } + public static int IsGreaterThanOrEqualToZero(int value, string paramName) => + IsGreaterThanOrEqualTo(value, 0, paramName); /// /// Ensures that the value of a parameter is greater than or equal to zero. @@ -120,15 +115,17 @@ public static long IsGreaterThanOrEqualToZero(long value, string paramName) /// The value of the parameter. /// The name of the parameter. /// The value of the parameter. - public static TimeSpan IsGreaterThanOrEqualToZero(TimeSpan value, string paramName) - { - if (value < TimeSpan.Zero) - { - var message = string.Format("Value is not greater than or equal to zero: {0}.", TimeSpanParser.ToString(value)); - throw new ArgumentOutOfRangeException(paramName, message); - } - return value; - } + public static long IsGreaterThanOrEqualToZero(long value, string paramName) => + IsGreaterThanOrEqualTo(value, 0, paramName); + + /// + /// Ensures that the value of a parameter is greater than or equal to zero. + /// + /// The value of the parameter. + /// The name of the parameter. + /// The value of the parameter. + public static TimeSpan IsGreaterThanOrEqualToZero(TimeSpan value, string paramName) => + IsGreaterThanOrEqualTo(value, TimeSpan.Zero, paramName); /// /// Ensures that the value of a parameter is greater than zero. @@ -136,15 +133,8 @@ public static TimeSpan IsGreaterThanOrEqualToZero(TimeSpan value, string paramNa /// The value of the parameter. /// The name of the parameter. /// The value of the parameter. - public static int IsGreaterThanZero(int value, string paramName) - { - if (value <= 0) - { - var message = string.Format("Value is not greater than zero: {0}.", value); - throw new ArgumentOutOfRangeException(paramName, message); - } - return value; - } + public static int IsGreaterThanZero(int value, string paramName) => + IsGreaterThan(value, 0, paramName); /// /// Ensures that the value of a parameter is greater than zero. @@ -152,15 +142,8 @@ public static int IsGreaterThanZero(int value, string paramName) /// The value of the parameter. /// The name of the parameter. /// The value of the parameter. - public static long IsGreaterThanZero(long value, string paramName) - { - if (value <= 0) - { - var message = string.Format("Value is not greater than zero: {0}.", value); - throw new ArgumentOutOfRangeException(paramName, message); - } - return value; - } + public static long IsGreaterThanZero(long value, string paramName) => + IsGreaterThan(value, 0, paramName); /// /// Ensures that the value of a parameter is greater than zero. @@ -168,15 +151,17 @@ public static long IsGreaterThanZero(long value, string paramName) /// The value of the parameter. /// The name of the parameter. /// The value of the parameter. - public static TimeSpan IsGreaterThanZero(TimeSpan value, string paramName) - { - if (value <= TimeSpan.Zero) - { - var message = string.Format("Value is not greater than zero: {0}.", value); - throw new ArgumentOutOfRangeException(paramName, message); - } - return value; - } + public static double IsGreaterThanZero(double value, string paramName) => + IsGreaterThan(value, 0, paramName); + + /// + /// Ensures that the value of a parameter is greater than zero. + /// + /// The value of the parameter. + /// The name of the parameter. + /// The value of the parameter. + public static TimeSpan IsGreaterThanZero(TimeSpan value, string paramName) => + IsGreaterThan(value, TimeSpan.Zero, paramName); /// /// Ensures that the value of a parameter is infinite or greater than or equal to zero. @@ -298,6 +283,24 @@ public static T IsNull(T value, string paramName) where T : class return value; } + /// + /// Ensures that the value of a parameter is null or is between a minimum and a maximum value. + /// + /// Type type of the value. + /// The value of the parameter. + /// The minimum value. + /// The maximum value. + /// The name of the parameter. + /// The value of the parameter. + public static T? IsNullOrBetween(T? value, T min, T max, string paramName) where T : struct, IComparable + { + if (value != null) + { + IsBetween(value.Value, min, max, paramName); + } + return value; + } + /// /// Ensures that the value of a parameter is null or greater than or equal to zero. /// diff --git a/src/MongoDB.Driver/AggregateFluent.cs b/src/MongoDB.Driver/AggregateFluent.cs index 6479c61822c..a03a75f7e81 100644 --- a/src/MongoDB.Driver/AggregateFluent.cs +++ b/src/MongoDB.Driver/AggregateFluent.cs @@ -13,13 +13,14 @@ * limitations under the License. */ -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver.Core.Misc; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Search; namespace MongoDB.Driver { @@ -238,6 +239,16 @@ public override IAggregateFluent ReplaceWith(AggregateEx return WithPipeline(_pipeline.ReplaceWith(newRoot)); } + public override IAggregateFluent Search( + SearchDefinition query, + HighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + return WithPipeline(_pipeline.Search(query, highlight, indexName, count, returnStoredSource)); + } + public override IAggregateFluent SetWindowFields( AggregateExpressionDefinition, TWindowFields> output) { diff --git a/src/MongoDB.Driver/AggregateFluentBase.cs b/src/MongoDB.Driver/AggregateFluentBase.cs index 2393da49074..9223eccd87e 100644 --- a/src/MongoDB.Driver/AggregateFluentBase.cs +++ b/src/MongoDB.Driver/AggregateFluentBase.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Driver.Search; namespace MongoDB.Driver { @@ -215,6 +216,17 @@ public virtual IAggregateFluent ReplaceWith(AggregateExp throw new NotImplementedException(); } + /// + public virtual IAggregateFluent Search( + SearchDefinition query, + HighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + throw new NotImplementedException(); + } + /// public virtual IAggregateFluent SetWindowFields( AggregateExpressionDefinition, TWindowFields> output) diff --git a/src/MongoDB.Driver/Builders.cs b/src/MongoDB.Driver/Builders.cs index 2748d915ce5..b339da7644f 100644 --- a/src/MongoDB.Driver/Builders.cs +++ b/src/MongoDB.Driver/Builders.cs @@ -13,6 +13,8 @@ * limitations under the License. */ +using MongoDB.Driver.Search; + namespace MongoDB.Driver { /// @@ -21,50 +23,29 @@ namespace MongoDB.Driver /// The type of the document. public static class Builders { - private static FilterDefinitionBuilder __filter = new FilterDefinitionBuilder(); - private static IndexKeysDefinitionBuilder __index = new IndexKeysDefinitionBuilder(); - private static ProjectionDefinitionBuilder __projection = new ProjectionDefinitionBuilder(); - private static SortDefinitionBuilder __sort = new SortDefinitionBuilder(); - private static UpdateDefinitionBuilder __update = new UpdateDefinitionBuilder(); + /// Gets a . + public static FilterDefinitionBuilder Filter { get; } = new FilterDefinitionBuilder(); + + /// Gets an . + public static IndexKeysDefinitionBuilder IndexKeys { get; } = new IndexKeysDefinitionBuilder(); + + /// Gets a . + public static ProjectionDefinitionBuilder Projection { get; } = new ProjectionDefinitionBuilder(); - /// - /// Gets a . - /// - public static FilterDefinitionBuilder Filter - { - get { return __filter; } - } + /// Gets a . + public static SortDefinitionBuilder Sort { get; } = new SortDefinitionBuilder(); - /// - /// Gets an . - /// - public static IndexKeysDefinitionBuilder IndexKeys - { - get { return __index; } - } + /// Gets an . + public static UpdateDefinitionBuilder Update { get; } = new UpdateDefinitionBuilder(); - /// - /// Gets a . - /// - public static ProjectionDefinitionBuilder Projection - { - get { return __projection; } - } + // Search builders + /// Gets a . + public static PathDefinitionBuilder Path { get; } = new PathDefinitionBuilder(); - /// - /// Gets a . - /// - public static SortDefinitionBuilder Sort - { - get { return __sort; } - } + /// Gets a . + public static ScoreDefinitionBuilder Score { get; } = new ScoreDefinitionBuilder(); - /// - /// Gets an . - /// - public static UpdateDefinitionBuilder Update - { - get { return __update; } - } + /// Gets a . + public static SearchDefinitionBuilder Search { get; } = new SearchDefinitionBuilder(); } } diff --git a/src/MongoDB.Driver/IAggregateFluent.cs b/src/MongoDB.Driver/IAggregateFluent.cs index 703dba4956d..7ba11536235 100644 --- a/src/MongoDB.Driver/IAggregateFluent.cs +++ b/src/MongoDB.Driver/IAggregateFluent.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Driver.Search; namespace MongoDB.Driver { @@ -353,6 +354,25 @@ IAggregateFluent Lookup SetWindowFields( AggregateExpressionDefinition, TWindowFields> output); + /// + /// Appends a $search stage to the pipeline. + /// + /// The search definition. + /// The highlight options. + /// The index name. + /// The count options. + /// + /// Flag that specifies whether to perform a full document lookup on the backend database + /// or return only stored source fields directly from Atlas Search. + /// + /// The fluent aggregate interface. + IAggregateFluent Search( + SearchDefinition query, + HighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false); + /// /// Appends a $setWindowFields to the pipeline. /// diff --git a/src/MongoDB.Driver/Linq/MongoQueryable.cs b/src/MongoDB.Driver/Linq/MongoQueryable.cs index 3e3af9519e4..b1937521c15 100644 --- a/src/MongoDB.Driver/Linq/MongoQueryable.cs +++ b/src/MongoDB.Driver/Linq/MongoQueryable.cs @@ -21,6 +21,7 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Bson.Serialization; +using MongoDB.Driver.Search; namespace MongoDB.Driver.Linq { @@ -923,6 +924,33 @@ public static IMongoQueryable Sample(this IMongoQueryable + /// Appends a $search stage to the LINQ pipeline + /// + /// The type of the elements of . + /// A sequence of values. + /// The search definition. + /// The highlight options. + /// The index name. + /// The count options. + /// + /// Flag that specifies whether to perform a full document lookup on the backend database + /// or return only stored source fields directly from Atlas Search. + /// + /// The fluent aggregate interface. + public static IMongoQueryable Search( + this IMongoQueryable source, + SearchDefinition searchDefinition, + HighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + return AppendStage( + source, + PipelineStageDefinitionBuilder.Search(searchDefinition, highlight, indexName, count, returnStoredSource)); + } + /// /// Projects each element of a sequence into a new form by incorporating the /// element's index. diff --git a/src/MongoDB.Driver/MongoUtils.cs b/src/MongoDB.Driver/MongoUtils.cs index 60cff0bcb9a..43717b4dfc9 100644 --- a/src/MongoDB.Driver/MongoUtils.cs +++ b/src/MongoDB.Driver/MongoUtils.cs @@ -14,8 +14,6 @@ */ using System; -using System.Runtime.InteropServices; -using System.Security; using System.Security.Cryptography; using System.Text; using MongoDB.Bson; @@ -61,5 +59,8 @@ public static string ToCamelCase(string value) { return value.Length == 0 ? "" : value.Substring(0, 1).ToLower() + value.Substring(1); } + + internal static string ToCamelCase(this TEnum @enum) where TEnum : Enum => + ToCamelCase(@enum.ToString()); } } diff --git a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs index 7ece8ab2363..c4ecef2b701 100644 --- a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs @@ -21,6 +21,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Linq; +using MongoDB.Driver.Search; namespace MongoDB.Driver { @@ -1162,6 +1163,35 @@ public static PipelineDefinition ReplaceWith + /// Appends a $search stage to the pipeline. + /// + /// The type of the input documents. + /// The type of the output documents. + /// The pipeline. + /// The search definition. + /// The highlight options. + /// The index name. + /// The count options. + /// + /// Flag that specifies whether to perform a full document lookup on the backend database + /// or return only stored source fields directly from Atlas Search. + /// + /// + /// A new pipeline with an additional stage. + /// + public static PipelineDefinition Search( + this PipelineDefinition pipeline, + SearchDefinition searchDefinition, + HighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + Ensure.IsNotNull(pipeline, nameof(pipeline)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.Search(searchDefinition, highlight, indexName, count, returnStoredSource)); + } + /// /// Create a $setWindowFields stage. /// diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 4ef2c53d80a..78477da9fa6 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -22,9 +22,9 @@ using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Linq; -using MongoDB.Driver.Linq.Linq3Implementation.Misc; using MongoDB.Driver.Linq.Linq3Implementation.Serializers; using MongoDB.Driver.Linq.Linq3Implementation.Translators; +using MongoDB.Driver.Search; namespace MongoDB.Driver { @@ -1309,6 +1309,46 @@ public static PipelineStageDefinition Project( return Project(new ProjectExpressionProjection(projection, translationOptions)); } + /// + /// Creates a $search stage. + /// + /// The type of the input documents. + /// The search definition. + /// The highlight options. + /// The index name. + /// The count options. + /// + /// Flag that specifies whether to perform a full document lookup on the backend database + /// or return only stored source fields directly from Atlas Search. + /// + /// The stage. + public static PipelineStageDefinition Search( + SearchDefinition searchDefinition, + HighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + Ensure.IsNotNull(searchDefinition, nameof(searchDefinition)); + + const string operatorName = "$search"; + var stage = new DelegatedPipelineStageDefinition( + operatorName, + (s, sr, linqProvider) => + { + var renderedSearchDefinition = searchDefinition.Render(s, sr); + renderedSearchDefinition.Add("highlight", () => highlight.Render(s, sr), highlight != null); + renderedSearchDefinition.Add("count", () => count.Render(), count != null); + renderedSearchDefinition.Add("index", indexName, indexName != null); + renderedSearchDefinition.Add("returnStoredSource", returnStoredSource, returnStoredSource); + + var document = new BsonDocument(operatorName, renderedSearchDefinition); + return new RenderedPipelineStageDefinition(operatorName, document, s); + }); + + return stage; + } + /// /// Creates a $replaceRoot stage. /// diff --git a/src/MongoDB.Driver/Search/FuzzyOptions.cs b/src/MongoDB.Driver/Search/FuzzyOptions.cs new file mode 100644 index 00000000000..1ad9456dade --- /dev/null +++ b/src/MongoDB.Driver/Search/FuzzyOptions.cs @@ -0,0 +1,66 @@ +// 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. + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// Options for fuzzy search. + /// + public sealed class FuzzyOptions + { + private int? _maxEdits; + private int? _prefixLength; + private int? _maxExpansions; + + /// + /// Gets or sets the maximum number of single-character edits required to match the + /// specified search term. + /// + public int? MaxEdits + { + get => _maxEdits; + set => _maxEdits = Ensure.IsNullOrBetween(value, 1, 2, nameof(value)); + } + + /// + /// Gets or sets the number of characters at the beginning of each term in the result that + /// must exactly match. + /// + public int? PrefixLength + { + get => _prefixLength; + set => _prefixLength = Ensure.IsNullOrGreaterThanOrEqualToZero(value, nameof(value)); + } + + /// + /// Gets or sets the number of variations to generate and search for. + /// + public int? MaxExpansions + { + get => _maxExpansions; + set => _maxExpansions = Ensure.IsNullOrGreaterThanZero(value, nameof(value)); + } + + internal BsonDocument Render() + => new() + { + { "maxEdits", _maxEdits, _maxEdits != null }, + { "prefixLength", _prefixLength, _prefixLength != null }, + { "maxExpansions", _maxExpansions, _maxExpansions != null } + }; + } +} diff --git a/src/MongoDB.Driver/Search/HighlightOptions.cs b/src/MongoDB.Driver/Search/HighlightOptions.cs new file mode 100644 index 00000000000..1eb3b2e81b2 --- /dev/null +++ b/src/MongoDB.Driver/Search/HighlightOptions.cs @@ -0,0 +1,74 @@ +// 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. + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// Options for highlighting. + /// + /// The type of the document. + public sealed class HighlightOptions + { + private PathDefinition _path; + private int? _maxCharsToExamine; + private int? _maxNumPassages; + + /// + /// Gets or sets the document field to search. + /// + public PathDefinition Path + { + get => _path; + set => _path = Ensure.IsNotNull(value, nameof(value)); + } + + /// + /// Gets or sets the maximum number of characters to examine on a document when performing + /// highlighting for a field. + /// + public int? MaxCharsToExamine + { + get => _maxCharsToExamine; + set => _maxCharsToExamine = Ensure.IsNullOrGreaterThanZero(value, nameof(value)); + } + + /// + /// Gets or sets the number of high-scoring passages to return per document in the + /// highlighting results for each field. + /// + public int? MaxNumPassages + { + get => _maxNumPassages; + set => _maxNumPassages = Ensure.IsNullOrGreaterThanZero(value, nameof(value)); + } + + /// + /// Renders the options to a . + /// + /// The document serializer. + /// The serializer registry. + /// A . + public BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + => new() + { + { "path", _path.Render(documentSerializer, serializerRegistry) }, + { "maxCharsToExamine", _maxCharsToExamine, _maxCharsToExamine != null}, + { "maxNumPassages", _maxNumPassages, _maxNumPassages != null } + }; + } +} diff --git a/src/MongoDB.Driver/Search/HighlightOptionsBuilder.cs b/src/MongoDB.Driver/Search/HighlightOptionsBuilder.cs new file mode 100644 index 00000000000..4005a5a2945 --- /dev/null +++ b/src/MongoDB.Driver/Search/HighlightOptionsBuilder.cs @@ -0,0 +1,70 @@ +// 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. + +using System; +using System.Linq.Expressions; + +namespace MongoDB.Driver.Search +{ + /// + /// A build for highlighting options. + /// + /// The type of the document. + public sealed class HighlightOptionsBuilder + { + /// + /// Creates highlighting options. + /// + /// The document field to search. + /// + /// The maximum number of characters to examine on a document when performing highlighting + /// for a field. + /// + /// + /// The number of high-scoring passages to return per document in the highlighting results + /// for each field. + /// + /// Highlighting options. + public HighlightOptions Options( + PathDefinition path, + int? maxCharsToExamine = null, + int? maxNumPassages = null) + => new() + { + Path = path, + MaxCharsToExamine = maxCharsToExamine, + MaxNumPassages = maxNumPassages + }; + + /// + /// Creates highlighting options. + /// + /// The type of the field. + /// The document field to search. + /// + /// The maximum number of characters to examine on a document when performing highlighting + /// for a field. + /// + /// + /// The number of high-scoring passages to return per document in the highlighting results + /// for each field. + /// + /// Highlighting options. + public HighlightOptions Options( + Expression> path, + int? maxCharsToExamine = null, + int? maxNumPassages = null) => + Options(new ExpressionFieldDefinition(path), maxCharsToExamine, maxNumPassages); + } +} diff --git a/src/MongoDB.Driver/Search/PathDefinition.cs b/src/MongoDB.Driver/Search/PathDefinition.cs new file mode 100644 index 00000000000..80d138de38c --- /dev/null +++ b/src/MongoDB.Driver/Search/PathDefinition.cs @@ -0,0 +1,90 @@ +// 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. + +using System.Collections.Generic; +using System.Linq; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; + +namespace MongoDB.Driver.Search +{ + /// + /// Base class for search paths. + /// + /// The type of the document. + public abstract class PathDefinition + { + /// + /// Renders the path to a . + /// + /// The document serializer. + /// The serializer registry. + /// A . + public abstract BsonValue Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry); + + /// + /// Performs an implicit conversion from to + /// . + /// + /// The field. + /// + /// The result of the conversion. + /// + public static implicit operator PathDefinition(FieldDefinition field) => + new SinglePathDefinition(field); + + /// + /// Performs an implicit conversion from a field name to . + /// + /// The field name. + /// + /// The result of the conversion. + /// + public static implicit operator PathDefinition(string fieldName) => + new SinglePathDefinition(new StringFieldDefinition(fieldName)); + + /// + /// Performs an implicit conversion from an array of to + /// . + /// + /// The array of fields. + /// + /// The result of the conversion. + /// + public static implicit operator PathDefinition(FieldDefinition[] fields) => + new MultiPathDefinition(fields); + + /// + /// Performs an implicit conversion from a list of to + /// . + /// + /// The list of fields. + /// + /// The result of the conversion. + /// + public static implicit operator PathDefinition(List> fields) => + new MultiPathDefinition(fields); + + /// + /// Performs an implicit conversion from an array of field names to + /// . + /// + /// The array of field names. + /// + /// The result of the conversion. + /// + public static implicit operator PathDefinition(string[] fieldNames) => + new MultiPathDefinition(fieldNames.Select(fieldName => new StringFieldDefinition(fieldName))); + } +} diff --git a/src/MongoDB.Driver/Search/PathDefinitionBuilder.cs b/src/MongoDB.Driver/Search/PathDefinitionBuilder.cs new file mode 100644 index 00000000000..437d0f8c6e9 --- /dev/null +++ b/src/MongoDB.Driver/Search/PathDefinitionBuilder.cs @@ -0,0 +1,165 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// A builder for a search path. + /// + /// The type of the document. + public sealed class PathDefinitionBuilder + { + /// + /// Creates a search path for a single field. + /// + /// The field definition. + /// A single-field search path. + public PathDefinition Single(FieldDefinition field) => + new SinglePathDefinition(field); + + /// + /// Creates a search path for a single field. + /// + /// The type of the field. + /// The field definition. + /// A single-field search path. + public PathDefinition Single(Expression> field) => + Single(new ExpressionFieldDefinition(field)); + + /// + /// Creates a search path for multiple fields. + /// + /// The collection of field definitions. + /// A multi-field search path. + public PathDefinition Multi(IEnumerable> fields) => + new MultiPathDefinition(fields); + + /// + /// Creates a search path for multiple fields. + /// + /// The array of field definitions. + /// A multi-field search path. + public PathDefinition Multi(params FieldDefinition[] fields) => + Multi((IEnumerable>)fields); + + /// + /// Creates a search path for multiple fields. + /// + /// The type of the fields. + /// The array of field definitions. + /// A multi-field search path. + public PathDefinition Multi(params Expression>[] fields) => + Multi(fields.Select(x => new ExpressionFieldDefinition(x))); + + /// + /// Creates a search path that searches using the specified analyzer. + /// + /// The field definition + /// The name of the analyzer. + /// An analyzer search path. + public PathDefinition Analyzer(FieldDefinition field, string analyzerName) => + new AnalyzerPathDefinition(field, analyzerName); + + /// + /// Creates a search path that searches using the specified analyzer. + /// + /// The type of the field. + /// The field definition + /// The name of the analyzer. + /// An analyzer search path. + public PathDefinition Analyzer(Expression> field, string analyzerName) => + Analyzer(new ExpressionFieldDefinition(field), analyzerName); + + /// + /// Creates a search path that uses special characters in the field name + /// that can match any character. + /// + /// + /// The wildcard string that the field name must match. + /// + /// A wildcard search path. + public PathDefinition Wildcard(string query) => + new WildcardPathDefinition(query); + } + + internal sealed class SinglePathDefinition : PathDefinition + { + private readonly FieldDefinition _field; + + public SinglePathDefinition(FieldDefinition field) + { + _field = Ensure.IsNotNull(field, nameof(field)); + } + + public override BsonValue Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + { + var renderedField = _field.Render(documentSerializer, serializerRegistry); + return new BsonString(renderedField.FieldName); + } + } + + internal sealed class MultiPathDefinition : PathDefinition + { + private readonly IEnumerable> _fields; + + public MultiPathDefinition(IEnumerable> fields) + { + _fields = Ensure.IsNotNull(fields, nameof(fields)); + } + + public override BsonValue Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new BsonArray(_fields.Select(field => field.Render(documentSerializer, serializerRegistry).FieldName)); + } + + internal sealed class AnalyzerPathDefinition : PathDefinition + { + private readonly FieldDefinition _field; + private readonly string _analyzerName; + + public AnalyzerPathDefinition(FieldDefinition field, string analyzerName) + { + _field = Ensure.IsNotNull(field, nameof(field)); + _analyzerName = Ensure.IsNotNull(analyzerName, nameof(analyzerName)); + } + + public override BsonValue Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => new BsonDocument() + { + { "value", _field.Render(documentSerializer, serializerRegistry).FieldName }, + { "multi", _analyzerName } + }; + } + + internal sealed class WildcardPathDefinition : PathDefinition + { + private readonly string _query; + + public WildcardPathDefinition(string query) + { + _query = Ensure.IsNotNull(query, nameof(query)); + } + + public override BsonValue Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => new BsonDocument() + { + { "wildcard", _query } + }; + } +} diff --git a/src/MongoDB.Driver/Search/QueryDefinition.cs b/src/MongoDB.Driver/Search/QueryDefinition.cs new file mode 100644 index 00000000000..d8a92bbd011 --- /dev/null +++ b/src/MongoDB.Driver/Search/QueryDefinition.cs @@ -0,0 +1,102 @@ +// 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. + +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// Base class for search queries. + /// + public abstract class QueryDefinition + { + /// + /// Renders the query to a . + /// + /// A . + public abstract BsonValue Render(); + + /// + /// Performs an implicit conversion from a string to . + /// + /// The string. + /// + /// The result of the conversion. + /// + public static implicit operator QueryDefinition(string query) => + new SingleQueryDefinition(query); + + /// + /// Performs an implicit conversion from an array of strings to . + /// + /// The array of strings. + /// + /// The result of the conversion. + /// + public static implicit operator QueryDefinition(string[] queries) => + new MultiQueryDefinition(queries); + + /// + /// Performs an implicit conversion from a list of strings to . + /// + /// The list of strings. + /// + /// The result of the conversion. + /// + public static implicit operator QueryDefinition(List queries) => + new MultiQueryDefinition(queries); + } + + /// + /// A query definition for a single string. + /// + public sealed class SingleQueryDefinition : QueryDefinition + { + private readonly string _query; + + /// + /// Initializes a new instance of the class. + /// + /// The query string. + public SingleQueryDefinition(string query) + { + _query = Ensure.IsNotNull(query, nameof(query)); + } + + /// + public override BsonValue Render() => new BsonString(_query); + } + + /// + /// A query definition for multiple strings. + /// + public sealed class MultiQueryDefinition : QueryDefinition + { + private readonly IEnumerable _queries; + + /// + /// Initializes a new instance of the class. + /// + /// The query strings. + public MultiQueryDefinition(IEnumerable queries) + { + _queries = Ensure.IsNotNull(queries, nameof(queries)); + } + + /// + public override BsonValue Render() => new BsonArray(_queries); + } +} diff --git a/src/MongoDB.Driver/Search/ScoreDefinition.cs b/src/MongoDB.Driver/Search/ScoreDefinition.cs new file mode 100644 index 00000000000..78bc649b0bf --- /dev/null +++ b/src/MongoDB.Driver/Search/ScoreDefinition.cs @@ -0,0 +1,34 @@ +// 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. + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; + +namespace MongoDB.Driver.Search +{ + /// + /// Base class for search score modifiers. + /// + /// The type of the document. + public abstract class ScoreDefinition + { + /// + /// Renders the score modifier to a . + /// + /// The document serializer. + /// The serializer registry. + /// A . + public abstract BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry); + } +} diff --git a/src/MongoDB.Driver/Search/ScoreDefinitionBuilder.cs b/src/MongoDB.Driver/Search/ScoreDefinitionBuilder.cs new file mode 100644 index 00000000000..c4f3190533b --- /dev/null +++ b/src/MongoDB.Driver/Search/ScoreDefinitionBuilder.cs @@ -0,0 +1,134 @@ +// 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. + +using System; +using System.Linq.Expressions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// A builder for a score modifier. + /// + /// The type of the document. + public sealed class ScoreDefinitionBuilder + { + /// + /// Creates a score modifier that multiplies a result's base score by a given number. + /// + /// The number to multiply the default base score by. + /// + /// A boost score modifier. + /// + public ScoreDefinition Boost(double value) => new BoostValueScoreDefinition(value); + + /// + /// Creates a score modifier that multiples a result's base score by the value of a numeric + /// field in the documents. + /// + /// + /// The path to the numeric field whose value to multiply the default base score by. + /// + /// + /// The numeric value to substitute if the numeric field is not found in the documents. + /// + /// + /// A boost score modifier. + /// + public ScoreDefinition Boost(PathDefinition path, double undefined = 0) => + new BoostPathScoreDefinition(path, undefined); + + /// + /// Creates a score modifier that multiplies a result's base score by the value of a numeric + /// field in the documents. + /// + /// + /// The path to the numeric field whose value to multiply the default base score by. + /// + /// + /// The numeric value to substitute if the numeric field is not found in the documents. + /// + /// + /// A boost score modifier. + /// + public ScoreDefinition Boost(Expression> path, double undefined = 0) => + Boost(new ExpressionFieldDefinition(path), undefined); + + /// + /// Creates a score modifier that replaces the base score with a given number. + /// + /// The number to replace the base score with. + /// + /// A constant score modifier. + /// + public ScoreDefinition Constant(double value) => + new ConstantScoreDefinition(value); + } + + internal class BoostValueScoreDefinition : ScoreDefinition + { + private readonly double _value; + + public BoostValueScoreDefinition(double value) + { + _value = Ensure.IsGreaterThanZero(value, nameof(value)); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + => new() + { + { "boost", new BsonDocument("value", _value) } + }; + } + + internal class BoostPathScoreDefinition : ScoreDefinition + { + private readonly PathDefinition _path; + private readonly double _undefined; + + public BoostPathScoreDefinition(PathDefinition path, double undefined) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _undefined = undefined; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + { + var document = new BsonDocument() + { + { "path", _path.Render(documentSerializer, serializerRegistry) }, + { "undefined", _undefined, _undefined != 0 } + }; + + return new("boost", document); + } + } + + internal sealed class ConstantScoreDefinition : ScoreDefinition + { + private readonly double _value; + + public ConstantScoreDefinition(double value) + { + _value = Ensure.IsGreaterThanZero(value, nameof(value)); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => new() + { + { "constant", new BsonDocument("value", _value) } + }; + } +} diff --git a/src/MongoDB.Driver/Search/SearchCountOptions.cs b/src/MongoDB.Driver/Search/SearchCountOptions.cs new file mode 100644 index 00000000000..7f26fef5c8c --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchCountOptions.cs @@ -0,0 +1,54 @@ +// 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. + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// Options for counting the search results. + /// + public class SearchCountOptions + { + private SearchCountType _type = SearchCountType.LowerBound; + private int? _threshold; + + /// + /// Gets or sets the type of count of the documents in the result set. + /// + public SearchCountType Type + { + get => _type; + set => _type = value; + } + + /// + /// Gets or sets the number of documents to include in the exact count if + /// is . + /// + public int? Threshold + { + get => _threshold; + set => _threshold = Ensure.IsNullOrGreaterThanZero(value, nameof(value)); + } + + internal BsonDocument Render() + => new() + { + { "type", _type.ToCamelCase(), _type != SearchCountType.LowerBound }, + { "threshold", _threshold, _threshold != null } + }; + } +} diff --git a/src/MongoDB.Driver/Search/SearchCountType.cs b/src/MongoDB.Driver/Search/SearchCountType.cs new file mode 100644 index 00000000000..4dfac693a98 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchCountType.cs @@ -0,0 +1,32 @@ +// Copyright 2021-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 +{ + /// + /// The type of count of the documents in a search result set. + /// + public enum SearchCountType + { + /// + /// A lower bound count of the number of documents that match the query. + /// + LowerBound, + + /// + /// An exact count of the number of documents that match the query. + /// + Total + } +} diff --git a/src/MongoDB.Driver/Search/SearchDefinition.cs b/src/MongoDB.Driver/Search/SearchDefinition.cs new file mode 100644 index 00000000000..8b7ec459bf2 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchDefinition.cs @@ -0,0 +1,105 @@ +// 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. + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// Base class for search definitions. + /// + /// The type of the document. + public abstract class SearchDefinition + { + /// + /// Renders the search definition to a . + /// + /// The document serializer. + /// The serializer registry. + /// A . + public abstract BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry); + + /// + /// Performs an implicit conversion from a BSON document to a . + /// + /// The BSON document specifying the search definition. + /// + /// The result of the conversion. + /// + public static implicit operator SearchDefinition(BsonDocument document) => + document != null ? new BsonDocumentSearchDefinition(document) : null; + + /// + /// Performs an implicit conversion from a string to a . + /// + /// The string specifying the search definition in JSON. + /// + /// The result of the conversion. + /// + public static implicit operator SearchDefinition(string json) => + json != null ? new JsonSearchDefinition(json) : null; + } + + /// + /// A search definition based on a BSON document. + /// + /// The type of the document. + public sealed class BsonDocumentSearchDefinition : SearchDefinition + { + /// + /// Initializes a new instance of the class. + /// + /// The BSON document specifying the search definition. + public BsonDocumentSearchDefinition(BsonDocument document) + { + Document = Ensure.IsNotNull(document, nameof(document)); + } + + /// + /// Gets the BSON document. + /// + public BsonDocument Document { get; private set; } + + /// + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + Document; + } + + /// + /// A search definition based on a JSON string. + /// + /// The type of the document. + public sealed class JsonSearchDefinition : SearchDefinition + { + /// + /// Initializes a new instance of the class. + /// + /// The JSON string specifying the search definition. + public JsonSearchDefinition(string json) + { + Json = Ensure.IsNotNullOrEmpty(json, nameof(json)); + } + + /// + /// Gets the JSON string. + /// + public string Json { get; private set; } + + /// + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + BsonDocument.Parse(Json); + } +} diff --git a/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs b/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs new file mode 100644 index 00000000000..14598e090c4 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs @@ -0,0 +1,97 @@ +// 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. + +using System; +using System.Linq.Expressions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// A builder for a search definition. + /// + /// The type of the document. + public sealed class SearchDefinitionBuilder + { + /// + /// Creates a search definition that performs full-text search using the analyzer specified + /// in the index configuration. + /// + /// The string or strings to search for. + /// The indexed field or fields to search. + /// The options for fuzzy search. + /// The score modifier. + /// A text search definition. + public SearchDefinition Text( + QueryDefinition query, + PathDefinition path, + FuzzyOptions fuzzy = null, + ScoreDefinition score = null) => + new TextSearchDefinition(query, path, fuzzy, score); + + /// + /// Creates a search definition that performs full-text search using the analyzer specified + /// in the index configuration. + /// + /// The type of the field. + /// The string or strings to search for. + /// The indexed field or field to search. + /// The options for fuzzy search. + /// The score modifier. + /// A text search definition. + public SearchDefinition Text( + QueryDefinition query, + Expression> path, + FuzzyOptions fuzzy = null, + ScoreDefinition score = null) => + Text(query, new ExpressionFieldDefinition(path), fuzzy, score); + } + + internal sealed class TextSearchDefinition : SearchDefinition + { + private readonly QueryDefinition _query; + private readonly PathDefinition _path; + private readonly FuzzyOptions _fuzzy; + private readonly ScoreDefinition _score; + + public TextSearchDefinition( + QueryDefinition query, + PathDefinition path, + FuzzyOptions fuzzy, + ScoreDefinition score) + { + _query = Ensure.IsNotNull(query, nameof(query)); + _path = Ensure.IsNotNull(path, nameof(path)); + _fuzzy = fuzzy; + _score = score; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + => new() + { + { + "text", + new BsonDocument() + { + { "query", _query.Render() }, + { "path", _path.Render(documentSerializer, serializerRegistry) }, + { "fuzzy", () => _fuzzy.Render(), _fuzzy != null }, + { "score", () => _score.Render(documentSerializer, serializerRegistry), _score != null } + } + } + }; + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs new file mode 100644 index 00000000000..a27cd4d125a --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs @@ -0,0 +1,155 @@ +// Copyright 2021-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. + +using System; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.GeoJsonObjectModel; +using MongoDB.Driver.Search; +using Xunit; + +namespace MongoDB.Driver.Tests.Search +{ + public class SearchDefinitionBuilderTests + { + [Fact] + public void Text() + { + var subject = CreateSubject(); + + AssertRendered(subject.Text("foo", "x"), + "{ text: { query: 'foo', path: 'x' } }"); + + AssertRendered( + subject.Text("foo", new[] { "x", "y" }), + "{ text: { query: 'foo', path: ['x', 'y'] } }"); + AssertRendered( + subject.Text(new[] { "foo", "bar" }, "x"), + "{ text: { query: ['foo', 'bar'], path: 'x' } }"); + AssertRendered( + subject.Text(new[] { "foo", "bar" }, new[] { "x", "y" }), + "{ text: { query: ['foo', 'bar'], path: ['x', 'y'] } }"); + + AssertRendered( + subject.Text("foo", "x", new FuzzyOptions()), + "{ text: { query: 'foo', path: 'x', fuzzy: {} } }"); + AssertRendered( + subject.Text("foo", "x", new FuzzyOptions() + { + MaxEdits = 1, + PrefixLength = 5, + MaxExpansions = 25 + }), + "{ text: { query: 'foo', path: 'x', fuzzy: { maxEdits: 1, prefixLength: 5, maxExpansions: 25 } } }"); + + var scoreBuilder = new ScoreDefinitionBuilder(); + AssertRendered( + subject.Text("foo", "x", score: scoreBuilder.Constant(1)), + "{ text: { query: 'foo', path: 'x', score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Text_Typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Text("foo", x => x.FirstName), + "{ text: { query: 'foo', path: 'fn' } }"); + AssertRendered( + subject.Text("foo", "FirstName"), + "{ text: { query: 'foo', path: 'fn' } }"); + + AssertRendered( + subject.Text( + "foo", + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }), + "{ text: { query: 'foo', path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Text("foo", new[] { "FirstName", "LastName" }), + "{ text: { query: 'foo', path: ['fn', 'ln'] } }"); + + AssertRendered( + subject.Text(new[] { "foo", "bar" }, x => x.FirstName), + "{ text: { query: ['foo', 'bar'], path: 'fn' } }"); + AssertRendered( + subject.Text(new[] { "foo", "bar" }, "FirstName"), + "{ text: { query: ['foo', 'bar'], path: 'fn' } }"); + + AssertRendered( + subject.Text( + new[] { "foo", "bar" }, + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }), + "{ text: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Text(new[] { "foo", "bar" }, new[] { "FirstName", "LastName" }), + "{ text: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + } + + private void AssertRendered(SearchDefinition query, string expected) + { + AssertRendered(query, BsonDocument.Parse(expected)); + } + + private void AssertRendered(SearchDefinition query, BsonDocument expected) + { + var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var renderedQuery = query.Render(documentSerializer, BsonSerializer.SerializerRegistry); + + renderedQuery.Should().Be(expected); + } + + private SearchDefinitionBuilder CreateSubject() + { + return new SearchDefinitionBuilder(); + } + + private class SimplePerson + { + [BsonElement("fn")] + public string FirstName { get; set; } + + [BsonElement("ln")] + public string LastName { get; set; } + } + + private class Person : SimplePerson + { + [BsonId] + public ObjectId Id { get; set; } + + [BsonElement("age")] + public int Age { get; set; } + + [BsonElement("ret")] + public bool Retired { get; set; } + + [BsonElement("dob")] + public DateTime Birthday { get; set; } + + [BsonElement("location")] + public GeoJsonPoint Location { get; set; } + } + } +}