From 130bc2ba36ce7e60e674cc4adca58bb7ec25a046 Mon Sep 17 00:00:00 2001 From: Samir Boulema Date: Sat, 1 Jul 2023 15:29:54 +0200 Subject: [PATCH 1/3] CSHARP-4703: Initial implementation --- src/MongoDB.Driver/AggregateFluent.cs | 5 +- src/MongoDB.Driver/AggregateFluentBase.cs | 3 +- src/MongoDB.Driver/IAggregateFluent.cs | 7 ++- src/MongoDB.Driver/Linq/MongoQueryable.cs | 9 ++- .../PipelineDefinitionBuilder.cs | 9 ++- .../PipelineStageDefinitionBuilder.cs | 8 ++- .../ProjectionDefinitionBuilder.cs | 29 +++++++++ .../Search/SearchScoreDetails.cs | 60 +++++++++++++++++++ .../PipelineDefinitionBuilderTests.cs | 12 ++++ .../Search/AtlasSearchTests.cs | 16 ++++- .../ProjectionDefinitionBuilderTests.cs | 8 +++ 11 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 src/MongoDB.Driver/Search/SearchScoreDetails.cs diff --git a/src/MongoDB.Driver/AggregateFluent.cs b/src/MongoDB.Driver/AggregateFluent.cs index f4732d3fc69..52db3ab89ef 100644 --- a/src/MongoDB.Driver/AggregateFluent.cs +++ b/src/MongoDB.Driver/AggregateFluent.cs @@ -244,9 +244,10 @@ public override IAggregateFluent Search( SearchHighlightOptions highlight = null, string indexName = null, SearchCountOptions count = null, - bool returnStoredSource = false) + bool returnStoredSource = false, + bool scoreDetails = false) { - return WithPipeline(_pipeline.Search(searchDefinition, highlight, indexName, count, returnStoredSource)); + return WithPipeline(_pipeline.Search(searchDefinition, highlight, indexName, count, returnStoredSource, scoreDetails)); } public override IAggregateFluent SearchMeta( diff --git a/src/MongoDB.Driver/AggregateFluentBase.cs b/src/MongoDB.Driver/AggregateFluentBase.cs index 8239750962a..511eced50ec 100644 --- a/src/MongoDB.Driver/AggregateFluentBase.cs +++ b/src/MongoDB.Driver/AggregateFluentBase.cs @@ -222,7 +222,8 @@ public virtual IAggregateFluent Search( SearchHighlightOptions highlight = null, string indexName = null, SearchCountOptions count = null, - bool returnStoredSource = false) + bool returnStoredSource = false, + bool scoreDetails = false) { throw new NotImplementedException(); } diff --git a/src/MongoDB.Driver/IAggregateFluent.cs b/src/MongoDB.Driver/IAggregateFluent.cs index cca33c9e1c9..079f16d897d 100644 --- a/src/MongoDB.Driver/IAggregateFluent.cs +++ b/src/MongoDB.Driver/IAggregateFluent.cs @@ -365,13 +365,18 @@ IAggregateFluent SetWindowFields( /// Flag that specifies whether to perform a full document lookup on the backend database /// or return only stored source fields directly from Atlas Search. /// + /// + /// Flag that specifies whether to return a detailed breakdown + /// of the score for each document in the result. + /// /// The fluent aggregate interface. IAggregateFluent Search( SearchDefinition searchDefinition, SearchHighlightOptions highlight = null, string indexName = null, SearchCountOptions count = null, - bool returnStoredSource = false); + bool returnStoredSource = false, + bool scoreDetails = false); /// /// Appends a $searchMeta stage to the pipeline. diff --git a/src/MongoDB.Driver/Linq/MongoQueryable.cs b/src/MongoDB.Driver/Linq/MongoQueryable.cs index b84eba14b88..ffee7be89d3 100644 --- a/src/MongoDB.Driver/Linq/MongoQueryable.cs +++ b/src/MongoDB.Driver/Linq/MongoQueryable.cs @@ -1149,6 +1149,10 @@ public static IMongoQueryable Sample(this IMongoQueryable + /// + /// Flag that specifies whether to return a detailed breakdown + /// of the score for each document in the result. + /// /// The queryable with a new stage appended. public static IMongoQueryable Search( this IMongoQueryable source, @@ -1156,11 +1160,12 @@ public static IMongoQueryable Search( SearchHighlightOptions highlight = null, string indexName = null, SearchCountOptions count = null, - bool returnStoredSource = false) + bool returnStoredSource = false, + bool scoreDetails = false) { return AppendStage( source, - PipelineStageDefinitionBuilder.Search(searchDefinition, highlight, indexName, count, returnStoredSource)); + PipelineStageDefinitionBuilder.Search(searchDefinition, highlight, indexName, count, returnStoredSource, scoreDetails)); } /// diff --git a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs index a78b3f98af1..8c25fe5c2e3 100644 --- a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs @@ -1179,6 +1179,10 @@ public static PipelineDefinition ReplaceWith + /// + /// Flag that specifies whether to return a detailed breakdown + /// of the score for each document in the result. + /// /// /// A new pipeline with an additional stage. /// @@ -1188,10 +1192,11 @@ public static PipelineDefinition Search( SearchHighlightOptions highlight = null, string indexName = null, SearchCountOptions count = null, - bool returnStoredSource = false) + bool returnStoredSource = false, + bool scoreDetails = false) { Ensure.IsNotNull(pipeline, nameof(pipeline)); - return pipeline.AppendStage(PipelineStageDefinitionBuilder.Search(searchDefinition, highlight, indexName, count, returnStoredSource)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.Search(searchDefinition, highlight, indexName, count, returnStoredSource, scoreDetails)); } /// diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 636345d6413..6ef9e68c111 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -1320,13 +1320,18 @@ public static PipelineStageDefinition Project( /// Flag that specifies whether to perform a full document lookup on the backend database /// or return only stored source fields directly from Atlas Search. /// + /// + /// Flag that specifies whether to return a detailed breakdown + /// of the score for each document in the result. + /// /// The stage. public static PipelineStageDefinition Search( SearchDefinition searchDefinition, SearchHighlightOptions highlight = null, string indexName = null, SearchCountOptions count = null, - bool returnStoredSource = false) + bool returnStoredSource = false, + bool scoreDetails = false) { Ensure.IsNotNull(searchDefinition, nameof(searchDefinition)); @@ -1340,6 +1345,7 @@ public static PipelineStageDefinition Search( renderedSearchDefinition.Add("count", () => count.Render(), count != null); renderedSearchDefinition.Add("index", indexName, indexName != null); renderedSearchDefinition.Add("returnStoredSource", returnStoredSource, returnStoredSource); + renderedSearchDefinition.Add("scoreDetails", scoreDetails, scoreDetails); var document = new BsonDocument(operatorName, renderedSearchDefinition); return new RenderedPipelineStageDefinition(operatorName, document, s); diff --git a/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs b/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs index 52b01102815..8fdd6d140e5 100644 --- a/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs +++ b/src/MongoDB.Driver/ProjectionDefinitionBuilder.cs @@ -190,6 +190,23 @@ public static ProjectionDefinition MetaSearchScore( return builder.Combine(projection, builder.MetaSearchScore(field)); } + /// + /// Combines an existing projection with a search score details projection. + /// + /// The type of the document. + /// The projection. + /// The field. + /// + /// A combined projection. + /// + public static ProjectionDefinition MetaSearchScoreDetails( + this ProjectionDefinition projection, + string field) + { + var builder = Builders.Projection; + return builder.Combine(projection, builder.MetaSearchScoreDetails(field)); + } + /// /// Combines an existing projection with a text score projection. /// @@ -455,6 +472,18 @@ public ProjectionDefinition MetaSearchScore(string field) return Meta(field, "searchScore"); } + /// + /// Creates a search score details projection. + /// + /// The field. + /// + /// A search score details projection. + /// + public ProjectionDefinition MetaSearchScoreDetails(string field) + { + return Meta(field, "searchScoreDetails"); + } + /// /// Creates a text score projection. /// diff --git a/src/MongoDB.Driver/Search/SearchScoreDetails.cs b/src/MongoDB.Driver/Search/SearchScoreDetails.cs new file mode 100644 index 00000000000..01426a9c3c7 --- /dev/null +++ b/src/MongoDB.Driver/Search/SearchScoreDetails.cs @@ -0,0 +1,60 @@ +/* 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 +{ + /// + /// A search count result set. + /// + public sealed class SearchScoreDetails + { + /// + /// Initializes a new instance of the class. + /// + /// Contribution towards the score by a subset of the scoring formula. + /// Subset of the scoring formula. + /// Breakdown of the score for each match in the document. + public SearchScoreDetails(double value, string description, SearchScoreDetails[] details) + { + Value = value; + Description = description; + Details = details; + } + + /// + /// Gets the contribution towards the score by a subset of the scoring formula. + /// + [BsonElement("value")] + public double Value { get; } + + /// + /// Gets the subset of the scoring formula including details about how the document + /// was scored and factors considered in calculating the score. + /// + [BsonElement("description")] + public string Description { get; } + + /// + /// Breakdown of the score for each match in the document based on the subset of the scoring formula. + /// (if any). + /// + [BsonDefaultValue(null)] + [BsonElement("details")] + public SearchScoreDetails[] Details { get; } + } +} diff --git a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs index a393565a60d..384f28dc28b 100644 --- a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs @@ -182,6 +182,18 @@ public void Search_should_add_expected_stage_with_return_stored_source() stages[0].Should().Be("{ $search: { text: { query: 'foo', path: 'bar' }, returnStoredSource: true } }"); } + [Fact] + public void Search_should_add_expected_stage_with_score_details() + { + var pipeline = new EmptyPipelineDefinition(); + var builder = new SearchDefinitionBuilder(); + + var result = pipeline.Search(builder.Text("bar", "foo"), scoreDetails: true); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages[0].Should().Be("{ $search: { text: { query: 'foo', path: 'bar' }, scoreDetails: true } }"); + } + [Fact] public void Search_should_throw_when_pipeline_is_null() { diff --git a/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs index cb36c5d6482..73b50abb4f2 100644 --- a/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs +++ b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs @@ -256,13 +256,15 @@ public void Phrase() .Search(Builders.Search.Phrase(x => x.Body, "life, liberty, and the pursuit of happiness"), new SearchHighlightOptions(x => x.Body), indexName: "default", - returnStoredSource: true) + returnStoredSource: true, + scoreDetails: true) .Limit(1) .Project(Builders.Projection .Include(x => x.Title) .Include(x => x.Body) .MetaSearchScore("score") - .MetaSearchHighlights("highlights")) + .MetaSearchHighlights("highlights") + .MetaSearchScoreDetails("scoreDetails")) .ToList(); var result = results.Should().ContainSingle().Subject; @@ -280,6 +282,13 @@ public void Phrase() var highlightRangeStr = string.Join(string.Empty, highlightTexts.Skip(1).Select(x => x.Value)); highlightRangeStr.Should().Be("Life, Liberty and the pursuit of Happiness."); + + var scoreDetails = result.ScoreDetails.Value.Should().NotBe(0); + + foreach (var scoreDetail in result.ScoreDetails.Details) + { + scoreDetail.Value.Should().NotBe(0); + } } [Fact] @@ -493,6 +502,9 @@ public class HistoricalDocument [BsonElement("metaResult")] public SearchMetaResult MetaResult { get; set; } + + [BsonElement("scoreDetails")] + public SearchScoreDetails ScoreDetails { get; set; } } [BsonIgnoreExtraElements] diff --git a/tests/MongoDB.Driver.Tests/Search/ProjectionDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/ProjectionDefinitionBuilderTests.cs index d827ca1220f..20933df595f 100644 --- a/tests/MongoDB.Driver.Tests/Search/ProjectionDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Search/ProjectionDefinitionBuilderTests.cs @@ -38,6 +38,14 @@ public void MetaSearchScore() AssertRendered(subject.MetaSearchScore("a"), "{ a : { $meta: 'searchScore' } }"); } + [Fact] + public void MetaSearchScoreDetails() + { + var subject = CreateSubject(); + + AssertRendered(subject.MetaSearchScoreDetails("a"), "{ a : { $meta: 'searchScoreDetails' } }"); + } + [Fact] public void SearchMeta() { From 47fc19b2810ae9f306ec40da30987e322b899f9f Mon Sep 17 00:00:00 2001 From: Samir Boulema Date: Tue, 11 Jul 2023 23:12:43 +0200 Subject: [PATCH 2/3] Update tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs Co-authored-by: BorisDog --- tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs index 73b50abb4f2..dd8a52ec5a7 100644 --- a/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs +++ b/tests/MongoDB.Driver.Tests/Search/AtlasSearchTests.cs @@ -283,12 +283,13 @@ public void Phrase() var highlightRangeStr = string.Join(string.Empty, highlightTexts.Skip(1).Select(x => x.Value)); highlightRangeStr.Should().Be("Life, Liberty and the pursuit of Happiness."); - var scoreDetails = result.ScoreDetails.Value.Should().NotBe(0); + result.ScoreDetails.Description.Should().Contain("life liberty and the pursuit of happiness"); + result.ScoreDetails.Value.Should().NotBe(0); - foreach (var scoreDetail in result.ScoreDetails.Details) - { - scoreDetail.Value.Should().NotBe(0); - } + var scoreDetail = result.ScoreDetails.Details.Should().ContainSingle().Subject; + scoreDetail.Description.Should().NotBeNullOrEmpty(); + scoreDetail.Value.Should().NotBe(0); + scoreDetail.Details.Should().NotBeEmpty(); } [Fact] From dc82d107addb146b6f4ed30d65c4fdbcf372adb0 Mon Sep 17 00:00:00 2001 From: Samir Boulema Date: Tue, 11 Jul 2023 23:23:23 +0200 Subject: [PATCH 3/3] CSHARP-4703: Improve SearchScoreDetails comment --- src/MongoDB.Driver/Search/SearchScoreDetails.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/Search/SearchScoreDetails.cs b/src/MongoDB.Driver/Search/SearchScoreDetails.cs index 01426a9c3c7..897a9b0143f 100644 --- a/src/MongoDB.Driver/Search/SearchScoreDetails.cs +++ b/src/MongoDB.Driver/Search/SearchScoreDetails.cs @@ -19,7 +19,7 @@ namespace MongoDB.Driver.Search { /// - /// A search count result set. + /// Represents the scoreDetails object for a document in the result. /// public sealed class SearchScoreDetails {