diff --git a/build.cake b/build.cake index d18483f8d70..eb277fe4fe0 100644 --- a/build.cake +++ b/build.cake @@ -240,6 +240,13 @@ Task("TestAtlasDataLake") action: (BuildConfig buildConfig, Path testProject) => RunTests(buildConfig, testProject, filter: "Category=\"AtlasDataLake\"")); +Task("TestAtlasSearch") + .IsDependentOn("Build") + .DoesForEach( + items: GetFiles("./**/MongoDB.Driver.Tests.csproj"), + action: (BuildConfig buildConfig, Path testProject) => + RunTests(buildConfig, testProject, filter: "Category=\"AtlasSearch\"")); + Task("TestOcsp") .IsDependentOn("Build") .DoesForEach( diff --git a/evergreen/evergreen.yml b/evergreen/evergreen.yml index 2a48d6909e5..85223c09a38 100644 --- a/evergreen/evergreen.yml +++ b/evergreen/evergreen.yml @@ -684,6 +684,15 @@ functions: ${PREPARE_SHELL} evergreen/run-atlas-data-lake-test.sh + run-atlas-search-test: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + ATLAS_SEARCH="${ATLAS_SEARCH}" evergreen/run-atlas-search-test.sh + run-ocsp-test: - command: shell.exec type: test @@ -1202,6 +1211,10 @@ tasks: - func: bootstrap-mongohoused - func: run-atlas-data-lake-test + - name: atlas-search-test + commands: + - func: run-atlas-search-test + - name: test-serverless-net472 exec_timeout_secs: 2700 # 45 minutes: 15 for setup + 30 for tests commands: @@ -2068,6 +2081,13 @@ buildvariants: tasks: - name: atlas-data-lake-test +- name: atlas-search-test + display_name: "Atlas Search Tests" + run_on: + - windows-64-vs2017-test + tasks: + - name: atlas-search-test + - name: gssapi-auth-tests-windows run_on: - windows-64-vs2017-test diff --git a/evergreen/run-atlas-search-test.sh b/evergreen/run-atlas-search-test.sh new file mode 100644 index 00000000000..88b9b151a85 --- /dev/null +++ b/evergreen/run-atlas-search-test.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -o xtrace +set -o errexit # Exit the script with error if any of the commands fail + +# Environment variables produced as output +# ATLAS_SEARCH_TESTS_ENABLED Enable atlas search tests. + +############################################ +# Main Program # +############################################ + +echo "Running Atlas Search driver tests" + +export ATLAS_SEARCH_TESTS_ENABLED=true + +powershell.exe .\\build.ps1 --target=TestAtlasSearch \ No newline at end of file diff --git a/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs b/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs index 9d9def7ebe4..10560a1cbd7 100644 --- a/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs +++ b/src/MongoDB.Driver.Core/Core/Misc/Ensure.cs @@ -27,6 +27,22 @@ namespace MongoDB.Driver.Core.Misc [DebuggerStepThrough] public static class Ensure { + /// + /// Ensures that the value of a parameter is not null. + /// + /// Type type of the value. + /// The value of the parameter. + /// The name of the parameter. + /// The value of the parameter. + public static Nullable HasValue(Nullable value, string paramName) where T : struct + { + if (!value.HasValue) + { + throw new ArgumentException("The Nullable parameter must have a value.", paramName); + } + return value; + } + /// /// Ensures that the value of a parameter is between a minimum and a maximum value. /// @@ -65,34 +81,36 @@ public static T IsEqualTo(T value, T comparand, string paramName) } /// - /// Ensures that the value of a parameter is greater than or equal to a comparand. + /// 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 T IsGreaterThanOrEqualTo(T value, T comparand, string paramName) where T : IComparable + public static T IsGreaterThan(T value, T comparand, string paramName) where T : IComparable { - if (value.CompareTo(comparand) < 0) + if (value.CompareTo(comparand) <= 0) { - var message = string.Format("Value is not greater than or equal to {1}: {0}.", value, comparand); + var message = $"Value is not greater than {comparand}: {value}."; throw new ArgumentOutOfRangeException(paramName, message); } return value; } /// - /// Ensures that the value of a parameter is greater than or equal to zero. + /// Ensures that the value of a parameter is greater than or equal to 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 IsGreaterThanOrEqualTo(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 = string.Format("Value is not greater than or equal to {1}: {0}.", value, comparand); throw new ArgumentOutOfRangeException(paramName, message); } return value; @@ -104,15 +122,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 +131,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 +149,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 +158,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 +167,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. @@ -247,22 +248,6 @@ public static IEnumerable IsNotNullAndDoesNotContainAnyNulls(IEnumerable - /// Ensures that the value of a parameter is not null. - /// - /// Type type of the value. - /// The value of the parameter. - /// The name of the parameter. - /// The value of the parameter. - public static Nullable HasValue(Nullable value, string paramName) where T : struct - { - if (!value.HasValue) - { - throw new ArgumentException("The Nullable parameter must have a value.", paramName); - } - return value; - } - /// /// Ensures that the value of a parameter is not null or empty. /// @@ -298,6 +283,24 @@ public static string IsNotNullOrEmpty(string value, string paramName) 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 38fd1dca4f8..f4732d3fc69 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,24 @@ public override IAggregateFluent ReplaceWith(AggregateEx return WithPipeline(_pipeline.ReplaceWith(newRoot)); } + public override IAggregateFluent Search( + SearchDefinition searchDefinition, + SearchHighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + return WithPipeline(_pipeline.Search(searchDefinition, highlight, indexName, count, returnStoredSource)); + } + + public override IAggregateFluent SearchMeta( + SearchDefinition searchDefinition, + string indexName = null, + SearchCountOptions count = null) + { + return WithPipeline(_pipeline.SearchMeta(searchDefinition, indexName, count)); + } + public override IAggregateFluent SetWindowFields( AggregateExpressionDefinition, TWindowFields> output) { diff --git a/src/MongoDB.Driver/AggregateFluentBase.cs b/src/MongoDB.Driver/AggregateFluentBase.cs index ec7fdc18023..8239750962a 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,26 @@ public virtual IAggregateFluent ReplaceWith(AggregateExp throw new NotImplementedException(); } + /// + public virtual IAggregateFluent Search( + SearchDefinition searchDefinition, + SearchHighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + throw new NotImplementedException(); + } + + /// + public virtual IAggregateFluent SearchMeta( + SearchDefinition searchDefinition, + string indexName = null, + SearchCountOptions count = null) + { + 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..cace9a352f0 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,38 @@ 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 SortDefinitionBuilder Sort { get; } = new SortDefinitionBuilder(); + + /// Gets an . + public static UpdateDefinitionBuilder Update { get; } = new UpdateDefinitionBuilder(); + + // Search builders + /// Gets a . + public static SearchFacetBuilder SearchFacet { get; } = new SearchFacetBuilder(); - /// - /// Gets a . - /// - public static FilterDefinitionBuilder Filter - { - get { return __filter; } - } + /// Gets a . + public static SearchPathDefinitionBuilder SearchPath { get; } = new SearchPathDefinitionBuilder(); - /// - /// Gets an . - /// - public static IndexKeysDefinitionBuilder IndexKeys - { - get { return __index; } - } + /// Gets a . + public static SearchScoreDefinitionBuilder SearchScore { get; } = new SearchScoreDefinitionBuilder(); - /// - /// Gets a . - /// - public static ProjectionDefinitionBuilder Projection - { - get { return __projection; } - } + /// Gets a . + public static SearchScoreFunctionBuilder SearchScoreFunction { get; } = new SearchScoreFunctionBuilder(); - /// - /// Gets a . - /// - public static SortDefinitionBuilder Sort - { - get { return __sort; } - } + /// Gets a . + public static SearchDefinitionBuilder Search { get; } = new SearchDefinitionBuilder(); - /// - /// Gets an . - /// - public static UpdateDefinitionBuilder Update - { - get { return __update; } - } + /// Gets a . + public static SearchSpanDefinitionBuilder SearchSpan { get; } = new SearchSpanDefinitionBuilder(); } } diff --git a/src/MongoDB.Driver/IAggregateFluent.cs b/src/MongoDB.Driver/IAggregateFluent.cs index ff7a85714d0..cca33c9e1c9 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,37 @@ public interface IAggregateFluent : IAsyncCursorSource IAggregateFluent 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 searchDefinition, + SearchHighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false); + + /// + /// Appends a $searchMeta stage to the pipeline. + /// + /// The search definition. + /// The index name. + /// The count options. + /// The fluent aggregate interface. + IAggregateFluent SearchMeta( + SearchDefinition searchDefinition, + string indexName = null, + SearchCountOptions count = null); + /// /// Appends a $setWindowFields to the pipeline. /// diff --git a/src/MongoDB.Driver/Linq/MongoQueryable.cs b/src/MongoDB.Driver/Linq/MongoQueryable.cs index 7f342daad74..f2dcdab9751 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; using MongoDB.Driver.Core.Misc; namespace MongoDB.Driver.Linq @@ -1110,6 +1111,53 @@ 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 queryable with a new stage appended. + public static IMongoQueryable Search( + this IMongoQueryable source, + SearchDefinition searchDefinition, + SearchHighlightOptions highlight = null, + string indexName = null, + SearchCountOptions count = null, + bool returnStoredSource = false) + { + return AppendStage( + source, + PipelineStageDefinitionBuilder.Search(searchDefinition, highlight, indexName, count, returnStoredSource)); + } + + /// + /// Appends a $searchMeta stage to the LINQ pipeline. + /// + /// The type of the elements of . + /// A sequence of values. + /// The search definition. + /// The index name. + /// The count options. + /// The queryable with a new stage appended. + public static IMongoQueryable SearchMeta( + this IMongoQueryable source, + SearchDefinition searchDefinition, + string indexName = null, + SearchCountOptions count = null) + { + return AppendStage( + source, + PipelineStageDefinitionBuilder.SearchMeta(searchDefinition, indexName, count)); + } + /// /// 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 360b5cad918..cd31f0014d2 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,57 @@ public static class PipelineDefinitionBuilder return pipeline.AppendStage(PipelineStageDefinitionBuilder.ReplaceWith(newRoot, translationOptions)); } + /// + /// 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, + SearchHighlightOptions 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)); + } + + /// + /// Appends a $searchMeta stage to the pipeline. + /// + /// The type of the input documents. + /// The type of the output documents. + /// The pipeline. + /// The search definition. + /// The index name. + /// The count options. + /// + /// A new pipeline with an additional stage. + /// + public static PipelineDefinition SearchMeta( + this PipelineDefinition pipeline, + SearchDefinition query, + string indexName = null, + SearchCountOptions count = null) + { + Ensure.IsNotNull(pipeline, nameof(pipeline)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.SearchMeta(query, indexName, count)); + } + /// /// Create a $setWindowFields stage. /// diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 940818d693a..70d4c24bd13 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,80 @@ public static class PipelineStageDefinitionBuilder 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, + SearchHighlightOptions 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 $searchMeta stage. + /// + /// The type of the input documents. + /// The search definition. + /// The index name. + /// The count options. + /// The stage. + public static PipelineStageDefinition SearchMeta( + SearchDefinition searchDefinition, + string indexName = null, + SearchCountOptions count = null) + { + Ensure.IsNotNull(searchDefinition, nameof(searchDefinition)); + + const string operatorName = "$searchMeta"; + var stage = new DelegatedPipelineStageDefinition( + operatorName, + (s, sr, linqProvider) => + { + var renderedSearchDefinition = searchDefinition.Render(s, sr); + renderedSearchDefinition.Add("count", () => count.Render(), count != null); + renderedSearchDefinition.Add("index", indexName, indexName != null); + + var document = new BsonDocument(operatorName, renderedSearchDefinition); + return new RenderedPipelineStageDefinition( + operatorName, + document, + sr.GetSerializer()); + }); + + return stage; + } + /// /// Creates a $replaceRoot stage. /// diff --git a/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs b/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs index 42fe10e0c9a..528116cb272 100644 --- a/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs +++ b/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs @@ -156,6 +156,40 @@ public static ProjectionDefinition Meta(this ProjectionDef return builder.Combine(projection, builder.Meta(field, metaFieldName)); } + /// + /// Combines an existing projection with a search highlights projection. + /// + /// The type of the document. + /// The projection. + /// The field. + /// + /// A combined projection. + /// + public static ProjectionDefinition MetaSearchHighlights( + this ProjectionDefinition projection, + string field) + { + var builder = Builders.Projection; + return builder.Combine(projection, builder.MetaSearchHighlights(field)); + } + + /// + /// Combines an existing projection with a search score projection. + /// + /// The type of the document. + /// The projection. + /// The field. + /// + /// A combined projection. + /// + public static ProjectionDefinition MetaSearchScore( + this ProjectionDefinition projection, + string field) + { + var builder = Builders.Projection; + return builder.Combine(projection, builder.MetaSearchScore(field)); + } + /// /// Combines an existing projection with a text score projection. /// @@ -171,6 +205,40 @@ public static ProjectionDefinition MetaTextScore(this Proj return builder.Combine(projection, builder.MetaTextScore(field)); } + /// + /// Combines an existing projection with a search metadata projection. + /// + /// The type of the document. + /// The projection. + /// The field. + /// + /// A combined projection. + /// + public static ProjectionDefinition SearchMeta( + this ProjectionDefinition projection, + FieldDefinition field) + { + var builder = Builders.Projection; + return builder.Combine(projection, builder.SearchMeta(field)); + } + + /// + /// Combines an existing projection with a search metadata projection. + /// + /// The type of the document. + /// The projection. + /// The field. + /// + /// A combined projection. + /// + public static ProjectionDefinition SearchMeta( + this ProjectionDefinition projection, + Expression> field) + { + var builder = Builders.Projection; + return builder.Combine(projection, builder.SearchMeta(field)); + } + /// /// Combines an existing projection with an array slice projection. /// @@ -363,6 +431,30 @@ public ProjectionDefinition Meta(string field, string metaFieldName) return new SingleFieldProjectionDefinition(field, new BsonDocument("$meta", metaFieldName)); } + /// + /// Creates a search highlights projection. + /// + /// The field. + /// + /// A search highlights projection. + /// + public ProjectionDefinition MetaSearchHighlights(string field) + { + return Meta(field, "searchHighlights"); + } + + /// + /// Creates a search score projection. + /// + /// The field. + /// + /// A search score projection. + /// + public ProjectionDefinition MetaSearchScore(string field) + { + return Meta(field, "searchScore"); + } + /// /// Creates a text score projection. /// @@ -375,6 +467,30 @@ public ProjectionDefinition MetaTextScore(string field) return Meta(field, "textScore"); } + /// + /// Creates a search metadata projection. + /// + /// The field. + /// + /// A search metadata projection. + /// + public ProjectionDefinition SearchMeta(FieldDefinition field) + { + return new SingleFieldProjectionDefinition(field, new BsonString("$$SEARCH_META")); + } + + /// + /// Creates a search metadata projection. + /// + /// The field. + /// + /// A search metadata projection. + /// + public ProjectionDefinition SearchMeta(Expression> field) + { + return SearchMeta(new ExpressionFieldDefinition(field)); + } + /// /// Creates an array slice projection. /// diff --git a/src/MongoDB.Driver/Search/CompoundSearchDefinitionBuilder.cs b/src/MongoDB.Driver/Search/CompoundSearchDefinitionBuilder.cs new file mode 100644 index 00000000000..d37516ae84d --- /dev/null +++ b/src/MongoDB.Driver/Search/CompoundSearchDefinitionBuilder.cs @@ -0,0 +1,139 @@ +/* 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.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// A builder for compound search definitions. + /// + /// The type of the document. + public sealed class CompoundSearchDefinitionBuilder + { + private List> _must; + private List> _mustNot; + private List> _should; + private List> _filter; + private int _minimumShouldMatch = 0; + + /// + /// Adds clauses which must match to produce results. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder Must(IEnumerable> clauses) => + AddClauses(ref _must, clauses); + + /// + /// Adds clauses which must match to produce results. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder Must(params SearchDefinition[] clauses) => + Must((IEnumerable>)clauses); + + /// + /// Adds clauses which must not match for a document to be included in the + /// results. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder MustNot(IEnumerable> clauses) => + AddClauses(ref _mustNot, clauses); + + /// + /// Adds clauses which must not match for a document to be included in the + /// results. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder MustNot(params SearchDefinition[] clauses) => + MustNot((IEnumerable>)clauses); + + /// + /// Adds clauses which cause documents in the result set to be scored higher if + /// they match. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder Should(IEnumerable> clauses) => + AddClauses(ref _should, clauses); + + /// + /// Adds clauses which cause documents in the result set to be scored higher if + /// they match. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder Should(params SearchDefinition[] clauses) => + Should((IEnumerable>)clauses); + + /// + /// Adds clauses which must all match for a document to be included in the + /// results. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder Filter(IEnumerable> clauses) => + AddClauses(ref _filter, clauses); + + /// + /// Adds clauses which must all match for a document to be included in the + /// results. + /// + /// The clauses. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder Filter(params SearchDefinition[] clauses) => + Filter((IEnumerable>)clauses); + + /// + /// Sets a value specifying the minimum number of should clauses the must match + /// to include a document in the results. + /// + /// The value to set. + /// The compound search definition builder. + public CompoundSearchDefinitionBuilder MinimumShouldMatch(int minimumShouldMatch) + { + _minimumShouldMatch = minimumShouldMatch; + return this; + } + + /// + /// Constructs a search definition from the builder. + /// + /// A compound search definition. + public SearchDefinition ToSearchDefinition() => + new CompoundSearchDefinition(_must, _mustNot, _should, _filter, _minimumShouldMatch); + + /// + /// Performs an implicit conversion from a + /// to a . + /// + /// The compound search definition builder. + /// The result of the conversion. + public static implicit operator SearchDefinition(CompoundSearchDefinitionBuilder builder) => + builder.ToSearchDefinition(); + + private CompoundSearchDefinitionBuilder AddClauses(ref List> clauses, IEnumerable> newClauses) + { + Ensure.IsNotNull(newClauses, nameof(newClauses)); + (clauses ??= new()).AddRange(newClauses); + + return this; + } + } +} diff --git a/src/MongoDB.Driver/Search/GeoShapeRelation.cs b/src/MongoDB.Driver/Search/GeoShapeRelation.cs new file mode 100644 index 00000000000..98bab0fec0b --- /dev/null +++ b/src/MongoDB.Driver/Search/GeoShapeRelation.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 +{ + /// + /// The relation of the query shape geometry to the indexed field geometry in a + /// geo shape search definition. + /// + public enum GeoShapeRelation + { + /// + /// Indicates that the indexed geometry contains the query geometry. + /// + Contains, + + /// + /// Indicates that both the query and indexed geometries have nothing in common. + /// + Disjoint, + + /// + /// Indicates that both the query and indexed geometries intersect. + /// + Intersects, + + /// + /// Indicates that the indexed geometry is within the query geometry. + /// + Within + } +} diff --git a/src/MongoDB.Driver/Search/GeoWithinArea.cs b/src/MongoDB.Driver/Search/GeoWithinArea.cs new file mode 100644 index 00000000000..4622409d474 --- /dev/null +++ b/src/MongoDB.Driver/Search/GeoWithinArea.cs @@ -0,0 +1,113 @@ +/* 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; +using MongoDB.Driver.GeoJsonObjectModel; + +namespace MongoDB.Driver.Search +{ + /// + /// Base class for area argument for GeoWithin queries. + /// + /// The type of the coordinates. + public abstract class GeoWithinArea where TCoordinates : GeoJsonCoordinates + { + internal abstract BsonElement Render(); + } + + /// + /// Object that specifies the bottom left and top right GeoJSON points of a box to + /// search within. + /// + /// The type of the coordinates. + public sealed class GeoWithinBox : GeoWithinArea where TCoordinates : GeoJsonCoordinates + { + /// + /// Initializes a new instance of the class. + /// + /// The bottom left GeoJSON point. + /// The top right GeoJSON point. + public GeoWithinBox(GeoJsonPoint bottomLeft, GeoJsonPoint topRight) + { + BottomLeft = Ensure.IsNotNull(bottomLeft, nameof(bottomLeft)); + TopRight = Ensure.IsNotNull(topRight, nameof(topRight)); + } + + /// Gets the bottom left GeoJSON point. + public GeoJsonPoint BottomLeft { get; } + + /// Gets the top right GeoJSON point. + public GeoJsonPoint TopRight { get; } + + internal override BsonElement Render() => + new("box", new BsonDocument + { + { "bottomLeft", BottomLeft.ToBsonDocument() }, + { "topRight", TopRight.ToBsonDocument() } + }); + } + + /// + /// Object that specifies the center point and the radius in meters to search within. + /// + public sealed class GeoWithinCircle : GeoWithinArea where TCoordinates : GeoJsonCoordinates + { + /// + /// Initializes a new instance of the class. + /// + /// Center of the circle specified as a GeoJSON point. + /// Radius specified in meters. + public GeoWithinCircle(GeoJsonPoint center, double radius) + { + Center = Ensure.IsNotNull(center, nameof(center)); + Radius = Ensure.IsGreaterThanZero(radius, nameof(radius)); + } + + /// Gets the center of the circle specified as a GeoJSON point. + public GeoJsonPoint Center { get; } + + /// Gets the radius specified in meters. + public double Radius { get; } + + internal override BsonElement Render() => + new("circle", new BsonDocument + { + { "center", Center.ToBsonDocument() }, + { "radius", Radius } + }); + } + + /// + /// Object that specifies the GeoJson geometry to search within. + /// + /// The type of the coordinates. + public sealed class GeoWithinGeometry : GeoWithinArea where TCoordinates : GeoJsonCoordinates + { + /// + /// Initializes a new instance of the class. + /// + /// GeoJSON object specifying the MultiPolygon or Polygon. + public GeoWithinGeometry(GeoJsonGeometry geometry) + { + Geometry = Ensure.IsNotNull(geometry, nameof(geometry)); + } + + /// Gets the GeoJson geometry. + public GeoJsonGeometry Geometry { get; } + + internal override BsonElement Render() => new("geometry", Geometry.ToBsonDocument()); + } +} diff --git a/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs b/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs new file mode 100644 index 00000000000..de3ac5a9b14 --- /dev/null +++ b/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs @@ -0,0 +1,393 @@ +/* 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 MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.GeoJsonObjectModel; + +namespace MongoDB.Driver.Search +{ + internal sealed class AutocompleteSearchDefinition : OperatorSearchDefinition + { + private readonly SearchFuzzyOptions _fuzzy; + private readonly SearchQueryDefinition _query; + private readonly SearchAutocompleteTokenOrder _tokenOrder; + + public AutocompleteSearchDefinition( + SearchPathDefinition path, + SearchQueryDefinition query, + SearchAutocompleteTokenOrder tokenOrder, + SearchFuzzyOptions fuzzy, + SearchScoreDefinition score) + : base(OperatorType.Autocomplete, path, score) + { + _query = Ensure.IsNotNull(query, nameof(query)); + _tokenOrder = tokenOrder; + _fuzzy = fuzzy; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "query", _query.Render() }, + { "tokenOrder", _tokenOrder.ToCamelCase(), _tokenOrder != SearchAutocompleteTokenOrder.Any }, + { "fuzzy", () => _fuzzy.Render(), _fuzzy != null }, + }; + } + + internal sealed class CompoundSearchDefinition : OperatorSearchDefinition + { + private readonly List> _filter; + private readonly int _minimumShouldMatch; + private readonly List> _must; + private readonly List> _mustNot; + private readonly List> _should; + + public CompoundSearchDefinition( + List> must, + List> mustNot, + List> should, + List> filter, + int minimumShouldMatch) + : base(OperatorType.Compound) + { + // This constructor should always be called from the compound search definition builder that ensures the arguments are valid. + _must = must; + _mustNot = mustNot; + _should = should; + _filter = filter; + _minimumShouldMatch = minimumShouldMatch; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + { + return new() + { + { "must", Render(_must), _must != null }, + { "mustNot", Render(_mustNot), _mustNot != null }, + { "should", Render(_should), _should != null }, + { "filter", Render(_filter), _filter != null }, + { "minimumShouldMatch", _minimumShouldMatch, _minimumShouldMatch > 0 }, + }; + + Func Render(List> searchDefinitions) => + () => new BsonArray(searchDefinitions.Select(clause => clause.Render(documentSerializer, serializerRegistry))); + } + } + + internal sealed class EqualsSearchDefinition : OperatorSearchDefinition + { + private readonly BsonValue _value; + + public EqualsSearchDefinition(FieldDefinition path, BsonValue value, SearchScoreDefinition score) + : base(OperatorType.Equals, path, score) + { + _value = value; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new("value", _value); + } + + internal sealed class ExistsSearchDefinition : OperatorSearchDefinition + { + public ExistsSearchDefinition(FieldDefinition path) + : base(OperatorType.Exists, path, null) + { + } + } + + internal sealed class FacetSearchDefinition : OperatorSearchDefinition + { + private readonly SearchFacet[] _facets; + private readonly SearchDefinition _operator; + + public FacetSearchDefinition(SearchDefinition @operator, IEnumerable> facets) + : base(OperatorType.Facet) + { + _operator = Ensure.IsNotNull(@operator, nameof(@operator)); + _facets = Ensure.IsNotNull(facets, nameof(facets)).ToArray(); + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "operator", _operator.Render(documentSerializer, serializerRegistry) }, + { "facets", new BsonDocument(_facets.Select(f => new BsonElement(f.Name, f.Render(documentSerializer, serializerRegistry)))) } + }; + } + + internal sealed class GeoShapeSearchDefinition : OperatorSearchDefinition + where TCoordinates : GeoJsonCoordinates + { + private readonly GeoJsonGeometry _geometry; + private readonly GeoShapeRelation _relation; + + public GeoShapeSearchDefinition( + SearchPathDefinition path, + GeoShapeRelation relation, + GeoJsonGeometry geometry, + SearchScoreDefinition score) + : base(OperatorType.GeoShape, path, score) + { + _geometry = Ensure.IsNotNull(geometry, nameof(geometry)); + _relation = relation; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "geometry", _geometry.ToBsonDocument() }, + { "relation", _relation.ToCamelCase() } + }; + } + + internal sealed class GeoWithinSearchDefinition : OperatorSearchDefinition + where TCoordinates : GeoJsonCoordinates + { + private readonly GeoWithinArea _area; + + public GeoWithinSearchDefinition( + SearchPathDefinition path, + GeoWithinArea area, + SearchScoreDefinition score) + : base(OperatorType.GeoWithin, path, score) + { + _area = Ensure.IsNotNull(area, nameof(area)); + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new(_area.Render()); + } + + internal sealed class MoreLikeThisSearchDefinition : OperatorSearchDefinition + { + private readonly TLike[] _like; + + public MoreLikeThisSearchDefinition(IEnumerable like) + : base(OperatorType.MoreLikeThis) + { + _like = Ensure.IsNotNull(like, nameof(like)).ToArray(); + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + { + var likeSerializer = typeof(TLike) switch + { + var t when t == typeof(BsonDocument) => null, + var t when t == typeof(TDocument) => (IBsonSerializer)documentSerializer, + _ => serializerRegistry.GetSerializer() + }; + + return new("like", new BsonArray(_like.Select(document => document.ToBsonDocument(likeSerializer)))); + } + } + + internal sealed class NearSearchDefinition : OperatorSearchDefinition + { + private readonly BsonValue _origin; + private readonly BsonValue _pivot; + + public NearSearchDefinition( + SearchPathDefinition path, + BsonValue origin, + BsonValue pivot, + SearchScoreDefinition score = null) + : base(OperatorType.Near, path, score) + { + _origin = origin; + _pivot = pivot; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "origin", _origin }, + { "pivot", _pivot } + }; + } + + internal sealed class PhraseSearchDefinition : OperatorSearchDefinition + { + private readonly SearchQueryDefinition _query; + private readonly int? _slop; + + public PhraseSearchDefinition( + SearchPathDefinition path, + SearchQueryDefinition query, + int? slop, + SearchScoreDefinition score) + : base(OperatorType.Phrase, path, score) + { + _query = Ensure.IsNotNull(query, nameof(query)); + _slop = slop; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "query", _query.Render() }, + { "slop", _slop, _slop != null } + }; + } + + internal sealed class QueryStringSearchDefinition : OperatorSearchDefinition + { + private readonly FieldDefinition _defaultPath; + private readonly string _query; + + public QueryStringSearchDefinition(FieldDefinition defaultPath, string query, SearchScoreDefinition score) + : base(OperatorType.QueryString, score) + { + _defaultPath = Ensure.IsNotNull(defaultPath, nameof(defaultPath)); + _query = Ensure.IsNotNull(query, nameof(query)); + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "defaultPath", _defaultPath.Render(documentSerializer, serializerRegistry).FieldName }, + { "query", _query } + }; + } + + internal sealed class RangeSearchDefinition : OperatorSearchDefinition + where TField : struct, IComparable + { + private readonly SearchRange _range; + + public RangeSearchDefinition( + SearchPathDefinition path, + SearchRange range, + SearchScoreDefinition score) + : base(OperatorType.Range, path, score) + { + _range = range; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { _range.IsMinInclusive ? "gte" : "gt", () => ToBsonValue(_range.Min.Value), _range.Min != null }, + { _range.IsMaxInclusive ? "lte" : "lt", () => ToBsonValue(_range.Max.Value), _range.Max != null }, + }; + + private static BsonValue ToBsonValue(TField value) => + value switch + { + sbyte v => (BsonInt32)v, + byte v => (BsonInt32)v, + short v => (BsonInt32)v, + ushort v => (BsonInt32)v, + int v => (BsonInt32)v, + uint v => (BsonInt32)v, + long v => (BsonInt64)v, + float v => (BsonDouble)v, + double v => (BsonDouble)v, + DateTime v => (BsonDateTime)v, + _ => throw new InvalidCastException() + }; + } + + internal sealed class RegexSearchDefinition : OperatorSearchDefinition + { + private readonly bool _allowAnalyzedField; + private readonly SearchQueryDefinition _query; + + public RegexSearchDefinition( + SearchPathDefinition path, + SearchQueryDefinition query, + bool allowAnalyzedField, + SearchScoreDefinition score) + : base(OperatorType.Regex, path, score) + { + _query = Ensure.IsNotNull(query, nameof(query)); + _allowAnalyzedField = allowAnalyzedField; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "query", _query.Render() }, + { "allowAnalyzedField", _allowAnalyzedField, _allowAnalyzedField }, + }; + } + + internal sealed class SpanSearchDefinition : OperatorSearchDefinition + { + private readonly SearchSpanDefinition _clause; + + public SpanSearchDefinition(SearchSpanDefinition clause) + : base(OperatorType.Span) + { + _clause = Ensure.IsNotNull(clause, nameof(clause)); + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + _clause.Render(documentSerializer, serializerRegistry); + } + + internal sealed class TextSearchDefinition : OperatorSearchDefinition + { + private readonly SearchFuzzyOptions _fuzzy; + private readonly SearchQueryDefinition _query; + + public TextSearchDefinition( + SearchPathDefinition path, + SearchQueryDefinition query, + SearchFuzzyOptions fuzzy, + SearchScoreDefinition score) + : base(OperatorType.Text, path, score) + { + _query = Ensure.IsNotNull(query, nameof(query)); + _fuzzy = fuzzy; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "query", _query.Render() }, + { "fuzzy", () => _fuzzy.Render(), _fuzzy != null }, + }; + } + + internal sealed class WildcardSearchDefinition : OperatorSearchDefinition + { + private readonly bool _allowAnalyzedField; + private readonly SearchQueryDefinition _query; + + public WildcardSearchDefinition( + SearchPathDefinition path, + SearchQueryDefinition query, + bool allowAnalyzedField, + SearchScoreDefinition score) + : base(OperatorType.Wildcard, path, score) + { + _query = Ensure.IsNotNull(query, nameof(query)); + _allowAnalyzedField = allowAnalyzedField; + } + + private protected override BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "query", _query.Render() }, + { "allowAnalyzedField", _allowAnalyzedField, _allowAnalyzedField }, + }; + } +} diff --git a/src/MongoDB.Driver/Search/SearchAutocompleteTokenOrder.cs b/src/MongoDB.Driver/Search/SearchAutocompleteTokenOrder.cs new file mode 100644 index 00000000000..780698e2d93 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchAutocompleteTokenOrder.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. +*/ + +namespace MongoDB.Driver.Search +{ + /// + /// The order in which to search for tokens in an autocomplete search definition. + /// + public enum SearchAutocompleteTokenOrder + { + /// + /// Indicates that tokens in the query can appear in any order in the documents. + /// + Any, + + /// + /// Indicates that tokens in the query must appear adjacent to each other or in the order + /// specified in the query in the documents. + /// + Sequential + } +} diff --git a/src/MongoDB.Driver/Search/SearchCountOptions.cs b/src/MongoDB.Driver/Search/SearchCountOptions.cs new file mode 100644 index 00000000000..c3624c9175b --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchCountOptions.cs @@ -0,0 +1,55 @@ +/* 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 sealed class SearchCountOptions + { + private int? _threshold; + private SearchCountType _type = SearchCountType.LowerBound; + + /// + /// 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)); + } + + /// + /// Gets or sets the type of count of the documents in the result set. + /// + public SearchCountType Type + { + get => _type; + set => _type = 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..f4a87047eae --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchCountType.cs @@ -0,0 +1,33 @@ +/* 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 +{ + /// + /// 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..a8bc26686e4 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchDefinition.cs @@ -0,0 +1,166 @@ +/* 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); + } + + internal abstract class OperatorSearchDefinition : SearchDefinition + { + private protected enum OperatorType + { + Autocomplete, + Compound, + EmbeddedDocument, + Equals, + Exists, + Facet, + GeoShape, + GeoWithin, + MoreLikeThis, + Near, + Phrase, + QueryString, + Range, + Regex, + Search, + Span, + Term, + Text, + Wildcard + } + + private readonly OperatorType _operatorType; + // _path and _score used by many but not all subclasses + private readonly SearchPathDefinition _path; + private readonly SearchScoreDefinition _score; + + private protected OperatorSearchDefinition(OperatorType operatorType) + : this(operatorType, null) + { + } + + private protected OperatorSearchDefinition(OperatorType operatorType, SearchScoreDefinition score) + { + _operatorType = operatorType; + _score = score; + } + + private protected OperatorSearchDefinition(OperatorType operatorType, SearchPathDefinition path, SearchScoreDefinition score) + { + _operatorType = operatorType; + _path = Ensure.IsNotNull(path, nameof(path)); + _score = score; + } + + public sealed override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) + { + var renderedArgs = RenderArguments(documentSerializer, serializerRegistry); + renderedArgs.Add("path", () => _path.Render(documentSerializer, serializerRegistry), _path != null); + renderedArgs.Add("score", () => _score.Render(documentSerializer, serializerRegistry), _score != null); + + return new(_operatorType.ToCamelCase(), renderedArgs); + } + + private protected virtual BsonDocument RenderArguments(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => new(); + } +} diff --git a/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs b/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs new file mode 100644 index 00000000000..b7c3fb8cf2d --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs @@ -0,0 +1,668 @@ +/* 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.Expressions; +using MongoDB.Bson; +using MongoDB.Driver.GeoJsonObjectModel; + +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 a search for a word or phrase that contains + /// a sequence of characters from an incomplete input string. + /// + /// The indexed field to search. + /// The query definition specifying the string or strings to search for. + /// The order in which to search for tokens. + /// The options for fuzzy search. + /// The score modifier. + /// An autocomplete search definition. + public SearchDefinition Autocomplete( + SearchPathDefinition path, + SearchQueryDefinition query, + SearchAutocompleteTokenOrder tokenOrder = SearchAutocompleteTokenOrder.Any, + SearchFuzzyOptions fuzzy = null, + SearchScoreDefinition score = null) => + new AutocompleteSearchDefinition(path, query, tokenOrder, fuzzy, score); + + /// + /// Creates a search definition that performs a search for a word or phrase that contains + /// a sequence of characters from an incomplete search string. + /// + /// The type of the field. + /// The indexed field to search. + /// The query definition specifying the string or strings to search for. + /// The order in which to search for tokens. + /// The options for fuzzy search. + /// The score modifier. + /// An autocomplete search definition. + public SearchDefinition Autocomplete( + Expression> path, + SearchQueryDefinition query, + SearchAutocompleteTokenOrder tokenOrder = SearchAutocompleteTokenOrder.Any, + SearchFuzzyOptions fuzzy = null, + SearchScoreDefinition score = null) => + Autocomplete(new ExpressionFieldDefinition(path), query, tokenOrder, fuzzy, score); + + /// + /// Creates a builder for a compound search definition. + /// + /// + public CompoundSearchDefinitionBuilder Compound() => new CompoundSearchDefinitionBuilder(); + + /// + /// Creates a search definition that queries for documents where an indexed field is equal + /// to the specified value. + /// + /// The indexed field to search. + /// The value to query for. + /// The score modifier. + /// An equality search definition. + public SearchDefinition Equals( + FieldDefinition path, + bool value, + SearchScoreDefinition score = null) => + new EqualsSearchDefinition(path, value, score); + + /// + /// Creates a search definition that queries for documents where an indexed field is equal + /// to the specified value. + /// + /// The indexed field to search. + /// The value to query for. + /// The score modifier. + /// An equality search definition. + public SearchDefinition Equals( + FieldDefinition path, + ObjectId value, + SearchScoreDefinition score = null) => + new EqualsSearchDefinition(path, value, score); + + /// + /// Creates a search definition that queries for documents where an indexed field is equal + /// to the specified value. + /// + /// The indexed field to search. + /// The value to query for. + /// The score modifier. + /// An equality search definition. + public SearchDefinition Equals( + Expression> path, + bool value, + SearchScoreDefinition score = null) => + Equals(new ExpressionFieldDefinition(path), value, score); + + /// + /// Creates a search definition that queries for documents where an indexed field is equal + /// to the specified value. + /// + /// The indexed field to search. + /// The value to query for. + /// The score modifier. + /// An equality search definition. + public SearchDefinition Equals( + Expression> path, + ObjectId value, + SearchScoreDefinition score = null) => + Equals(new ExpressionFieldDefinition(path), value, score); + + /// + /// Creates a search definition that tests if a path to a specified indexed field name + /// exists in a document. + /// + /// The field to test for. + /// An existence search definition. + public SearchDefinition Exists(FieldDefinition path) => + new ExistsSearchDefinition(path); + + /// + /// Creates a search definition that tests if a path to a specified indexed field name + /// exists in a document. + /// + /// The type of the field. + /// The field to test for. + /// An existence search definition. + public SearchDefinition Exists(Expression> path) => + Exists(new ExpressionFieldDefinition(path)); + + /// + /// Creates a search definition that groups results by values or ranges in the specified + /// faceted fields and returns the count for each of those groups. + /// + /// The operator to use to perform the facet over. + /// Information for bucketing the data for each facet. + /// A facet search definition. + public SearchDefinition Facet( + SearchDefinition @operator, + IEnumerable> facets) => + new FacetSearchDefinition(@operator, facets); + + /// + /// Creates a search definition that groups results by values or ranges in the specified + /// faceted fields and returns the count for each of those groups. + /// + /// The operator to use to perform the facet over. + /// Information for bucketing the data for each facet. + /// A facet search definition. + public SearchDefinition Facet( + SearchDefinition @operator, + params SearchFacet[] facets) => + Facet(@operator, (IEnumerable>)facets); + + /// + /// Creates a search definition that queries for shapes with a given geometry. + /// + /// The type of the coordinates. + /// Indexed geo type field or fields to search. + /// + /// + /// GeoJSON object specifying the Polygon, MultiPolygon, or LineString shape or point + /// to search. + /// + /// Relation of the query shape geometry to the indexed field geometry. + /// + /// The score modifier. + /// A geo shape search definition. + public SearchDefinition GeoShape( + SearchPathDefinition path, + GeoShapeRelation relation, + GeoJsonGeometry geometry, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + new GeoShapeSearchDefinition(path, relation, geometry, score); + + /// + /// Creates a search definition that queries for shapes with a given geometry. + /// + /// The type of the coordinates. + /// The type of the field. + /// Indexed geo type field or fields to search. + /// + /// + /// GeoJSON object specifying the Polygon, MultiPolygon, or LineString shape or point + /// to search. + /// + /// Relation of the query shape geometry to the indexed field geometry. + /// + /// The score modifier. + /// A geo shape search definition. + public SearchDefinition GeoShape( + Expression> path, + GeoShapeRelation relation, + GeoJsonGeometry geometry, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + GeoShape( + new ExpressionFieldDefinition(path), + relation, + geometry, + score); + + /// + /// Creates a search definition that queries for geographic points within a given + /// geometry. + /// + /// The type of the coordinates. + /// Indexed geo type field or fields to search. + /// + /// GeoJSON object specifying the MultiPolygon or Polygon to search within. + /// + /// The score modifier. + /// A geo within search definition. + public SearchDefinition GeoWithin( + SearchPathDefinition path, + GeoJsonGeometry geometry, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + GeoWithin(path, new GeoWithinGeometry(geometry), score); + + /// + /// Creates a search definition that queries for geographic points within a given + /// geometry. + /// + /// The type of the coordinates. + /// The type of the field. + /// Indexed geo type field or fields to search. + /// + /// GeoJSON object specifying the MultiPolygon or Polygon to search within. + /// + /// The score modifier. + /// A geo within search definition. + public SearchDefinition GeoWithin( + Expression> path, + GeoJsonGeometry geometry, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + GeoWithin(path, new GeoWithinGeometry(geometry), score); + + /// + /// Creates a search definition that queries for geographic points within a given geo object. + /// + /// The type of the coordinates. + /// The type of the field. + /// Indexed geo type field or fields to search. + /// Object that specifies the area to search within. + /// The score modifier. + /// A geo within search definition. + public SearchDefinition GeoWithin( + Expression> path, + GeoWithinArea area, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + GeoWithin(new ExpressionFieldDefinition(path), area, score); + + /// + /// Creates a search definition that queries for geographic points within a given geo object. + /// + /// The type of the coordinates. + /// Indexed geo type field or fields to search. + /// Object that specifies the area to search within. + /// The score modifier. + /// A geo within search definition. + public SearchDefinition GeoWithin( + SearchPathDefinition path, + GeoWithinArea area, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + new GeoWithinSearchDefinition(path, area, score); + + /// + /// Creates a search definition that returns documents similar to the input documents. + /// + /// The type of the like documents. + /// + /// One or more documents that Atlas Search uses to extract representative terms for. + /// + /// A more like this search definition. + public SearchDefinition MoreLikeThis(IEnumerable like) => + new MoreLikeThisSearchDefinition(like); + + /// + /// Creates a search definition that returns documents similar to the input documents. + /// + /// The type of the like documents. + /// + /// One or more documents that Atlas Search uses to extract representative terms for. + /// + /// A more like this search definition. + public SearchDefinition MoreLikeThis(params TLike[] like) => + MoreLikeThis((IEnumerable)like); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + SearchPathDefinition path, + double origin, + double pivot, + SearchScoreDefinition score = null) => + new NearSearchDefinition(path, origin, pivot, score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + Expression> path, + double origin, + double pivot, + SearchScoreDefinition score = null) => + Near(new ExpressionFieldDefinition(path), origin, pivot, score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + SearchPathDefinition path, + int origin, + int pivot, + SearchScoreDefinition score = null) => + new NearSearchDefinition(path, new BsonInt32(origin), new BsonInt32(pivot), score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + Expression> path, + int origin, + int pivot, + SearchScoreDefinition score = null) => + Near(new ExpressionFieldDefinition(path), origin, pivot, score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + SearchPathDefinition path, + long origin, + long pivot, + SearchScoreDefinition score = null) => + new NearSearchDefinition(path, new BsonInt64(origin), new BsonInt64(pivot), score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + Expression> path, + long origin, + long pivot, + SearchScoreDefinition score = null) => + Near(new ExpressionFieldDefinition(path), origin, pivot, score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + SearchPathDefinition path, + DateTime origin, + long pivot, + SearchScoreDefinition score = null) => + new NearSearchDefinition(path, new BsonDateTime(origin), new BsonInt64(pivot), score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + Expression> path, + DateTime origin, + long pivot, + SearchScoreDefinition score = null) => + Near(new ExpressionFieldDefinition(path), origin, pivot, score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The type of the coordinates. + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to use to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + SearchPathDefinition path, + GeoJsonPoint origin, + double pivot, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + new NearSearchDefinition(path, origin.ToBsonDocument(), pivot, score); + + /// + /// Creates a search definition that supports querying and scoring numeric and date values. + /// + /// The type of the coordinates + /// The type of the fields. + /// The indexed field or fields to search. + /// The number, date, or geographic point to search near. + /// The value to user to calculate scores of result documents. + /// The score modifier. + /// A near search definition. + public SearchDefinition Near( + Expression> path, + GeoJsonPoint origin, + double pivot, + SearchScoreDefinition score = null) + where TCoordinates : GeoJsonCoordinates => + Near(new ExpressionFieldDefinition(path), origin, pivot, 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 allowable distance between words in the query phrase. + /// The score modifier. + /// A phrase search definition. + public SearchDefinition Phrase( + SearchPathDefinition path, + SearchQueryDefinition query, + int? slop = null, + SearchScoreDefinition score = null) => + new PhraseSearchDefinition(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 allowable distance between words in the query phrase. + /// The score modifier. + /// A phrase search definition. + public SearchDefinition Phrase( + Expression> path, + SearchQueryDefinition query, + int? slop = null, + SearchScoreDefinition score = null) => + Phrase(new ExpressionFieldDefinition(path), query, slop, score); + + /// + /// Creates a search definition that queries a combination of indexed fields and values. + /// + /// The indexed field to search by default. + /// One or more indexed fields and values to search. + /// The score modifier. + /// A query string search definition. + public SearchDefinition QueryString( + FieldDefinition defaultPath, + string query, + SearchScoreDefinition score = null) => + new QueryStringSearchDefinition(defaultPath, query, score); + + /// + /// Creates a search definition that queries a combination of indexed fields and values. + /// + /// The type of the field. + /// The indexed field to search by default. + /// One or more indexed fields and values to search. + /// The score modifier. + /// A query string search definition. + public SearchDefinition QueryString( + Expression> defaultPath, + string query, + SearchScoreDefinition score = null) => + QueryString(new ExpressionFieldDefinition(defaultPath), query, score); + + /// + /// Creates a search definition that queries for documents where a floating-point + /// field is in the specified range. + /// + /// A fluent range interface. + public SearchDefinition Range( + Expression> path, + SearchRange range, + SearchScoreDefinition score = null) + where TField : struct, IComparable => + Range(new ExpressionFieldDefinition(path), range, score); + + /// + /// Creates a search definition that queries for documents where a floating-point + /// field is in the specified range. + /// + /// A fluent range interface. + public SearchDefinition Range( + SearchPathDefinition path, + SearchRange range, + SearchScoreDefinition score = null) + where TField : struct, IComparable => + new RangeSearchDefinition(path, range, score); + + /// + /// Creates a search definition that interprets the query as a regular expression. + /// + /// The indexed field or fields to search. + /// The string or strings to search for. + /// + /// Must be set to true if the query is run against an analyzed field. + /// + /// The score modifier. + /// A regular expression search definition. + public SearchDefinition Regex( + SearchPathDefinition path, + SearchQueryDefinition query, + bool allowAnalyzedField = false, + SearchScoreDefinition score = null) => + new RegexSearchDefinition(path, query, allowAnalyzedField, score); + + /// + /// Creates a search definition that interprets the query as a regular expression. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The string or strings to search for. + /// + /// Must be set to true if the query is run against an analyzed field. + /// + /// The score modifier. + /// A regular expression search definition. + public SearchDefinition Regex( + Expression> path, + SearchQueryDefinition query, + bool allowAnalyzedField = false, + SearchScoreDefinition score = null) => + Regex(new ExpressionFieldDefinition(path), query, allowAnalyzedField, score); + + /// + /// Creates a search definition that finds text search matches within regions of a text + /// field. + /// + /// The span clause. + /// A span search definition. + 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 for fuzzy search. + /// The score modifier. + /// A text search definition. + public SearchDefinition Text( + SearchPathDefinition path, + SearchQueryDefinition query, + SearchFuzzyOptions fuzzy = null, + SearchScoreDefinition score = null) => + new TextSearchDefinition(path, query, 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 indexed field or field to search. + /// The string or strings to search for. + /// The options for fuzzy search. + /// The score modifier. + /// A text search definition. + public SearchDefinition Text( + Expression> path, + SearchQueryDefinition query, + SearchFuzzyOptions fuzzy = null, + SearchScoreDefinition score = null) => + Text(new ExpressionFieldDefinition(path), query, fuzzy, score); + + /// + /// Creates a search definition that uses special characters in the search string that can + /// match any character. + /// + /// The indexed field or fields to search. + /// The string or strings to search for. + /// + /// Must be set to true if the query is run against an analyzed field. + /// + /// The score modifier. + /// A wildcard search definition. + public SearchDefinition Wildcard( + SearchPathDefinition path, + SearchQueryDefinition query, + bool allowAnalyzedField = false, + SearchScoreDefinition score = null) => + new WildcardSearchDefinition(path, query, allowAnalyzedField, score); + + /// + /// Creates a search definition that uses special characters in the search string that can + /// match any character. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The string or strings to search for. + /// + /// Must be set to true if the query is run against an analyzed field. + /// + /// The score modifier. + /// A wildcard search definition. + public SearchDefinition Wildcard( + Expression> path, + SearchQueryDefinition query, + bool allowAnalyzedField = false, + SearchScoreDefinition score = null) => + Wildcard(new ExpressionFieldDefinition(path), query, allowAnalyzedField, score); + } +} diff --git a/src/MongoDB.Driver/Search/SearchFacet.cs b/src/MongoDB.Driver/Search/SearchFacet.cs new file mode 100644 index 00000000000..4cd134b04d4 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchFacet.cs @@ -0,0 +1,49 @@ +/* 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 facets. + /// + /// The type of the document. + public abstract class SearchFacet + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the facet. + protected SearchFacet(string name) + { + Name = name; + } + + /// + /// Gets the name of the facet. + /// + public string Name { get; } + + /// + /// Renders the search facet to a . + /// + /// The document serializer. + /// The serializer registry. + /// A . + public abstract BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry); + } +} diff --git a/src/MongoDB.Driver/Search/SearchFacetBuilder.cs b/src/MongoDB.Driver/Search/SearchFacetBuilder.cs new file mode 100644 index 00000000000..70472c6e280 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchFacetBuilder.cs @@ -0,0 +1,276 @@ +/* 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 facet. + /// + /// The type of the document. + public sealed class SearchFacetBuilder + { + /// + /// Creates a facet that narrows down search result based on a date. + /// + /// The name of the fact. + /// The field path to facet on. + /// + /// A list of date values that specify the boundaries for each bucket. + /// + /// + /// The name of an additional bucket that counts documents returned from the operator that + /// do not fall within the specified boundaries. + /// + /// A date search facet. + public SearchFacet Date( + string name, + SearchPathDefinition path, + IEnumerable boundaries, + string @default = null) => + new DateSearchFacet(name, path, boundaries, @default); + + /// + /// Creates a facet that narrows down search result based on a date. + /// + /// The name of the fact. + /// The field path to facet on. + /// + /// A list of date values that specify the boundaries for each bucket. + /// + /// A date search facet. + public SearchFacet Date( + string name, + SearchPathDefinition path, + params DateTime[] boundaries) => + Date(name, path, (IEnumerable)boundaries); + + /// + /// Creates a facet that narrows down search result based on a date. + /// + /// The type of the field. + /// The name of the fact. + /// The field path to facet on. + /// + /// A list of date values that specify the boundaries for each bucket. + /// + /// + /// The name of an additional bucket that counts documents returned from the operator that + /// do not fall within the specified boundaries. + /// + /// A date search facet. + public SearchFacet Date( + string name, + Expression> path, + IEnumerable boundaries, + string @default = null) => + Date(name, new ExpressionFieldDefinition(path), boundaries, @default); + + /// + /// Creates a facet that narrows down search result based on a date. + /// + /// The type of the field. + /// The name of the fact. + /// The field path to facet on. + /// + /// A list of date values that specify the boundaries for each bucket. + /// + /// A date search facet. + public SearchFacet Date( + string name, + Expression> path, + params DateTime[] boundaries) => + Date(name, new ExpressionFieldDefinition(path), boundaries); + + /// + /// Creates a facet that determines the frequency of numeric values by breaking the search + /// results into separate ranges of numbers. + /// + /// The name of the facet. + /// The field path to facet on. + /// + /// A list of numeric values that specify the boundaries for each bucket. + /// + /// + /// The name of an additional bucket that counts documents returned from the operator that + /// do not fall within the specified boundaries. + /// + /// A number search facet. + public SearchFacet Number( + string name, + SearchPathDefinition path, + IEnumerable boundaries, + string @default = null) => + new NumberSearchFacet(name, path, boundaries, @default); + + /// + /// Creates a facet that determines the frequency of numeric values by breaking the search + /// results into separate ranges of numbers. + /// + /// The name of the facet. + /// The field path to facet on. + /// + /// A list of numeric values that specify the boundaries for each bucket. + /// + /// A number search facet. + public SearchFacet Number( + string name, + SearchPathDefinition path, + params BsonValue[] boundaries) => + Number(name, path, (IEnumerable)boundaries); + + /// + /// Creates a facet that determines the frequency of numeric values by breaking the search + /// results into separate ranges of numbers. + /// + /// The type of the field. + /// The name of the facet. + /// The field path to facet on. + /// + /// A list of numeric values that specify the boundaries for each bucket. + /// + /// + /// The name of an additional bucket that counts documents returned from the operator that + /// do not fall within the specified boundaries. + /// + /// A number search facet. + public SearchFacet Number( + string name, + Expression> path, + IEnumerable boundaries, + string @default = null) => + Number(name, new ExpressionFieldDefinition(path), boundaries, @default); + + /// + /// Creates a facet that determines the frequency of numeric values by breaking the search + /// results into separate ranges of numbers. + /// + /// The type of the field. + /// The name of the facet. + /// The field path to facet on. + /// + /// A list of numeric values that specify the boundaries for each bucket. + /// + /// A number search facet. + public SearchFacet Number( + string name, + Expression> path, + params BsonValue[] boundaries) => + Number(name, new ExpressionFieldDefinition(path), boundaries); + + /// + /// Creates a facet that narrows down Atlas Search results based on the most frequent + /// string values in the specified string field. + /// + /// The name of the facet. + /// The field path to facet on. + /// + /// The maximum number of facet categories to return in the results. + /// + /// A string search facet. + public SearchFacet String(string name, SearchPathDefinition path, int? numBuckets = null) => + new StringSearchFacet(name, path, numBuckets); + + /// + /// Creates a facet that narrows down Atlas Search result based on the most frequent + /// string values in the specified string field. + /// + /// The type of the field. + /// The name of the facet. + /// The field path to facet on. + /// + /// The maximum number of facet categories to return in the results. + /// + /// A string search facet. + public SearchFacet String(string name, Expression> path, int? numBuckets = null) => + String(name, new ExpressionFieldDefinition(path), numBuckets); + } + + internal sealed class DateSearchFacet : SearchFacet + { + private readonly DateTime[] _boundaries; + private readonly string _default; + private readonly SearchPathDefinition _path; + + public DateSearchFacet(string name, SearchPathDefinition path, IEnumerable boundaries, string @default) + : base(name) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _boundaries = Ensure.IsNotNull(boundaries, nameof(boundaries)).ToArray(); + _default = @default; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "type", "date" }, + { "path", _path.Render(documentSerializer, serializerRegistry) }, + { "boundaries", new BsonArray(_boundaries) }, + { "default", _default, _default != null } + }; + } + + internal sealed class NumberSearchFacet : SearchFacet + { + private readonly BsonValue[] _boundaries; + private readonly string _default; + private readonly SearchPathDefinition _path; + + public NumberSearchFacet(string name, SearchPathDefinition path, IEnumerable boundaries, string @default) + : base(name) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _boundaries = Ensure.IsNotNull(boundaries, nameof(boundaries)).ToArray(); + _default = @default; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "type", "number" }, + { "path", _path.Render(documentSerializer, serializerRegistry) }, + { "boundaries", new BsonArray(_boundaries) }, + { "default", _default, _default != null } + }; + } + + internal sealed class StringSearchFacet : SearchFacet + { + private readonly int? _numBuckets; + private readonly SearchPathDefinition _path; + + public StringSearchFacet(string name, SearchPathDefinition path, int? numBuckets = null) + : base(name) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _numBuckets = Ensure.IsNullOrBetween(numBuckets, 1, 1000, nameof(numBuckets)); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "type", "string" }, + { "path", _path.Render(documentSerializer, serializerRegistry) }, + { "numBuckets", _numBuckets, _numBuckets != null } + }; + } +} diff --git a/src/MongoDB.Driver/Search/SearchFuzzyOptions.cs b/src/MongoDB.Driver/Search/SearchFuzzyOptions.cs new file mode 100644 index 00000000000..2558cb9e6ae --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchFuzzyOptions.cs @@ -0,0 +1,67 @@ +/* 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 SearchFuzzyOptions + { + private int? _maxEdits; + private int? _maxExpansions; + private int? _prefixLength; + + /// + /// 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 variations to generate and search for. + /// + public int? MaxExpansions + { + get => _maxExpansions; + set => _maxExpansions = Ensure.IsNullOrGreaterThanZero(value, 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)); + } + + internal BsonDocument Render() + => new() + { + { "maxEdits", _maxEdits, _maxEdits != null }, + { "prefixLength", _prefixLength, _prefixLength != null }, + { "maxExpansions", _maxExpansions, _maxExpansions != null } + }; + } +} diff --git a/src/MongoDB.Driver/Search/SearchHighlight.cs b/src/MongoDB.Driver/Search/SearchHighlight.cs new file mode 100644 index 00000000000..204584359a0 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchHighlight.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.Attributes; + +namespace MongoDB.Driver.Search +{ + /// + /// Represents a result of highlighting. + /// + public sealed class SearchHighlight + { + /// + /// Initializes a new instance of the class. + /// + /// document field which returned a match. + /// Score assigned to this result. + /// Objects containing the matching text and the surrounding text. + public SearchHighlight(string path, double score, SearchHighlightText[] texts) + { + Path = path; + Score = score; + Texts = texts; + } + + /// + /// Gets the document field which returned a match. + /// + [BsonElement("path")] + public string Path { get; } + + /// + /// Gets the score assigned to this result. + /// + [BsonElement("score")] + public double Score { get; } + + /// + /// Gets one or more objects containing the matching text and the surrounding text + /// (if any). + /// + [BsonDefaultValue(null)] + [BsonElement("texts")] + public SearchHighlightText[] Texts { get; } + } + + /// + /// Represents the matching text or the surrounding text of a highlighting result. + /// + public sealed class SearchHighlightText + { + /// + /// Initializes a new instance of the class. + /// + /// Type of search highlight. + /// Text from the field which returned a match. + public SearchHighlightText(HighlightTextType type, string value) + { + Type = type; + Value = value; + } + + /// + /// Gets or sets the type of text, matching or surrounding. + /// + [BsonElement("type")] + [BsonRepresentation(BsonType.String)] + public HighlightTextType Type { get; } + + /// + /// Gets the text from the field which returned a match. + /// + [BsonElement("value")] + public string Value { get; } + } + + /// + /// Represents the type of text in a highlighting result, matching or surrounding. + /// + public enum HighlightTextType + { + /// + /// Indicates that the text contains a match. + /// + Hit, + + /// + /// Indicates that the text contains the text content adjacent to a matching string. + /// + Text + } +} diff --git a/src/MongoDB.Driver/Search/SearchHighlightOptions.cs b/src/MongoDB.Driver/Search/SearchHighlightOptions.cs new file mode 100644 index 00000000000..ec4026742fb --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchHighlightOptions.cs @@ -0,0 +1,112 @@ +/* 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 +{ + /// + /// Options for highlighting. + /// + /// The type of the document. + public sealed class SearchHighlightOptions + { + private int? _maxCharsToExamine; + private int? _maxNumPassages; + private SearchPathDefinition _path; + + // constructors + /// + /// Initializes a new instance of the class. + /// + /// The document field to search. + /// maximum number of characters to examine. + /// The number of high-scoring passages. + public SearchHighlightOptions(SearchPathDefinition path, int? maxCharsToExamine = null, int? maxNumPassages = null) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _maxCharsToExamine = maxCharsToExamine; + _maxNumPassages = maxNumPassages; + } + + /// + /// 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 SearchHighlightOptions( + Expression> path, + int? maxCharsToExamine = null, + int? maxNumPassages = null) + : this(new ExpressionFieldDefinition(path), maxCharsToExamine, maxNumPassages) + { + } + + /// + /// 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)); + } + + /// + /// Gets or sets the document field to search. + /// + public SearchPathDefinition Path + { + get => _path; + set => _path = Ensure.IsNotNull(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/SearchMetaResult.cs b/src/MongoDB.Driver/Search/SearchMetaResult.cs new file mode 100644 index 00000000000..7f57dc35123 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchMetaResult.cs @@ -0,0 +1,133 @@ +/* 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.Bson.Serialization.Attributes; + +namespace MongoDB.Driver.Search +{ + /// + /// A search count result set. + /// + public sealed class SearchMetaCountResult + { + /// + /// Initializes a new instance of the class. + /// + /// Lower bound for this result set. + /// Total for this result set. + public SearchMetaCountResult(long? lowerBound, long? total) + { + LowerBound = lowerBound; + Total = total; + } + + /// + /// Gets the lower bound for this result set. + /// + [BsonDefaultValue(null)] + [BsonElement("lowerBound")] + public long? LowerBound { get; } + + /// + /// Gets the total for this result set. + /// + [BsonDefaultValue(null)] + [BsonElement("total")] + public long? Total { get; } + } + + /// + /// A search facet bucket result set. + /// + public sealed class SearchMetaFacetBucketResult + { + /// + /// Initializes a new instance of the class. + /// + /// count of documents in this facet bucket. + /// Unique identifier that identifies this facet bucket. + public SearchMetaFacetBucketResult(long count, BsonValue id) + { + Count = count; + Id = id; + } + + /// + /// Gets the count of documents in this facet bucket. + /// + [BsonElement("count")] + public long Count { get; } + + /// + /// Gets the unique identifier that identifies this facet bucket. + /// + [BsonId] + public BsonValue Id { get; } + } + + /// + /// A search facet result set. + /// + public sealed class SearchMetaFacetResult + { + /// + /// Initializes a new instance of the class. + /// + /// An array of bucket result sets. + public SearchMetaFacetResult(SearchMetaFacetBucketResult[] buckets) + { + Buckets = buckets; + } + + /// + /// Gets an array of bucket result sets. + /// + [BsonElement("buckets")] + public SearchMetaFacetBucketResult[] Buckets { get; } + } + + /// + /// A result set for a search metadata query. + /// + public sealed class SearchMetaResult + { + /// + /// Initializes a new instance of the class. + /// + /// Count result set. + /// Facet result sets. + public SearchMetaResult(SearchMetaCountResult count, IReadOnlyDictionary facet) + { + Count = count; + Facet = facet; + } + + /// + /// Gets the count result set. + /// + [BsonDefaultValue(null)] + [BsonElement("count")] + public SearchMetaCountResult Count { get; } + + /// + /// Gets the facet result sets. + /// + [BsonDefaultValue(null)] + [BsonElement("facet")] + public IReadOnlyDictionary Facet { get; } + } +} diff --git a/src/MongoDB.Driver/Search/SearchPathDefinition.cs b/src/MongoDB.Driver/Search/SearchPathDefinition.cs new file mode 100644 index 00000000000..b4efc54e607 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchPathDefinition.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 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 SearchPathDefinition + { + /// + /// 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 SearchPathDefinition(FieldDefinition field) => + new SingleSearchPathDefinition(field); + + /// + /// Performs an implicit conversion from a field name to . + /// + /// The field name. + /// + /// The result of the conversion. + /// + public static implicit operator SearchPathDefinition(string fieldName) => + new SingleSearchPathDefinition(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 SearchPathDefinition(FieldDefinition[] fields) => + new MultiSearchPathDefinition(fields); + + /// + /// Performs an implicit conversion from a list of to + /// . + /// + /// The list of fields. + /// + /// The result of the conversion. + /// + public static implicit operator SearchPathDefinition(List> fields) => + new MultiSearchPathDefinition(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 SearchPathDefinition(string[] fieldNames) => + new MultiSearchPathDefinition(fieldNames.Select(fieldName => new StringFieldDefinition(fieldName))); + + /// + /// Performs an implicit conversion from an array of field names to + /// . + /// + /// The list of field names. + /// + /// The result of the conversion. + /// + public static implicit operator SearchPathDefinition(List fieldNames) => + new MultiSearchPathDefinition(fieldNames.Select(fieldName => new StringFieldDefinition(fieldName))); + } +} diff --git a/src/MongoDB.Driver/Search/SearchPathDefinitionBuilder.cs b/src/MongoDB.Driver/Search/SearchPathDefinitionBuilder.cs new file mode 100644 index 00000000000..0537e13ce54 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchPathDefinitionBuilder.cs @@ -0,0 +1,168 @@ +/* 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 SearchPathDefinitionBuilder + { + /// + /// Creates a search path that searches using the specified analyzer. + /// + /// The field definition + /// The name of the analyzer. + /// An analyzer search path. + public SearchPathDefinition Analyzer(FieldDefinition field, string analyzerName) => + new AnalyzerSearchPathDefinition(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 SearchPathDefinition Analyzer(Expression> field, string analyzerName) => + Analyzer(new ExpressionFieldDefinition(field), analyzerName); + + /// + /// Creates a search path for multiple fields. + /// + /// The collection of field definitions. + /// A multi-field search path. + public SearchPathDefinition Multi(IEnumerable> fields) => + new MultiSearchPathDefinition(fields); + + /// + /// Creates a search path for multiple fields. + /// + /// The array of field definitions. + /// A multi-field search path. + public SearchPathDefinition 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 SearchPathDefinition Multi(params Expression>[] fields) => + Multi(fields.Select(x => new ExpressionFieldDefinition(x))); + + /// + /// Creates a search path for a single field. + /// + /// The field definition. + /// A single-field search path. + public SearchPathDefinition Single(FieldDefinition field) => + new SingleSearchPathDefinition(field); + + /// + /// Creates a search path for a single field. + /// + /// The type of the field. + /// The field definition. + /// A single-field search path. + public SearchPathDefinition Single(Expression> field) => + Single(new ExpressionFieldDefinition(field)); + + /// + /// 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 SearchPathDefinition Wildcard(string query) => + new WildcardSearchPathDefinition(query); + } + + internal sealed class AnalyzerSearchPathDefinition : SearchPathDefinition + { + private readonly string _analyzerName; + private readonly FieldDefinition _field; + + public AnalyzerSearchPathDefinition(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 MultiSearchPathDefinition : SearchPathDefinition + { + private readonly FieldDefinition[] _fields; + + public MultiSearchPathDefinition(IEnumerable> fields) + { + _fields = Ensure.IsNotNull(fields, nameof(fields)).ToArray(); + } + + public override BsonValue Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new BsonArray(_fields.Select(field => field.Render(documentSerializer, serializerRegistry).FieldName)); + } + + internal sealed class SingleSearchPathDefinition : SearchPathDefinition + { + private readonly FieldDefinition _field; + + public SingleSearchPathDefinition(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 WildcardSearchPathDefinition : SearchPathDefinition + { + private readonly string _query; + + public WildcardSearchPathDefinition(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/SearchQueryDefinition.cs b/src/MongoDB.Driver/Search/SearchQueryDefinition.cs new file mode 100644 index 00000000000..765fb4264db --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchQueryDefinition.cs @@ -0,0 +1,104 @@ +/* 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.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// Base class for search queries. + /// + public abstract class SearchQueryDefinition + { + /// + /// 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 SearchQueryDefinition(string query) => + new SingleSearchQueryDefinition(query); + + /// + /// Performs an implicit conversion from an array of strings to . + /// + /// The array of strings. + /// + /// The result of the conversion. + /// + public static implicit operator SearchQueryDefinition(string[] queries) => + new MultiSearchQueryDefinition(queries); + + /// + /// Performs an implicit conversion from a list of strings to . + /// + /// The list of strings. + /// + /// The result of the conversion. + /// + public static implicit operator SearchQueryDefinition(List queries) => + new MultiSearchQueryDefinition(queries); + } + + /// + /// A query definition for a single string. + /// + public sealed class SingleSearchQueryDefinition : SearchQueryDefinition + { + private readonly string _query; + + /// + /// Initializes a new instance of the class. + /// + /// The query string. + public SingleSearchQueryDefinition(string query) + { + _query = Ensure.IsNotNull(query, nameof(query)); + } + + /// + public override BsonValue Render() => new BsonString(_query); + } + + /// + /// A query definition for multiple strings. + /// + public sealed class MultiSearchQueryDefinition : SearchQueryDefinition + { + private readonly string[] _queries; + + /// + /// Initializes a new instance of the class. + /// + /// The query strings. + public MultiSearchQueryDefinition(IEnumerable queries) + { + _queries = Ensure.IsNotNull(queries, nameof(queries)).ToArray(); + } + + /// + public override BsonValue Render() => new BsonArray(_queries); + } +} diff --git a/src/MongoDB.Driver/Search/SearchRange.cs b/src/MongoDB.Driver/Search/SearchRange.cs new file mode 100644 index 00000000000..e31ac93f941 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchRange.cs @@ -0,0 +1,138 @@ +/* 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 MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// Object that specifies range of scalar and DateTime values. + /// + /// The type of the range value. + public struct SearchRange where TValue : struct, IComparable + { + #region static + /// Empty range. + public static SearchRange Empty { get; } = new(default, default, default, default); + #endregion + + /// + /// Initializes a new instance of the class. + /// + /// The lower bound of the range. + /// The upper bound of the range + /// Indicates whether the lower bound of the range is inclusive. + /// Indicates whether the upper bound of the range is inclusive. + public SearchRange(TValue? min, TValue? max, bool isMinInclusive, bool isMaxInclusive) + { + if (min != null && max != null) + { + Ensure.IsGreaterThanOrEqualTo(max.Value, min.Value, nameof(max)); + } + + Min = min; + Max = max; + IsMinInclusive = isMinInclusive; + IsMaxInclusive = isMaxInclusive; + } + + /// Gets the value that indicates whether the upper bound of the range is inclusive. + public bool IsMaxInclusive { get; } + + /// Gets the value that indicates whether the lower bound of the range is inclusive. + public bool IsMinInclusive { get; } + + /// Gets the lower bound of the range. + public TValue? Max { get; } + + /// Gets the lower bound of the range. + public TValue? Min { get; } + } + + /// + /// A builder for a SearchRange. + /// + public static class SearchRangeBuilder + { + /// + /// Creates a greater than search range. + /// + /// The value. + /// Search range. + public static SearchRange Gt(TValue value) where TValue : struct, IComparable + => SearchRange.Empty.Gt(value); + + /// + /// Adds a greater than value to a search range. + /// + /// Search range. + /// The value. + /// Search range. + public static SearchRange Gt(this SearchRange searchRange, TValue value) where TValue : struct, IComparable + => new(min: value, searchRange.Max, isMinInclusive: false, searchRange.IsMaxInclusive); + + /// + /// Creates a greater or equal than search range. + /// + /// The value. + /// Search range. + public static SearchRange Gte(TValue value) where TValue : struct, IComparable + => SearchRange.Empty.Gte(value); + + /// + /// Adds a greater or equal than value to a search range. + /// + /// Search range. + /// The value. + /// Search range. + public static SearchRange Gte(this SearchRange searchRange, TValue value) where TValue : struct, IComparable + => new(min: value, searchRange.Max, isMinInclusive: true, searchRange.IsMaxInclusive); + + /// + /// Creates a less than search range. + /// + /// The value. + /// Search range. + public static SearchRange Lt(TValue value) where TValue : struct, IComparable + => SearchRange.Empty.Lt(value); + + /// + /// Adds a less than value to a search range. + /// + /// Search range. + /// The value. + /// Search range. + public static SearchRange Lt(this SearchRange searchRange, TValue value) where TValue : struct, IComparable + => new(searchRange.Min, max: value, searchRange.IsMinInclusive, isMaxInclusive: false); + + /// + /// Creates a less than or equal search range. + /// + /// The value. + /// search range. + public static SearchRange Lte(TValue value) where TValue : struct, IComparable + => SearchRange.Empty.Lte(value); + + /// + /// Adds a less than or equal value to a search range. + /// + /// Search range. + /// The value. + /// search range. + public static SearchRange Lte(this SearchRange searchRange, TValue value) where TValue : struct, IComparable + => new(searchRange.Min, max: value, searchRange.IsMinInclusive, isMaxInclusive: true); + } +} diff --git a/src/MongoDB.Driver/Search/SearchScoreDefinition.cs b/src/MongoDB.Driver/Search/SearchScoreDefinition.cs new file mode 100644 index 00000000000..5e94b79633e --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchScoreDefinition.cs @@ -0,0 +1,35 @@ +/* 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 SearchScoreDefinition + { + /// + /// 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/SearchScoreDefinitionBuilder.cs b/src/MongoDB.Driver/Search/SearchScoreDefinitionBuilder.cs new file mode 100644 index 00000000000..cfcd33036a5 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchScoreDefinitionBuilder.cs @@ -0,0 +1,150 @@ +/* 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 SearchScoreDefinitionBuilder + { + /// + /// 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 SearchScoreDefinition Boost(double value) => + new BoostValueSearchScoreDefinition(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 SearchScoreDefinition Boost(SearchPathDefinition path, double undefined = 0) => + new BoostPathSearchScoreDefinition(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 SearchScoreDefinition 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 SearchScoreDefinition Constant(double value) => + new ConstantSearchScoreDefinition(value); + + /// + /// Creates a score modifier that computes the final score through an expression. + /// + /// The expression used to compute the score. + /// + /// A function score modifier. + /// + public SearchScoreDefinition Function(SearchScoreFunction function) => + new FunctionSearchScoreDefinition(function); + } + + internal sealed class BoostPathSearchScoreDefinition : SearchScoreDefinition + { + private readonly SearchPathDefinition _path; + private readonly double _undefined; + + public BoostPathSearchScoreDefinition(SearchPathDefinition path, double undefined) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _undefined = undefined; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new("boost", new BsonDocument + { + { "path", _path.Render(documentSerializer, serializerRegistry) }, + { "undefined", _undefined, _undefined != 0 } + }); + } + + internal sealed class BoostValueSearchScoreDefinition : SearchScoreDefinition + { + private readonly double _value; + + public BoostValueSearchScoreDefinition(double value) + { + _value = Ensure.IsGreaterThanZero(value, nameof(value)); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new("boost", new BsonDocument("value", _value)); + } + + internal sealed class ConstantSearchScoreDefinition : SearchScoreDefinition + { + private readonly double _value; + + public ConstantSearchScoreDefinition(double value) + { + _value = Ensure.IsGreaterThanZero(value, nameof(value)); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new("constant", new BsonDocument("value", _value)); + } + + internal sealed class FunctionSearchScoreDefinition : SearchScoreDefinition + { + private readonly SearchScoreFunction _function; + + public FunctionSearchScoreDefinition(SearchScoreFunction function) + { + _function = Ensure.IsNotNull(function, nameof(function)); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new("function", _function.Render(documentSerializer, serializerRegistry)); + } +} diff --git a/src/MongoDB.Driver/Search/SearchScoreFunction.cs b/src/MongoDB.Driver/Search/SearchScoreFunction.cs new file mode 100644 index 00000000000..e77ac9d153e --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchScoreFunction.cs @@ -0,0 +1,35 @@ +/* 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 functions. + /// + /// The type of the document. + public abstract class SearchScoreFunction + { + /// + /// Renders the score function to a . + /// + /// The document serializer. + /// The serializer registry. + /// A . + public abstract BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry); + } +} diff --git a/src/MongoDB.Driver/Search/SearchScoreFunctionBuilder.cs b/src/MongoDB.Driver/Search/SearchScoreFunctionBuilder.cs new file mode 100644 index 00000000000..10d08cf3fb3 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchScoreFunctionBuilder.cs @@ -0,0 +1,276 @@ +/* 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 score function. + /// + /// The type of the document. + public sealed class SearchScoreFunctionBuilder + { + /// + /// Creates a function that adds a series of numbers. + /// + /// An array of expressions, which can have negative values. + /// An addition score function. + public SearchScoreFunction Add(IEnumerable> operands) => + new ArithmeticSearchScoreFunction("add", operands); + + /// + /// Creates a function that adds a series of numbers. + /// + /// An array of expressions, which can have negative values. + /// An addition score function. + public SearchScoreFunction Add(params SearchScoreFunction[] operands) => + Add((IEnumerable>)operands); + + /// + /// Creates a function that represents a constant number. + /// + /// Number that indicates a fixed value. + /// A constant score function. + public SearchScoreFunction Constant(double value) => + new ConstantSearchScoreFunction(value); + + /// + /// Creates a function that decays, or reduces by multiplying, the final scores of the + /// documents based on the distance of a numeric field from a specified origin point. + /// + /// The path to the numeric field. + /// The point of origin from which to calculate the distance. + /// + /// The distance from plus or minus at + /// which scores must be multiplied. + /// + /// + /// The rate at which to multiply score values, which must be a positive number between + /// 0 and 1 exclusive. + /// + /// + /// The number of use to determine the distance from . + /// + /// A Guassian score function. + public SearchScoreFunction Gauss( + SearchPathDefinition path, + double origin, + double scale, + double decay = 0.5, + double offset = 0) + => new GaussSearchScoreFunction(path, origin, scale, decay, offset); + + /// + /// Creates a function that decays, or reduces by multiplying, the final scores of the + /// documents based on the distance of a numeric field from a specified origin point. + /// + /// The path to the numeric field. + /// The point of origin from which to calculate the distance. + /// + /// The distance from plus or minus at + /// which scores must be multiplied. + /// + /// + /// The rate at which to multiply score values, which must be a positive number between + /// 0 and 1 exclusive. + /// + /// + /// The number of use to determine the distance from . + /// + /// A Guassian score function. + public SearchScoreFunction Gauss( + Expression> path, + double origin, + double scale, + double decay = 0.5, + double offset = 0) + => Gauss(new ExpressionFieldDefinition(path), origin, scale, decay, offset); + + /// + /// Creates a function that calculates the base-10 logarithm of a number. + /// + /// The number. + /// A logarithmic score function. + public SearchScoreFunction Log(SearchScoreFunction operand) => + new UnarySearchScoreFunction("log", operand); + + /// + /// Creates a function that adds 1 to a number and then calculates its base-10 logarithm. + /// + /// The number. + /// A logarithmic score function. + public SearchScoreFunction Log1p(SearchScoreFunction operand) => + new UnarySearchScoreFunction("log1p", operand); + + /// + /// Creates a function that multiplies a series of numbers. + /// + /// An array of expressions, which can have negative values. + /// A multiplication score function. + public SearchScoreFunction Multiply(IEnumerable> operands) => + new ArithmeticSearchScoreFunction("multiply", operands); + + /// + /// Creates a function that multiplies a series of numbers. + /// + /// An array of expressions, which can have negative values. + /// A mulitplication score function. + public SearchScoreFunction Multiply(params SearchScoreFunction[] operands) => + Multiply((IEnumerable>)operands); + + /// + /// Creates a function that incorporates an indexed numeric field value into the score. + /// + /// The path to the numeric field. + /// + /// The value to use if the numeric field specified using is + /// missing in the document. + /// + /// A path score function. + public SearchScoreFunction Path(SearchPathDefinition path, double undefined = 0) => + new PathSearchScoreFunction(path, undefined); + + /// + /// Creates a function that incorporates an indexed numeric field value into the score. + /// + /// The path to the numeric field. + /// + /// The value to use if the numeric field specified using is + /// missing in the document. + /// + /// A path score function. + public SearchScoreFunction Path(Expression> path, double undefined = 0) => + Path(new ExpressionFieldDefinition(path), undefined); + + /// + /// Creates a function that represents the relevance score, which is the score Atlas Search + /// assigns documents based on relevance. + /// + /// A relevance score function. + public SearchScoreFunction Relevance() => new RelevanceSearchScoreFunction(); + } + + internal sealed class ArithmeticSearchScoreFunction : SearchScoreFunction + { + private readonly SearchScoreFunction[] _operands; + private readonly string _operatorName; + + public ArithmeticSearchScoreFunction(string operatorName, IEnumerable> operands) + { + _operatorName = operatorName; + _operands = Ensure.IsNotNull(operands, nameof(operands)).ToArray(); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegister) => + new(_operatorName, new BsonArray(_operands.Select(o => o.Render(documentSerializer, serializerRegister)))); + } + + internal sealed class ConstantSearchScoreFunction : SearchScoreFunction + { + private readonly double _value; + + public ConstantSearchScoreFunction(double value) + { + _value = value; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegister) => + new("constant", _value); + } + + internal sealed class GaussSearchScoreFunction : SearchScoreFunction + { + private readonly double _decay; + private readonly double _offset; + private readonly double _origin; + private readonly SearchPathDefinition _path; + private readonly double _scale; + + public GaussSearchScoreFunction( + SearchPathDefinition path, + double origin, + double scale, + double decay, + double offset) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _origin = origin; + _scale = scale; + _decay = Ensure.IsBetween(decay, 0, 1, nameof(decay)); + _offset = offset; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegister) => + new("gauss", new BsonDocument() + { + { "path", _path.Render(documentSerializer, serializerRegister) }, + { "origin", _origin }, + { "scale", _scale }, + { "decay", _decay, _decay != 0.5 }, + { "offset", _offset, _offset != 0 }, + }); + } + + internal sealed class PathSearchScoreFunction : SearchScoreFunction + { + private readonly SearchPathDefinition _path; + private readonly double _undefined; + + public PathSearchScoreFunction(SearchPathDefinition path, double undefined) + { + _path = Ensure.IsNotNull(path, nameof(path)); + _undefined = undefined; + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegister) + { + var renderedPath = _path.Render(documentSerializer, serializerRegister); + var pathDocument = _undefined == 0 ? renderedPath : new BsonDocument() + { + { "value", renderedPath }, + { "undefined", _undefined } + }; + + return new("path", pathDocument); + } + } + + internal sealed class RelevanceSearchScoreFunction : SearchScoreFunction + { + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegister) => + new("score", "relevance"); + } + + internal sealed class UnarySearchScoreFunction : SearchScoreFunction + { + private readonly SearchScoreFunction _operand; + private readonly string _operatorName; + public UnarySearchScoreFunction(string operatorName, SearchScoreFunction operand) + { + _operatorName = operatorName; + _operand = Ensure.IsNotNull(operand, nameof(operand)); + } + + public override BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegister) => + new(_operatorName, _operand.Render(documentSerializer, serializerRegister)); + } +} diff --git a/src/MongoDB.Driver/Search/SearchSpanDefinition.cs b/src/MongoDB.Driver/Search/SearchSpanDefinition.cs new file mode 100644 index 00000000000..59fa5de3c8f --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchSpanDefinition.cs @@ -0,0 +1,51 @@ +/* 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 span clauses. + /// + /// + public abstract class SearchSpanDefinition + { + private protected enum ClauseType + { + First, + Near, + Or, + Subtract, + Term + } + + private readonly ClauseType _clauseType; + + private protected SearchSpanDefinition(ClauseType clauseType) => _clauseType = clauseType; + + /// + /// Renders the span clause to a . + /// + /// The document serializer. + /// The serializer registry. + /// A . + public BsonDocument Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new(_clauseType.ToCamelCase(), RenderClause(documentSerializer, serializerRegistry)); + + private protected virtual BsonDocument RenderClause(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => new(); + } +} diff --git a/src/MongoDB.Driver/Search/SearchSpanDefinitionBuilder.cs b/src/MongoDB.Driver/Search/SearchSpanDefinitionBuilder.cs new file mode 100644 index 00000000000..93a6c65a542 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchSpanDefinitionBuilder.cs @@ -0,0 +1,199 @@ +/* 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; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Search +{ + /// + /// A builder for a span clause. + /// + /// The type of the document. + public sealed class SearchSpanDefinitionBuilder + { + /// + /// Creates a span clause that matches near the beginning of the string. + /// + /// The span operator. + /// The highest position in which to match the query. + /// A first span clause. + public SearchSpanDefinition First(SearchSpanDefinition @operator, int endPositionLte) => + new FirstSearchSpanDefinition(@operator, endPositionLte); + + /// + /// Creates a span clause that matches multiple string found near each other. + /// + /// The clauses. + /// The allowable distance between words in the query phrase. + /// Whether to require that the clauses appear in the specified order. + /// A near span clause. + public SearchSpanDefinition Near( + IEnumerable> clauses, + int slop, + bool inOrder = false) => + new NearSearchSpanDefinition(clauses, slop, inOrder); + + /// + /// Creates a span clause that matches any of its subclauses. + /// + /// The clauses. + /// An or span clause. + public SearchSpanDefinition Or(IEnumerable> clauses) => + new OrSearchSpanDefinition(clauses); + + /// + /// Creates a span clause that matches any of its subclauses. + /// + /// The clauses. + /// An or span clause. + public SearchSpanDefinition Or(params SearchSpanDefinition[] clauses) => + Or((IEnumerable>)clauses); + + /// + /// Creates a span clause that excludes certain strings from the search results. + /// + /// Clause to be included. + /// Clause to be excluded. + /// A subtract span clause. + public SearchSpanDefinition Subtract( + SearchSpanDefinition include, + SearchSpanDefinition exclude) => + new SubtractSearchSpanDefinition(include, exclude); + + /// + /// Creates a span clause that matches a single term. + /// + /// The indexed field or fields to search. + /// The string or strings to search for. + /// A term span clause. + public SearchSpanDefinition Term(SearchPathDefinition path, SearchQueryDefinition query) => + new TermSearchSpanDefinition(path, query); + + /// + /// Creates a span clause that matches a single term. + /// + /// The type of the field. + /// The indexed field or fields to search. + /// The string or string to search for. + /// A term span clause. + public SearchSpanDefinition Term( + Expression> path, + SearchQueryDefinition query) => + Term(new ExpressionFieldDefinition(path), query); + } + + internal sealed class FirstSearchSpanDefinition : SearchSpanDefinition + { + private readonly int _endPositionLte; + private readonly SearchSpanDefinition _operator; + + public FirstSearchSpanDefinition(SearchSpanDefinition @operator, int endPositionLte) + : base(ClauseType.First) + { + _operator = Ensure.IsNotNull(@operator, nameof(@operator)); + _endPositionLte = endPositionLte; + } + private protected override BsonDocument RenderClause(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "operator", _operator.Render(documentSerializer, serializerRegistry) }, + { "endPositionLte", _endPositionLte } + }; + } + + internal sealed class NearSearchSpanDefinition : SearchSpanDefinition + { + private readonly List> _clauses; + private readonly bool _inOrder; + private readonly int _slop; + + public NearSearchSpanDefinition(IEnumerable> clauses, int slop, bool inOrder) + : base(ClauseType.Near) + { + _clauses = Ensure.IsNotNull(clauses, nameof(clauses)).ToList(); + _slop = slop; + _inOrder = inOrder; + } + + private protected override BsonDocument RenderClause(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "clauses", new BsonArray(_clauses.Select(clause => clause.Render(documentSerializer, serializerRegistry))) }, + { "slop", _slop }, + { "inOrder", _inOrder }, + }; + } + + internal sealed class OrSearchSpanDefinition : SearchSpanDefinition + { + private readonly List> _clauses; + + public OrSearchSpanDefinition(IEnumerable> clauses) + : base(ClauseType.Or) + { + _clauses = Ensure.IsNotNull(clauses, nameof(clauses)).ToList(); + } + + private protected override BsonDocument RenderClause(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new("clauses", new BsonArray(_clauses.Select(clause => clause.Render(documentSerializer, serializerRegistry)))); + } + + internal sealed class SubtractSearchSpanDefinition : SearchSpanDefinition + { + private readonly SearchSpanDefinition _exclude; + private readonly SearchSpanDefinition _include; + + public SubtractSearchSpanDefinition(SearchSpanDefinition include, SearchSpanDefinition exclude) + : base(ClauseType.Subtract) + { + _include = Ensure.IsNotNull(include, nameof(include)); + _exclude = Ensure.IsNotNull(exclude, nameof(exclude)); + } + + private protected override BsonDocument RenderClause(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "include", _include.Render(documentSerializer, serializerRegistry) }, + { "exclude", _exclude.Render(documentSerializer, serializerRegistry) }, + }; + } + + internal sealed class TermSearchSpanDefinition : SearchSpanDefinition + { + private readonly SearchPathDefinition _path; + private readonly SearchQueryDefinition _query; + + public TermSearchSpanDefinition(SearchPathDefinition path, SearchQueryDefinition query) + : base(ClauseType.Term) + { + _query = Ensure.IsNotNull(query, nameof(query)); + _path = Ensure.IsNotNull(path, nameof(path)); + } + + private protected override BsonDocument RenderClause(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry) => + new() + { + { "query", _query.Render() }, + { "path", _path.Render(documentSerializer, serializerRegistry) }, + }; + } +} diff --git a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs index 90a25603231..a393565a60d 100644 --- a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs @@ -20,6 +20,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.Search; using Moq; using Xunit; @@ -117,6 +118,156 @@ public void Merge_should_add_expected_stage() stages[0].Should().Be("{ $merge : { into : { db : 'database', coll : 'collection' } } }"); } + [Fact] + public void Search_should_add_expected_stage() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + + var result = pipeline.Search(builder.Text("bar", "foo")); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $search: { text: { query: 'foo', path: 'bar' } } }"); + } + + [Fact] + public void Search_should_add_expected_stage_with_highlight() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + + var result = pipeline.Search(builder.Text("bar", "foo"), new SearchHighlightOptions("foo")); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().BeEquivalentTo("{ $search: { text: { query: 'foo', path: 'bar' }, highlight: { path: 'foo' } } }"); + } + + [Fact] + public void Search_should_add_expected_stage_with_index() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + + var result = pipeline.Search(builder.Text("bar", "foo"), indexName: "foo"); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $search: { text: { query: 'foo', path: 'bar' }, index: 'foo' } }"); + } + + [Fact] + public void Search_should_add_expected_stage_with_count() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + var count = new SearchCountOptions() + { + Type = SearchCountType.Total + }; + + var result = pipeline.Search(builder.Text("bar", "foo"), count: count); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $search: { text: { query: 'foo', path: 'bar' }, count: { type: 'total' } } }"); + } + + [Fact] + public void Search_should_add_expected_stage_with_return_stored_source() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + + var result = pipeline.Search(builder.Text("bar", "foo"), returnStoredSource: true); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $search: { text: { query: 'foo', path: 'bar' }, returnStoredSource: true } }"); + } + + [Fact] + public void Search_should_throw_when_pipeline_is_null() + { + PipelineDefinition pipeline = null; + var builder = new SearchDefinitionBuilder(); + + var exception = Record.Exception(() => pipeline.Search(builder.Text("bar", "foo"))); + + exception.Should().BeOfType() + .Which.ParamName.Should().Be("pipeline"); + } + + [Fact] + public void Search_should_throw_when_query_is_null() + { + var pipeline = new EmptyPipelineDefinition(); + + var exception = Record.Exception(() => pipeline.Search(null)); + + exception.Should().BeOfType() + .Which.ParamName.Should().Be("searchDefinition"); + } + + [Fact] + public void SearchMeta_should_add_expected_stage() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + + var result = pipeline.SearchMeta(builder.Text("bar", "foo")); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $searchMeta: { text: { query: 'foo', path: 'bar' } } }"); + } + + [Fact] + public void SearchMeta_should_add_expected_stage_with_index() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + + var result = pipeline.SearchMeta(builder.Text("bar", "foo"), indexName: "foo"); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $searchMeta: { text: { query: 'foo', path: 'bar' }, index: 'foo' } }"); + } + + [Fact] + public void SearchMeta_should_add_expected_stage_with_count() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + var count = new SearchCountOptions() + { + Type = SearchCountType.Total + }; + + var result = pipeline.SearchMeta(builder.Text("bar", "foo"), count: count); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $searchMeta: { text: { query: 'foo', path: 'bar' }, count: { type: 'total' } } }"); + } + + [Fact] + public void SearchMeta_should_throw_when_pipeline_is_null() + { + PipelineDefinition pipeline = null; + var builder = new SearchDefinitionBuilder(); + + var exception = Record.Exception(() => pipeline.SearchMeta(builder.Text("bar", "foo"))); + + exception.Should().BeOfType() + .Which.ParamName.Should().Be("pipeline"); + } + + [Fact] + public void SearchMeta_should_throw_when_query_is_null() + { + var pipeline = new EmptyPipelineDefinition(); + + var exception = Record.Exception(() => pipeline.SearchMeta(null)); + + exception.Should().BeOfType() + .Which.ParamName.Should().Be("searchDefinition"); + } + [Fact] public void UnionWith_should_add_expected_stage() { diff --git a/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs new file mode 100644 index 00000000000..18a4b69a40c --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs @@ -0,0 +1,518 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.Logging; +using MongoDB.Driver.GeoJsonObjectModel; +using MongoDB.Driver.Search; +using MongoDB.Driver.TestHelpers; +using MongoDB.TestHelpers.XunitExtensions; +using Xunit; +using Xunit.Abstractions; +using Builders = MongoDB.Driver.Builders; +using GeoBuilders = MongoDB.Driver.Builders; + +namespace MongoDB.Driver.Tests.Search +{ + [Trait("Category", "AtlasSearch")] + public class AtlasSearchTests : LoggableTestClass + { + #region static + + private static readonly GeoJsonPolygon __testPolygon = + new(new(new(new GeoJson2DGeographicCoordinates[] + { + new(-8.6131, 41.14), + new(-8.6131, 41.145), + new(-8.60308, 41.145), + new(-8.60308, 41.14), + new(-8.6131, 41.14), + }))); + + private static readonly GeoWithinBox __testBox = + new(new(new(-8.6131, 41.14)), new(new(-8.60308, 41.145))); + + private static readonly GeoWithinCircle __testCircle = + new(new(new(-8.61308, 41.1413)), 273); + + #endregion + + private readonly DisposableMongoClient _disposableMongoClient; + + public AtlasSearchTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + RequireEnvironment.Check().EnvironmentVariable("ATLAS_SEARCH_TESTS_ENABLED"); + + var atlasSearchUri = Environment.GetEnvironmentVariable("ATLAS_SEARCH"); + Ensure.IsNotNullOrEmpty(atlasSearchUri, nameof(atlasSearchUri)); + + _disposableMongoClient = new(new MongoClient(atlasSearchUri), CreateLogger()); + } + + protected override void DisposeInternal() => _disposableMongoClient.Dispose(); + + [Fact] + public void Autocomplete() + { + var result = SearchSingle(Builders.Search.Autocomplete(x => x.Title, "Declaration of Ind")); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void Compound() + { + var result = SearchSingle(Builders.Search.Compound() + .Must(Builders.Search.Text(x => x.Body, "life"), Builders.Search.Text(x => x.Body, "liberty")) + .MustNot(Builders.Search.Text(x => x.Body, "property")) + .Must(Builders.Search.Text(x => x.Body, "pursuit of happiness"))); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void Count_total() + { + var results = GetTestCollection().Aggregate() + .Search( + Builders.Search.Phrase(x => x.Body, "life, liberty, and the pursuit of happiness"), + count: new SearchCountOptions() + { + Type = SearchCountType.Total + }) + .Project(Builders.Projection.SearchMeta(x => x.MetaResult)) + .Limit(1) + .ToList(); + results.Should().ContainSingle().Which.MetaResult.Count.Total.Should().Be(108); + } + + [Fact] + public void Exists() + { + var result = SearchSingle( + Builders.Search.Compound().Must( + Builders.Search.Text(x => x.Body, "life, liberty, and the pursuit of happiness"), + Builders.Search.Exists(x => x.Title))); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void Filter() + { + var result = SearchSingle( + Builders.Search.Compound().Filter( + Builders.Search.Phrase(x => x.Body, "life, liberty"), + Builders.Search.Wildcard(x => x.Body, "happ*", true))); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Theory] + [InlineData("add")] + [InlineData("constant")] + [InlineData("gauss")] + [InlineData("log")] + [InlineData("log1p")] + [InlineData("multiply")] + [InlineData("path")] + [InlineData("relevance")] + public void FunctionScore(string functionScoreType) + { + var scoreFunction = functionScoreType switch + { + "add" => Builders.SearchScoreFunction.Add(Constant(1), Constant(2)), + "constant" => Constant(1), + "gauss" => Builders.SearchScoreFunction.Gauss(x => x.Score, 100, 1, 0.1, 1), + "log" => Builders.SearchScoreFunction.Log(Constant(1)), + "log1p" => Builders.SearchScoreFunction.Log1p(Constant(1)), + "multiply" => Builders.SearchScoreFunction.Multiply(Constant(1), Constant(2)), + "path" => Builders.SearchScoreFunction.Path(x => x.Score, 1), + "relevance" => Builders.SearchScoreFunction.Relevance(), + _ => throw new ArgumentOutOfRangeException(nameof(functionScoreType), functionScoreType, "Invalid score function") + }; + + var result = SearchSingle(Builders.Search.Phrase( + x => x.Body, + "life, liberty, and the pursuit of happiness", + score: Builders.SearchScore.Function(scoreFunction))); + + result.Title.Should().Be("Declaration of Independence"); + + SearchScoreFunction Constant(double value) => + Builders.SearchScoreFunction.Constant(value); + } + + [Fact] + public void GeoShape() + { + var results = GeoSearch( + GeoBuilders.Search.GeoShape( + x => x.Address.Location, + GeoShapeRelation.Intersects, + __testPolygon)); + + results.Count.Should().Be(25); + results.First().Name.Should().Be("Ribeira Charming Duplex"); + } + + [Theory] + [InlineData("box")] + [InlineData("circle")] + [InlineData("polygon")] + public void GeoWithin(string geometryType) + { + GeoWithinArea geoArea = geometryType switch + { + "box" => __testBox, + "circle" => __testCircle, + "polygon" => new GeoWithinGeometry(__testPolygon), + _ => throw new ArgumentOutOfRangeException(nameof(geometryType), geometryType, "Invalid geometry type") + }; + + var results = GeoSearch(GeoBuilders.Search.GeoWithin(x => x.Address.Location, geoArea)); + + results.Count.Should().Be(25); + results.First().Name.Should().Be("Ribeira Charming Duplex"); + } + + [Fact] + public void MoreLikeThis() + { + var likeThisDocument = new HistoricalDocument + { + Title = "Declaration of Independence", + Body = "We hold these truths to be self-evident that all men are created equal..." + }; + var result = SearchSingle(Builders.Search.MoreLikeThis(likeThisDocument)); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void Must() + { + var result = SearchSingle( + Builders.Search.Compound().Must( + Builders.Search.Phrase(x => x.Body, "life, liberty"), + Builders.Search.Wildcard(x => x.Body, "happ*", true))); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void MustNot() + { + var result = SearchSingle( + Builders.Search.Compound().MustNot( + Builders.Search.Phrase(x => x.Body, "life, liberty"))); + result.Title.Should().Be("US Constitution"); + } + + [Fact] + public void Near() + { + var results = GetGeoTestCollection().Aggregate() + .Search(GeoBuilders.Search.Near(x => x.Address.Location, __testCircle.Center, 1000)) + .Limit(1) + .ToList(); + + results.Should().ContainSingle().Which.Name.Should().Be("Ribeira Charming Duplex"); + } + + [Fact] + public void Phrase() + { + // This test case exercises the indexName and returnStoredSource arguments. The + // remaining test cases omit them. + var coll = GetTestCollection(); + var results = GetTestCollection().Aggregate() + .Search(Builders.Search.Phrase(x => x.Body, "life, liberty, and the pursuit of happiness"), + new SearchHighlightOptions(x => x.Body), + indexName: "default", + returnStoredSource: true) + .Limit(1) + .Project(Builders.Projection + .Include(x => x.Title) + .Include(x => x.Body) + .MetaSearchScore("score") + .MetaSearchHighlights("highlights")) + .ToList(); + + var result = results.Should().ContainSingle().Subject; + result.Title.Should().Be("Declaration of Independence"); + result.Score.Should().NotBe(0); + + var highlightTexts = result.Highlights.Should().ContainSingle().Subject.Texts; + highlightTexts.Should().HaveCount(15); + + foreach (var highlight in highlightTexts) + { + var expectedType = char.IsLetter(highlight.Value[0]) ? HighlightTextType.Hit : HighlightTextType.Text; + highlight.Type.Should().Be(expectedType); + } + + var highlightRangeStr = string.Join(string.Empty, highlightTexts.Skip(1).Select(x => x.Value)); + highlightRangeStr.Should().Be("Life, Liberty and the pursuit of Happiness."); + } + + [Fact] + public void PhraseMultiPath() + { + var result = SearchSingle( + Builders.Search.Phrase( + Builders.SearchPath.Multi(x => x.Title, x => x.Body), + "life, liberty, and the pursuit of happiness")); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void PhraseAnalyzerPath() + { + var result = SearchSingle( + Builders.Search.Phrase( + Builders.SearchPath.Analyzer(x => x.Body, "english"), + "life, liberty, and the pursuit of happiness")); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void PhraseWildcardPath() + { + var result = SearchSingle( + Builders.Search.Phrase( + Builders.SearchPath.Wildcard("b*"), + "life, liberty, and the pursuit of happiness")); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void QueryString() + { + var result = SearchSingle(Builders.Search.QueryString(x => x.Body, "life, liberty, and the pursuit of happiness")); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void Range() + { + var results = GeoSearch( + GeoBuilders.Search.Compound().Must( + GeoBuilders.Search.Range(x => x.Bedrooms, SearchRangeBuilder.Gt(2).Lt(4)), + GeoBuilders.Search.Range(x => x.Beds, SearchRangeBuilder.Gte(14).Lte(14)))); + + results.Should().ContainSingle().Which.Name.Should().Be("House close to station & direct to opera house...."); + } + + [Fact] + public void Search_count_lowerBound() + { + var results = GetTestCollection().Aggregate() + .Search( + Builders.Search.Phrase(x => x.Body, "life, liberty, and the pursuit of happiness"), + count: new SearchCountOptions() + { + Type = SearchCountType.LowerBound, + Threshold = 128 + }) + .Project(Builders.Projection.SearchMeta(x => x.MetaResult)) + .Limit(1) + .ToList(); + results.Should().ContainSingle().Which.MetaResult.Count.LowerBound.Should().Be(108); + } + + [Fact] + public void SearchMeta_count() + { + var result = GetTestCollection().Aggregate() + .SearchMeta( + Builders.Search.Phrase(x => x.Body, "life, liberty, and the pursuit of happiness"), + "default", + new SearchCountOptions() { Type = SearchCountType.Total }) + .Single(); + + result.Should().NotBeNull(); + result.Count.Should().NotBeNull(); + result.Count.Total.Should().Be(108); + } + + [Fact] + public void SearchMeta_facet() + { + var result = GetTestCollection().Aggregate() + .SearchMeta(Builders.Search.Facet( + Builders.Search.Phrase(x => x.Body, "life, liberty, and the pursuit of happiness"), + Builders.SearchFacet.String("string", x => x.Author, 100), + Builders.SearchFacet.Number("number", x => x.Index, 0, 100), + Builders.SearchFacet.Date("date", x => x.Date, DateTime.MinValue, DateTime.MaxValue))) + .Single(); + + result.Should().NotBeNull(); + + var bucket = result.Facet["string"].Buckets.Should().NotBeNull().And.ContainSingle().Subject; + bucket.Id.Should().Be((BsonString)"machine"); + bucket.Count.Should().Be(108); + + bucket = result.Facet["number"].Buckets.Should().NotBeNull().And.ContainSingle().Subject; + bucket.Id.Should().Be((BsonInt32)0); + bucket.Count.Should().Be(0); + + bucket = result.Facet["date"].Buckets.Should().NotBeNull().And.ContainSingle().Subject; + bucket.Id.Should().Be((BsonDateTime)DateTime.MinValue); + bucket.Count.Should().Be(108); + } + + [Fact] + public void Should() + { + var result = SearchSingle( + Builders.Search.Compound().Should( + Builders.Search.Phrase(x => x.Body, "life, liberty"), + Builders.Search.Wildcard(x => x.Body, "happ*", true)) + .MinimumShouldMatch(2)); + result.Title.Should().Be("Declaration of Independence"); + } + + [Theory] + [InlineData("first")] + [InlineData("near")] + [InlineData("or")] + [InlineData("subtract")] + public void Span(string spanType) + { + var spanDefinition = spanType switch + { + "first" => Builders.SearchSpan.First(Term("happiness"), 250), + "near" => Builders.SearchSpan.Near(new[] { Term("life"), Term("liberty"), Term("pursuit"), Term("happiness") }, 3, true), + "or" => Builders.SearchSpan.Or(Term("unalienable"), Term("inalienable")), + "subtract" => Builders.SearchSpan.Subtract(Term("unalienable"), Term("inalienable")), + _ => throw new ArgumentOutOfRangeException(nameof(spanType), spanType, "Invalid span type") + }; + + var result = SearchSingle(Builders.Search.Span(spanDefinition)); + result.Title.Should().Be("Declaration of Independence"); + + SearchSpanDefinition Term(string term) => Builders.SearchSpan.Term(x => x.Body, term); + } + + [Fact] + public void Text() + { + var result = SearchSingle(Builders.Search.Text(x => x.Body, "life, liberty, and the pursuit of happiness")); + + result.Title.Should().Be("Declaration of Independence"); + } + + [Fact] + public void Wildcard() + { + var result = SearchSingle(Builders.Search.Wildcard(x => x.Body, "tranquil*", true)); + + result.Title.Should().Be("US Constitution"); + } + + private List GeoSearch(SearchDefinition searchDefintion) => + GetGeoTestCollection().Aggregate().Search(searchDefintion).ToList(); + + private HistoricalDocument SearchSingle(SearchDefinition searchDefintion) => + GetTestCollection() + .Aggregate() + .Search(searchDefintion) + .Limit(1) + .ToList() + .Single(); + + private IMongoCollection GetTestCollection() => _disposableMongoClient + .GetDatabase("sample_training") + .GetCollection("posts"); + + private IMongoCollection GetGeoTestCollection() => _disposableMongoClient + .GetDatabase("sample_airbnb") + .GetCollection("listingsAndReviews"); + + [BsonIgnoreExtraElements] + public class HistoricalDocument + { + [BsonId] + public ObjectId Id { get; set; } + + [BsonElement("body")] + public string Body { get; set; } + + [BsonElement("author")] + public string Author { get; set; } + + [BsonElement("title")] + public string Title { get; set; } + + [BsonElement("highlights")] + public List Highlights { get; set; } + + [BsonElement("score")] + public double Score { get; set; } + + [BsonElement("date")] + public DateTime Date { get; set; } + + [BsonElement("index")] + public int Index { get; set; } + + [BsonElement("metaResult")] + public SearchMetaResult MetaResult { get; set; } + } + + [BsonIgnoreExtraElements] + public class Address + { + [BsonElement("location")] + public GeoJsonObject Location { get; set; } + + [BsonElement("street")] + public string Street { get; set; } + } + + [BsonIgnoreExtraElements] + public class AirbnbListing + { + [BsonElement("address")] + public Address Address { get; set; } + + [BsonElement("bedrooms")] + public int Bedrooms { get; set; } + + [BsonElement("beds")] + public int Beds { get; set; } + + [BsonElement("description")] + public string Description { get; set; } + + [BsonElement("space")] + public string Space { get; set; } + + [BsonElement("name")] + public string Name { get; set; } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/MongoQueryableTests.cs b/tests/MongoDB.Driver.Tests/Search/MongoQueryableTests.cs new file mode 100644 index 00000000000..fc94e4d498b --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/MongoQueryableTests.cs @@ -0,0 +1,64 @@ +/* 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 FluentAssertions; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Linq; +using Xunit; + +namespace MongoDB.Driver.Tests.Search +{ + public class MongoQueryableTests + { + [Fact] + public void Search() + { + var subject = CreateSubject(); + + var query = subject + .Search(Builders.Search.Text(x => x.FirstName, "Alex")); + + query.ToString().Should().EndWith("Aggregate([{ \"$search\" : { \"text\" : { \"query\" : \"Alex\", \"path\" : \"fn\" } } }])"); + } + + [Fact] + public void SearchMeta() + { + var subject = CreateSubject(); + + var query = subject + .SearchMeta(Builders.Search.Text(x => x.FirstName, "Alex")); + + query.ToString().Should().EndWith("Aggregate([{ \"$searchMeta\" : { \"text\" : { \"query\" : \"Alex\", \"path\" : \"fn\" } } }])"); + } + + private IMongoQueryable CreateSubject() + { + var client = DriverTestConfiguration.Linq3Client; + var database = client.GetDatabase(DriverTestConfiguration.DatabaseNamespace.DatabaseName); + var collection = database.GetCollection(DriverTestConfiguration.CollectionNamespace.CollectionName); + return collection.AsQueryable(); + } + + private class Person + { + [BsonElement("fn")] + public string FirstName { get; set; } + + [BsonElement("ln")] + public string LastName { get; set; } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/ProjectionDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/ProjectionDefinitionBuilderTests.cs new file mode 100644 index 00000000000..d827ca1220f --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/ProjectionDefinitionBuilderTests.cs @@ -0,0 +1,63 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using Xunit; + +namespace MongoDB.Driver.Tests.Search +{ + public class ProjectionDefinitionBuilderTests + { + [Fact] + public void MetaSearchHighlights() + { + var subject = CreateSubject(); + + AssertRendered(subject.MetaSearchHighlights("a"), "{ a: { $meta: 'searchHighlights' } }"); + } + + [Fact] + public void MetaSearchScore() + { + var subject = CreateSubject(); + + AssertRendered(subject.MetaSearchScore("a"), "{ a : { $meta: 'searchScore' } }"); + } + + [Fact] + public void SearchMeta() + { + var subject = CreateSubject(); + + AssertRendered(subject.SearchMeta("a"), "{ a: '$$SEARCH_META' }"); + } + + private void AssertRendered(ProjectionDefinition projection, string expected) => + AssertRendered(projection, BsonDocument.Parse(expected)); + + private void AssertRendered(ProjectionDefinition projection, BsonDocument expected) + { + var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var renderedProjection = projection.Render(documentSerializer, BsonSerializer.SerializerRegistry); + + renderedProjection.Should().BeEquivalentTo(expected); + } + + private ProjectionDefinitionBuilder CreateSubject() => + new ProjectionDefinitionBuilder(); + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs new file mode 100644 index 00000000000..95d7e9bd377 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs @@ -0,0 +1,963 @@ +/* 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 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 + { + private static readonly GeoWithinBox __testBox = + new GeoWithinBox( + new GeoJsonPoint( + new GeoJson2DGeographicCoordinates(-161.323242, 22.065278)), + new GeoJsonPoint( + new GeoJson2DGeographicCoordinates(-152.446289, 22.512557))); + + private static readonly GeoWithinCircle __testCircle = + new GeoWithinCircle( + new GeoJsonPoint( + new GeoJson2DGeographicCoordinates(-161.323242, 22.512557)), + 7.5); + + private static readonly GeoJsonPolygon __testPolygon = + new GeoJsonPolygon( + new GeoJsonPolygonCoordinates( + new GeoJsonLinearRingCoordinates( + new List() + { + new GeoJson2DGeographicCoordinates(-161.323242, 22.512557), + new GeoJson2DGeographicCoordinates(-152.446289, 22.065278), + new GeoJson2DGeographicCoordinates(-156.09375, 17.811456), + new GeoJson2DGeographicCoordinates(-161.323242, 22.512557) + }))); + + [Fact] + public void Autocomplete() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Autocomplete("x", "foo"), + "{ autocomplete: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Autocomplete(new[] { "x", "y" }, "foo"), + "{ autocomplete: { query: 'foo', path: ['x', 'y'] } }"); + AssertRendered( + subject.Autocomplete("x", new[] { "foo", "bar" }), + "{ autocomplete: { query: ['foo', 'bar'], path: 'x' } }"); + AssertRendered( + subject.Autocomplete(new[] { "x", "y" }, new[] { "foo", "bar" }), + "{ autocomplete: { query: ['foo', 'bar'], path: ['x', 'y'] } }"); + + AssertRendered( + subject.Autocomplete("x", "foo", SearchAutocompleteTokenOrder.Any), + "{ autocomplete: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Autocomplete("x", "foo", SearchAutocompleteTokenOrder.Sequential), + "{ autocomplete: { query: 'foo', path: 'x', tokenOrder: 'sequential' } }"); + + AssertRendered( + subject.Autocomplete("x", "foo", fuzzy: new SearchFuzzyOptions()), + "{ autocomplete: { query: 'foo', path: 'x', fuzzy: {} } }"); + AssertRendered( + subject.Autocomplete("x", "foo", fuzzy: new SearchFuzzyOptions() + { + MaxEdits = 1, + PrefixLength = 5, + MaxExpansions = 25 + }), + "{ autocomplete: { query: 'foo', path: 'x', fuzzy: { maxEdits: 1, prefixLength: 5, maxExpansions: 25 } } }"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Autocomplete("x", "foo", score: scoreBuilder.Constant(1)), + "{ autocomplete: { query: 'foo', path: 'x', score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Autocomplete_typed() + { + var subject = CreateSubject(); + AssertRendered( + subject.Autocomplete(x => x.FirstName, "foo"), + "{ autocomplete: { query: 'foo', path: 'fn' } }"); + AssertRendered( + subject.Autocomplete("FirstName", "foo"), + "{ autocomplete: { query: 'foo', path: 'fn' } }"); + + AssertRendered( + subject.Autocomplete( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + "foo"), + "{ autocomplete: { query: 'foo', path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Autocomplete(new[] { "FirstName", "LastName" }, "foo"), + "{ autocomplete: { query: 'foo', path: ['fn', 'ln'] } }"); + + AssertRendered( + subject.Autocomplete(x => x.FirstName, new[] { "foo", "bar" }), + "{ autocomplete: { query: ['foo', 'bar'], path: 'fn' } }"); + AssertRendered( + subject.Autocomplete("FirstName", new[] { "foo", "bar" }), + "{ autocomplete: { query: ['foo', 'bar'], path: 'fn' } }"); + + AssertRendered( + subject.Autocomplete( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + new[] { "foo", "bar" }), + "{ autocomplete: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Autocomplete(new[] { "FirstName", "LastName" }, new[] { "foo", "bar" }), + "{ autocomplete: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + } + + [Fact] + public void Compound() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Compound() + .Must( + subject.Exists("x"), + subject.Exists("y")) + .MustNot( + subject.Exists("foo"), + subject.Exists("bar")) + .Must( + subject.Exists("z")), + "{ compound: { must: [{ exists: { path: 'x' } }, { exists: { path: 'y' } }, { exists: { path: 'z' } }], mustNot: [{ exists: { path: 'foo' } }, { exists: { path: 'bar' } }] } }"); + } + + [Fact] + public void Compound_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Compound() + .Must( + subject.Exists(p => p.Age), + subject.Exists(p => p.FirstName)) + .MustNot( + subject.Exists(p => p.Retired), + subject.Exists(p => p.Birthday)) + .Must( + subject.Exists(p => p.LastName)), + "{ compound: { must: [{ exists: { path: 'age' } }, { exists: { path: 'fn' } }, { exists: { path: 'ln' } }], mustNot: [{ exists: { path: 'ret' } }, { exists: { path: 'dob' } }] } }"); + } + + [Fact] + public void Equals() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Equals("x", true), + "{ equals: { path: 'x', value: true } }"); + AssertRendered( + subject.Equals("x", ObjectId.Empty), + "{ equals: { path: 'x', value: { $oid: '000000000000000000000000' } } }"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Equals("x", true, scoreBuilder.Constant(1)), + "{ equals: { path: 'x', value: true, score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Equals_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Equals(x => x.Retired, true), + "{ equals: { path: 'ret', value: true } }"); + AssertRendered( + subject.Equals("Retired", true), + "{ equals: { path: 'ret', value: true } }"); + + AssertRendered( + subject.Equals(x => x.Id, ObjectId.Empty), + "{ equals: { path: '_id', value: { $oid: '000000000000000000000000' } } }"); + } + + [Fact] + public void Exists() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Exists("x"), + "{ exists: { path: 'x' } }"); + } + + [Fact] + public void Exists_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Exists(x => x.FirstName), + "{ exists: { path: 'fn' } }"); + AssertRendered( + subject.Exists("FirstName"), + "{ exists: { path: 'fn' } }"); + } + + [Fact] + public void Facet() + { + var subject = CreateSubject(); + var facetBuilder = new SearchFacetBuilder(); + + AssertRendered( + subject.Facet( + subject.Phrase("x", "foo"), + facetBuilder.String("string", "y", 100)), + "{ facet: { operator: { phrase: { query: 'foo', path: 'x' } }, facets: { string: { type: 'string', path: 'y', numBuckets: 100 } } } }"); + } + + [Fact] + public void Facet_typed() + { + var subject = CreateSubject(); + var facetBuilder = new SearchFacetBuilder(); + + AssertRendered( + subject.Facet( + subject.Phrase(x => x.LastName, "foo"), + facetBuilder.String("string", x => x.FirstName, 100)), + "{ facet: { operator: { phrase: { query: 'foo', path: 'ln' } }, facets: { string: { type: 'string', path: 'fn', numBuckets: 100 } } } }"); + AssertRendered( + subject.Facet( + subject.Phrase("LastName", "foo"), + facetBuilder.String("string", "FirstName", 100)), + "{ facet: { operator: { phrase: { query: 'foo', path: 'ln' } }, facets: { string: { type: 'string', path: 'fn', numBuckets: 100 } } } }"); + } + + [Fact] + public void Filter() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Compound().Filter( + subject.Exists("x"), + subject.Exists("y")), + "{ compound: { filter: [{ exists: { path: 'x' } }, { exists: { path: 'y' } }] } }"); + } + + [Fact] + public void Filter_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Compound().Filter( + subject.Exists(p => p.Age), + subject.Exists(p => p.Birthday)), + "{ compound: { filter: [{ exists: { path: 'age' } }, { exists: { path: 'dob' } }] } }"); + } + + [Fact] + public void GeoShape() + { + var subject = CreateSubject(); + + AssertRendered( + subject.GeoShape( + "location", + GeoShapeRelation.Disjoint, + __testPolygon), + "{ geoShape: { geometry: { type: 'Polygon', coordinates: [[[-161.323242, 22.512557], [-152.446289, 22.065278], [-156.09375, 17.811456], [-161.323242, 22.512557]]] }, path: 'location', relation: 'disjoint' } }"); + } + + [Fact] + public void GeoShape_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.GeoShape( + x => x.Location, + GeoShapeRelation.Disjoint, + __testPolygon), + "{ geoShape: { geometry: { type: 'Polygon', coordinates: [[[-161.323242, 22.512557], [-152.446289, 22.065278], [-156.09375, 17.811456], [-161.323242, 22.512557]]] }, path: 'location', relation: 'disjoint' } }"); + AssertRendered( + subject.GeoShape( + "Location", + GeoShapeRelation.Disjoint, + __testPolygon), + "{ geoShape: { geometry: { type: 'Polygon', coordinates: [[[-161.323242, 22.512557], [-152.446289, 22.065278], [-156.09375, 17.811456], [-161.323242, 22.512557]]] }, path: 'location', relation: 'disjoint' } }"); + } + + [Fact] + public void GeoWithin() + { + var subject = CreateSubject(); + + AssertRendered( + subject.GeoWithin("location", __testPolygon), + "{ geoWithin: { geometry: { type: 'Polygon', coordinates: [[[-161.323242, 22.512557], [-152.446289, 22.065278], [-156.09375, 17.811456], [-161.323242, 22.512557]]] }, path: 'location' } }"); + AssertRendered( + subject.GeoWithin("location", __testBox), + "{ geoWithin: { box: { bottomLeft: { type: 'Point', coordinates: [-161.323242, 22.065278] }, topRight: { type: 'Point', coordinates: [-152.446289, 22.512557] } }, path: 'location' } }"); + AssertRendered( + subject.GeoWithin("location", __testCircle), + "{ geoWithin: { circle: { center: { type: 'Point', coordinates: [-161.323242, 22.512557] }, radius: 7.5 }, path: 'location' } }"); + } + + [Fact] + public void GeoWithin_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.GeoWithin(x => x.Location, __testPolygon), + "{ geoWithin: { geometry: { type: 'Polygon', coordinates: [[[-161.323242, 22.512557], [-152.446289, 22.065278], [-156.09375, 17.811456], [-161.323242, 22.512557]]] }, path: 'location' } }"); + AssertRendered( + subject.GeoWithin("Location", __testPolygon), + "{ geoWithin: { geometry: { type: 'Polygon', coordinates: [[[-161.323242, 22.512557], [-152.446289, 22.065278], [-156.09375, 17.811456], [-161.323242, 22.512557]]] }, path: 'location' } }"); + + AssertRendered( + subject.GeoWithin(x => x.Location, __testBox), + "{ geoWithin: { box: { bottomLeft: { type: 'Point', coordinates: [-161.323242, 22.065278] }, topRight: { type: 'Point', coordinates: [-152.446289, 22.512557] } }, path: 'location' } }"); + AssertRendered( + subject.GeoWithin("Location", __testBox), + "{ geoWithin: { box: { bottomLeft: { type: 'Point', coordinates: [-161.323242, 22.065278] }, topRight: { type: 'Point', coordinates: [-152.446289, 22.512557] } }, path: 'location' } }"); + + AssertRendered( + subject.GeoWithin(x => x.Location, __testCircle), + "{ geoWithin: { circle: { center: { type: 'Point', coordinates: [-161.323242, 22.512557] }, radius: 7.5 }, path: 'location' } }"); + AssertRendered( + subject.GeoWithin("Location", __testCircle), + "{ geoWithin: { circle: { center: { type: 'Point', coordinates: [-161.323242, 22.512557] }, radius: 7.5 }, path: 'location' } }"); + } + + [Fact] + public void MoreLikeThis() + { + var subject = CreateSubject(); + + AssertRendered( + subject.MoreLikeThis( + new BsonDocument("x", "foo"), + new BsonDocument("x", "bar")), + "{ moreLikeThis: { like: [{ x: 'foo' }, { x: 'bar' }] } }"); + + AssertRendered( + subject.MoreLikeThis( + new SimplePerson { FirstName = "John", LastName = "Doe" }, + new SimplePerson { FirstName = "Jane", LastName = "Doe" }), + "{ moreLikeThis: { like: [{ fn: 'John', ln: 'Doe' }, { fn: 'Jane', ln: 'Doe' }] } }"); + } + + [Fact] + public void MoreLikeThis_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.MoreLikeThis( + new SimplestPerson { FirstName = "John" }, + new SimplestPerson { FirstName = "Jane" }), + "{ moreLikeThis: { like: [{ fn: 'John' }, { fn: 'Jane' }] } }"); + + AssertRendered( + subject.MoreLikeThis( + new SimplePerson { FirstName = "John", LastName = "Doe" }, + new SimplePerson { FirstName = "Jane", LastName = "Doe" }), + "{ moreLikeThis: { like: [{ fn: 'John', ln: 'Doe' }, { fn: 'Jane', ln: 'Doe' }] } }"); + + AssertRendered( + subject.MoreLikeThis( + new BsonDocument + { + { "fn", "John" }, + { "ln", "Doe" }, + }, + new BsonDocument + { + { "fn", "Jane" }, + { "ln", "Doe" }, + }), + "{ moreLikeThis: { like: [{ fn: 'John', ln: 'Doe' }, { fn: 'Jane', ln: 'Doe' }] } }"); + } + + [Fact] + public void Must() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Compound().Must( + subject.Exists(p => p.Age), + subject.Exists(p => p.Birthday)), + "{ compound: { must: [{ exists: { path: 'age' } }, { exists: { path: 'dob' } }] } }"); + } + + [Fact] + public void MustNot() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Compound().MustNot( + subject.Exists(p => p.Age), + subject.Exists(p => p.Birthday)), + "{ compound: { mustNot: [{ exists: { path: 'age' } }, { exists: { path: 'dob' } }] } }"); + } + + [Fact] + public void Near() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Near("x", 5.0, 1.0), + "{ near: { path: 'x', origin: 5.0, pivot: 1.0 } }"); + AssertRendered( + subject.Near("x", 5, 1), + "{ near: { path: 'x', origin: 5, pivot: 1 } }"); + AssertRendered( + subject.Near("x", 5L, 1L), + "{ near: { path: 'x', origin: { $numberLong: '5' }, pivot: { $numberLong: '1' } } }"); + AssertRendered( + subject.Near("x", new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), 1000L), + "{ near: { path: 'x', origin: { $date: '2000-01-01T00:00:00Z' }, pivot: { $numberLong: '1000' } } }"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Near("x", 5.0, 1.0, scoreBuilder.Constant(1)), + "{ near: { path: 'x', origin: 5, pivot: 1, score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Near_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Near(x => x.Age, 35.0, 5.0), + "{ near: { path: 'age', origin: 35.0, pivot: 5.0 } }"); + AssertRendered( + subject.Near("Age", 35.0, 5.0), + "{ near: { path: 'age', origin: 35.0, pivot: 5.0 } }"); + + AssertRendered( + subject.Near(x => x.Age, 35, 5), + "{ near: { path: 'age', origin: 35, pivot: 5 } }"); + AssertRendered( + subject.Near("Age", 35, 5), + "{ near: { path: 'age', origin: 35, pivot: 5 } }"); + + AssertRendered( + subject.Near(x => x.Age, 35L, 5L), + "{ near: { path: 'age', origin: { $numberLong: '35' }, pivot: { $numberLong: '5' } } }"); + AssertRendered( + subject.Near("Age", 35L, 5L), + "{ near: { path: 'age', origin: { $numberLong: '35' }, pivot: { $numberLong: '5' } } }"); + + AssertRendered( + subject.Near(x => x.Birthday, new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), 1000L), + "{ near: { path: 'dob', origin: { $date: '2000-01-01T00:00:00Z' }, pivot: { $numberLong: '1000' } } }"); + AssertRendered( + subject.Near("Birthday", new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), 1000L), + "{ near: { path: 'dob', origin: { $date: '2000-01-01T00:00:00Z' }, pivot: { $numberLong: '1000' } } }"); + } + + [Fact] + public void Phrase() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Phrase("x", "foo"), + "{ phrase: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Phrase(new[] { "x", "y" }, "foo"), + "{ phrase: { query: 'foo', path: ['x', 'y'] } }"); + AssertRendered( + subject.Phrase("x", new[] { "foo", "bar" }), + "{ phrase: { query: ['foo', 'bar'], path: 'x' } }"); + AssertRendered( + subject.Phrase(new[] { "x", "y" }, new[] { "foo", "bar" }), + "{ phrase: { query: ['foo', 'bar'], path: ['x', 'y'] } }"); + + 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 } } } }"); + } + + [Fact] + public void Phrase_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Phrase(x => x.FirstName, "foo"), + "{ phrase: { query: 'foo', path: 'fn' } }"); + AssertRendered( + subject.Phrase("FirstName", "foo"), + "{ phrase: { query: 'foo', path: 'fn' } }"); + + AssertRendered( + subject.Phrase( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + "foo"), + "{ phrase: { query: 'foo', path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Phrase(new[] { "FirstName", "LastName" }, "foo"), + "{ phrase: { query: 'foo', path: ['fn', 'ln'] } }"); + + AssertRendered( + subject.Phrase(x => x.FirstName, new[] { "foo", "bar" }), + "{ phrase: { query: ['foo', 'bar'], path: 'fn' } }"); + AssertRendered( + subject.Phrase("FirstName", new[] { "foo", "bar" }), + "{ phrase: { query: ['foo', 'bar'], path: 'fn' } }"); + + AssertRendered( + subject.Phrase( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + new[] { "foo", "bar" }), + "{ phrase: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Phrase(new[] { "FirstName", "LastName" }, new[] { "foo", "bar" }), + "{ phrase: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + } + + [Fact] + public void QueryString() + { + var subject = CreateSubject(); + + AssertRendered( + subject.QueryString("x", "foo"), + "{ queryString: { defaultPath: 'x', query: 'foo' } }"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.QueryString("x", "foo", scoreBuilder.Constant(1)), + "{ queryString: { defaultPath: 'x', query: 'foo', score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void QueryString_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.QueryString(x => x.FirstName, "foo"), + "{ queryString: { defaultPath: 'fn', query: 'foo' } }"); + AssertRendered( + subject.QueryString("FirstName", "foo"), + "{ queryString: { defaultPath: 'fn', query: 'foo' } }"); + } + + [Fact] + public void RangeDateTime() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Range( + p => p.Birthday, + SearchRangeBuilder + .Gte(new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)) + .Lte(new DateTime(2009, 12, 31, 0, 0, 0, DateTimeKind.Utc))), + "{ range: { path: 'dob', gte: { $date: '2000-01-01T00:00:00Z' }, lte: { $date: '2009-12-31T00:00:00Z' } } }"); + } + + [Fact] + public void RangeDouble() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Range(p => p.Age, SearchRangeBuilder.Gt(1.5).Lt(2.5)), + "{ range: { path: 'age', gt: 1.5, lt: 2.5 } }"); + } + + [Fact] + public void RangeInt32() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Range("x", SearchRangeBuilder.Gt(1).Lt(10)), + "{ range: { path: 'x', gt: 1, lt: 10 } }"); + AssertRendered( + subject.Range("x", SearchRangeBuilder.Lt(10).Gt(1)), + "{ range: { path: 'x', gt: 1, lt: 10 } }"); + AssertRendered( + subject.Range("x", SearchRangeBuilder.Gte(1).Lte(10)), + "{ range: { path: 'x', gte: 1, lte: 10 } }"); + AssertRendered( + subject.Range("x", SearchRangeBuilder.Lte(10).Gte(1)), + "{ range: { path: 'x', gte: 1, lte: 10 } }"); + } + + [Fact] + public void RangeInt32_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Range(x => x.Age, SearchRangeBuilder.Gte(18).Lt(65)), + "{ range: { path: 'age', gte: 18, lt: 65 } }"); + AssertRendered( + subject.Range("Age", SearchRangeBuilder.Gte(18).Lt(65)), + "{ range: { path: 'age', gte: 18, lt: 65 } }"); + } + + [Fact] + public void Regex() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Regex("x", "foo"), + "{ regex: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Regex(new[] { "x", "y" }, "foo"), + "{ regex: { query: 'foo', path: ['x', 'y'] } }"); + AssertRendered( + subject.Regex("x", new[] { "foo", "bar" }), + "{ regex: { query: ['foo', 'bar'], path: 'x' } }"); + AssertRendered( + subject.Regex(new[] { "x", "y" }, new[] { "foo", "bar" }), + "{ regex: { query: ['foo', 'bar'], path: ['x', 'y'] } }"); + + AssertRendered( + subject.Regex("x", "foo", false), + "{ regex: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Regex("x", "foo", true), + "{ regex: { query: 'foo', path: 'x', allowAnalyzedField: true } }"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Regex("x", "foo", score: scoreBuilder.Constant(1)), + "{ regex: { query: 'foo', path: 'x', score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Regex_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Regex(x => x.FirstName, "foo"), + "{ regex: { query: 'foo', path: 'fn' } }"); + AssertRendered( + subject.Regex("FirstName", "foo"), + "{ regex: { query: 'foo', path: 'fn' } }"); + + AssertRendered( + subject.Regex( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + "foo"), + "{ regex: { query: 'foo', path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Regex(new[] { "FirstName", "LastName" }, "foo"), + "{ regex: { query: 'foo', path: ['fn', 'ln'] } }"); + + AssertRendered( + subject.Regex(x => x.FirstName, new[] { "foo", "bar" }), + "{ regex: { query: ['foo', 'bar'], path: 'fn' } }"); + AssertRendered( + subject.Regex("FirstName", new[] { "foo", "bar" }), + "{ regex: { query: ['foo', 'bar'], path: 'fn' } }"); + + AssertRendered( + subject.Regex( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + new[] { "foo", "bar" }), + "{ regex: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Regex(new[] { "FirstName", "LastName" }, new[] { "foo", "bar" }), + "{ regex: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + } + + [Fact] + public void Should() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Compound() + .Should( + subject.Exists(p => p.Age), + subject.Exists(p => p.Birthday)) + .MinimumShouldMatch(2), + "{ compound: { should: [{ exists: { path: 'age' } }, { exists: { path: 'dob' } }], minimumShouldMatch: 2 } }"); + } + + [Fact] + public void Span() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Span(Builders.SearchSpan + .First(Builders.SearchSpan.Term(p => p.Age, "foo"), 5)), + "{ span: { first: { operator: { term: { query: 'foo', path: 'age' } }, endPositionLte: 5 } } }"); + } + + [Fact] + public void Text() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Text("x", "foo"), + "{ text: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Text(new[] { "x", "y" }, "foo"), + "{ text: { query: 'foo', path: ['x', 'y'] } }"); + AssertRendered( + subject.Text("x", new[] { "foo", "bar" }), + "{ text: { query: ['foo', 'bar'], path: 'x' } }"); + AssertRendered( + subject.Text(new[] { "x", "y" }, new[] { "foo", "bar" }), + "{ text: { query: ['foo', 'bar'], path: ['x', 'y'] } }"); + + AssertRendered( + subject.Text("x", "foo", new SearchFuzzyOptions()), + "{ text: { query: 'foo', path: 'x', fuzzy: {} } }"); + AssertRendered( + subject.Text("x", "foo", new SearchFuzzyOptions() + { + MaxEdits = 1, + PrefixLength = 5, + MaxExpansions = 25 + }), + "{ text: { query: 'foo', path: 'x', fuzzy: { maxEdits: 1, prefixLength: 5, maxExpansions: 25 } } }"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Text("x", "foo", score: scoreBuilder.Constant(1)), + "{ text: { query: 'foo', path: 'x', score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Text_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Text(x => x.FirstName, "foo"), + "{ text: { query: 'foo', path: 'fn' } }"); + AssertRendered( + subject.Text("FirstName", "foo"), + "{ text: { query: 'foo', path: 'fn' } }"); + + AssertRendered( + subject.Text( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + "foo"), + "{ text: { query: 'foo', path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Text(new[] { "FirstName", "LastName" }, "foo"), + "{ text: { query: 'foo', path: ['fn', 'ln'] } }"); + + AssertRendered( + subject.Text(x => x.FirstName, new[] { "foo", "bar" }), + "{ text: { query: ['foo', 'bar'], path: 'fn' } }"); + AssertRendered( + subject.Text("FirstName", new[] { "foo", "bar" }), + "{ text: { query: ['foo', 'bar'], path: 'fn' } }"); + + AssertRendered( + subject.Text( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + new[] { "foo", "bar" }), + "{ text: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Text(new[] { "FirstName", "LastName" }, new[] { "foo", "bar" }), + "{ text: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + } + + [Fact] + public void Wildcard() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Wildcard("x", "foo"), + "{ wildcard: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Wildcard(new[] { "x", "y" }, "foo"), + "{ wildcard: { query: 'foo', path: ['x', 'y'] } }"); + AssertRendered( + subject.Wildcard("x", new[] { "foo", "bar" }), + "{ wildcard: { query: ['foo', 'bar'], path: 'x' } }"); + AssertRendered( + subject.Wildcard(new[] { "x", "y" }, new[] { "foo", "bar" }), + "{ wildcard: { query: ['foo', 'bar'], path: ['x', 'y'] } }"); + + AssertRendered( + subject.Wildcard("x", "foo", false), + "{ wildcard: { query: 'foo', path: 'x' } }"); + AssertRendered( + subject.Wildcard("x", "foo", true), + "{ wildcard: { query: 'foo', path: 'x', allowAnalyzedField: true } }"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Wildcard("x", "foo", score: scoreBuilder.Constant(1)), + "{ wildcard: { query: 'foo', path: 'x', score: { constant: { value: 1 } } } }"); + } + + [Fact] + public void Wildcard_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Wildcard(x => x.FirstName, "foo"), + "{ wildcard: { query: 'foo', path: 'fn' } }"); + AssertRendered( + subject.Wildcard("FirstName", "foo"), + "{ wildcard: { query: 'foo', path: 'fn' } }"); + + AssertRendered( + subject.Wildcard( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + "foo"), + "{ wildcard: { query: 'foo', path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Wildcard(new[] { "FirstName", "LastName" }, "foo"), + "{ wildcard: { query: 'foo', path: ['fn', 'ln'] } }"); + + AssertRendered( + subject.Wildcard(x => x.FirstName, new[] { "foo", "bar" }), + "{ wildcard: { query: ['foo', 'bar'], path: 'fn' } }"); + AssertRendered( + subject.Wildcard("FirstName", new[] { "foo", "bar" }), + "{ wildcard: { query: ['foo', 'bar'], path: 'fn' } }"); + + AssertRendered( + subject.Wildcard( + new FieldDefinition[] + { + new ExpressionFieldDefinition(x => x.FirstName), + new ExpressionFieldDefinition(x => x.LastName) + }, + new[] { "foo", "bar" }), + "{ wildcard: { query: ['foo', 'bar'], path: ['fn', 'ln'] } }"); + AssertRendered( + subject.Wildcard(new[] { "FirstName", "LastName" }, new[] { "foo", "bar" }), + "{ wildcard: { 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().BeEquivalentTo(expected); + } + + private SearchDefinitionBuilder CreateSubject() => new SearchDefinitionBuilder(); + + private class Person : SimplePerson + { + [BsonElement("age")] + public int Age { get; set; } + + [BsonElement("dob")] + public DateTime Birthday { get; set; } + + [BsonId] + public ObjectId Id { get; set; } + [BsonElement("location")] + public GeoJsonPoint Location { get; set; } + + [BsonElement("ret")] + public bool Retired { get; set; } + } + + private class SimplePerson + { + [BsonElement("fn")] + public string FirstName { get; set; } + + [BsonElement("ln")] + public string LastName { get; set; } + } + + private class SimplestPerson + { + [BsonElement("fn")] + public string FirstName { get; set; } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/SearchFacetBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchFacetBuilderTests.cs new file mode 100644 index 00000000000..a65b39697e6 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/SearchFacetBuilderTests.cs @@ -0,0 +1,144 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Search; +using Xunit; + +namespace MongoDB.Driver.Tests.Search +{ + public class SearchFacetBuilderTests + { + [Fact] + public void Date() + { + var subject = CreateSubject(); + var boundaries = new List() + { + DateTime.MinValue, + DateTime.MaxValue + }; + + AssertRendered( + subject.Date("date", "x", boundaries, "foo"), + "{ type: 'date', path: 'x', boundaries: [{ $date: '0001-01-01T00:00:00Z' }, { $date: '9999-12-31T23:59:59.9999999Z' }], default: 'foo' }"); + AssertRendered( + subject.Date("date", "x", boundaries), + "{ type: 'date', path: 'x', boundaries: [{ $date: '0001-01-01T00:00:00Z' }, { $date: '9999-12-31T23:59:59.9999999Z' }] }"); + } + + [Fact] + public void Date_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Date("date", x => x.Birthday, DateTime.MinValue, DateTime.MaxValue), + "{ type: 'date', path: 'dob', boundaries: [{ $date: '0001-01-01T00:00:00Z' }, { $date: '9999-12-31T23:59:59.9999999Z' }] }"); + AssertRendered( + subject.Date("date", "Birthday", DateTime.MinValue, DateTime.MaxValue), + "{ type: 'date', path: 'dob', boundaries: [{ $date: '0001-01-01T00:00:00Z' }, { $date: '9999-12-31T23:59:59.9999999Z' }] }"); + } + + [Fact] + public void Number() + { + var subject = CreateSubject(); + var boundaries = new List() + { + 0, + 50, + 100 + }; + + AssertRendered( + subject.Number("number", "x", boundaries, "foo"), + "{ type: 'number', path: 'x', boundaries: [0, 50, 100], default: 'foo' }"); + AssertRendered( + subject.Number("number", "x", boundaries), + "{ type: 'number', path: 'x', boundaries: [0, 50, 100] }"); + } + + [Fact] + public void Number_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Number("number", x => x.Age, 0, 18, 65, 120), + "{ type: 'number', path: 'age', boundaries: [0, 18, 65, 120] }"); + AssertRendered( + subject.Number("number", "Age", 0, 18, 65, 120), + "{ type: 'number', path: 'age', boundaries: [0, 18, 65, 120] }"); + } + + [Fact] + public void String() + { + var subject = CreateSubject(); + + AssertRendered( + subject.String("string", "x", 100), + "{ type: 'string', path: 'x', numBuckets: 100 }"); + } + + [Fact] + public void String_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.String("string", x => x.FirstName, 100), + "{ type: 'string', path: 'fn', numBuckets: 100 }"); + AssertRendered( + subject.String("string", "FirstName", 100), + "{ type: 'string', path: 'fn', numBuckets: 100 }"); + } + + private void AssertRendered(SearchFacet facet, string expected) => + AssertRendered(facet, BsonDocument.Parse(expected)); + + private void AssertRendered(SearchFacet facet, BsonDocument expected) + { + var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var renderedFacet = facet.Render(documentSerializer, BsonSerializer.SerializerRegistry); + + renderedFacet.Should().BeEquivalentTo(expected); + } + + private SearchFacetBuilder CreateSubject() => + new SearchFacetBuilder(); + + private class Person + { + [BsonElement("age")] + public int Age { get; set; } + + [BsonElement("dob")] + public DateTime Birthday { get; set; } + + [BsonElement("fn")] + public string FirstName { get; set; } + + [BsonElement("ln")] + public string LastName { get; set; } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/SearchPathDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchPathDefinitionBuilderTests.cs new file mode 100644 index 00000000000..c88c8db8fda --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/SearchPathDefinitionBuilderTests.cs @@ -0,0 +1,148 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Search; +using Xunit; + +namespace MongoDB.Driver.Tests.Search +{ + public class SearchPathDefinitionBuilderTests + { + [Fact] + public void Analyzer() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Analyzer("x", "english"), + "{ value: 'x', multi: 'english' }"); + } + + [Fact] + public void Analyzer_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Analyzer(x => x.FirstName, "english"), + "{ value: 'fn', multi: 'english' }"); + AssertRendered( + subject.Analyzer("FirstName", "english"), + "{ value: 'fn', multi: 'english' }"); + } + + [Fact] + public void Multi() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Multi("x", "y"), + new BsonArray() + { + new BsonString("x"), + new BsonString("y") + }); + AssertRendered( + subject.Multi( + new List>() + { + "x", + "y" + }), + new BsonArray() + { + new BsonString("x"), + new BsonString("y") + }); + } + + [Fact] + public void Multi_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Multi(x => x.FirstName, x => x.LastName), + new BsonArray() + { + new BsonString("fn"), + new BsonString("ln") + }); + AssertRendered( + subject.Multi("FirstName", "LastName"), + new BsonArray() + { + new BsonString("fn"), + new BsonString("ln") + }); + } + + [Fact] + public void Single() + { + var subject = CreateSubject(); + + AssertRendered(subject.Single("x"), new BsonString("x")); + } + + [Fact] + public void Single_typed() + { + var subject = CreateSubject(); + + AssertRendered(subject.Single(x => x.FirstName), new BsonString("fn")); + AssertRendered(subject.Single("FirstName"), new BsonString("fn")); + } + + [Fact] + public void Wildcard() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Wildcard("*"), + "{ wildcard: '*' }"); + } + + private void AssertRendered(SearchPathDefinition path, string expected) => + AssertRendered(path, BsonDocument.Parse(expected)); + + private void AssertRendered(SearchPathDefinition path, BsonValue expected) + { + var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var renderedPath = path.Render(documentSerializer, BsonSerializer.SerializerRegistry); + + renderedPath.Should().Be(expected); + } + + private SearchPathDefinitionBuilder CreateSubject() => + new SearchPathDefinitionBuilder(); + + private class Person + { + [BsonElement("fn")] + public string FirstName { get; set; } + + [BsonElement("ln")] + public string LastName { get; set; } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/SearchScoreDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchScoreDefinitionBuilderTests.cs new file mode 100644 index 00000000000..7c18f598bb6 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/SearchScoreDefinitionBuilderTests.cs @@ -0,0 +1,103 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Search; +using Xunit; + +namespace MongoDB.Driver.Tests.Search +{ + public class SearchScoreDefinitionBuilderTests + { + [Fact] + public void Boost() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Boost(1), + "{ boost: { value: 1 } }"); + AssertRendered( + subject.Boost("x"), + "{ boost: { path: 'x' } }"); + AssertRendered( + subject.Boost("x", 1), + "{ boost: { path: 'x', undefined: 1 } }"); + AssertRendered( + subject.Boost(p => p.Age, 1), + "{ boost: { path: 'age', undefined: 1 } }"); + } + + [Fact] + public void Boost_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Boost(x => x.Age), + "{ boost: { path: 'age' } }"); + AssertRendered( + subject.Boost("age"), + "{ boost: { path: 'age' } }"); + } + + [Fact] + public void Constant() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Constant(1), + "{ constant: { value: 1 } }"); + } + + [Fact] + public void Function() + { + var subject = CreateSubject(); + var functionBuilder = new SearchScoreFunctionBuilder(); + + AssertRendered( + subject.Function(functionBuilder.Path(p => p.Age)), + "{ function: { path: 'age' } }"); + AssertRendered( + subject.Function(functionBuilder.Path("age")), + "{ function: { path: 'age' } }"); + } + + private void AssertRendered(SearchScoreDefinition score, string expected) => + AssertRendered(score, BsonDocument.Parse(expected)); + + private void AssertRendered(SearchScoreDefinition score, BsonDocument expected) + { + var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var renderedQuery = score.Render(documentSerializer, BsonSerializer.SerializerRegistry); + + renderedQuery.Should().BeEquivalentTo(expected); + } + + private SearchScoreDefinitionBuilder CreateSubject() => + new SearchScoreDefinitionBuilder(); + + private class Person + { + [BsonElement("age")] + public int Age { get; set; } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Search/SearchSpanDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchSpanDefinitionBuilderTests.cs new file mode 100644 index 00000000000..2a020e5bae7 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Search/SearchSpanDefinitionBuilderTests.cs @@ -0,0 +1,173 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Search; +using Xunit; + +namespace MongoDB.Driver.Tests.Search +{ + public class SearchSpanDefinitionBuilderTests + { + [Fact] + public void First() + { + var subject = CreateSubject(); + + AssertRendered( + subject.First(subject.Term("x", "foo"), 5), + "{ first: { operator: { term: { query: 'foo', path: 'x' } }, endPositionLte: 5 } }"); + } + + [Fact] + public void First_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.First(subject.Term(x => x.Biography, "born"), 5), + "{ first: { operator: { term: { query: 'born', path: 'bio' } }, endPositionLte: 5 } }"); + AssertRendered( + subject.First(subject.Term("Biography", "born"), 5), + "{ first: { operator: { term: { query: 'born', path: 'bio' } }, endPositionLte: 5 } }"); + } + + [Fact] + public void Near() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Near( + new List>() + { + subject.Term("x", "foo"), + subject.Term("x", "bar") + }, + 5, + inOrder: true), + "{ near: { clauses: [{ term: { query: 'foo', path: 'x' } }, { term: { query: 'bar', path: 'x' } }], slop: 5, inOrder: true } }"); + } + + [Fact] + public void Near_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Near( + new List>() + { + subject.Term(x => x.Biography, "born"), + subject.Term(x => x.Biography, "school") + }, + 5, + inOrder: true), + "{ near: { clauses: [{ term: { query: 'born', path: 'bio' } }, { term: { query: 'school', path: 'bio' } }], slop: 5, inOrder: true } }"); + AssertRendered( + subject.Near( + new List>() + { + subject.Term("Biography", "born"), + subject.Term("Biography", "school") + }, + 5, + inOrder: true), + "{ near: { clauses: [{ term: { query: 'born', path: 'bio' } }, { term: { query: 'school', path: 'bio' } }], slop: 5, inOrder: true } }"); + } + + [Fact] + public void Or() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Or( + subject.Term("x", "foo"), + subject.Term("x", "bar")), + "{ or: { clauses: [{ term: { query: 'foo', path: 'x' } }, { term: { query: 'bar', path: 'x' } }] } }"); + } + + [Fact] + public void Or_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Or( + subject.Term(x => x.Biography, "engineer"), + subject.Term(x => x.Biography, "developer")), + "{ or: { clauses: [{ term: { query: 'engineer', path: 'bio' } }, { term: { query: 'developer', path: 'bio' } }] } }"); + AssertRendered( + subject.Or( + subject.Term("Biography", "engineer"), + subject.Term("Biography", "developer")), + "{ or: { clauses: [{ term: { query: 'engineer', path: 'bio' } }, { term: { query: 'developer', path: 'bio' } }] } }"); + } + + [Fact] + public void Subtract() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Subtract( + subject.Term("x", "foo"), + subject.Term("x", "bar")), + "{ subtract: { include: { term: { query: 'foo', path: 'x' } }, exclude: { term: { query: 'bar', path: 'x' } } } }"); + } + + [Fact] + public void Subtract_typed() + { + var subject = CreateSubject(); + + AssertRendered( + subject.Subtract( + subject.Term(x => x.Biography, "engineer"), + subject.Term(x => x.Biography, "train")), + "{ subtract: { include: { term: { query: 'engineer', path: 'bio' } }, exclude: { term: { query: 'train', path: 'bio' } } } }"); + AssertRendered( + subject.Subtract( + subject.Term("Biography", "engineer"), + subject.Term("Biography", "train")), + "{ subtract: { include: { term: { query: 'engineer', path: 'bio' } }, exclude: { term: { query: 'train', path: 'bio' } } } }"); + } + + private void AssertRendered(SearchSpanDefinition span, string expected) => + AssertRendered(span, BsonDocument.Parse(expected)); + + private void AssertRendered(SearchSpanDefinition span, BsonDocument expected) + { + var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var renderedSpan = span.Render(documentSerializer, BsonSerializer.SerializerRegistry); + + renderedSpan.Should().BeEquivalentTo(expected); + } + + private SearchSpanDefinitionBuilder CreateSubject() => + new SearchSpanDefinitionBuilder(); + + private class Person + { + [BsonElement("bio")] + public string Biography { get; set; } + } + } +}