Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions src/MongoDB.Driver/AggregateFluentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,26 +248,20 @@ public virtual IAggregateFluent<TResult> Search(
SearchCountOptions count = null,
bool returnStoredSource = false,
bool scoreDetails = false)
{
throw new NotImplementedException();
}
=> throw new NotImplementedException();

/// <inheritdoc />
public virtual IAggregateFluent<TResult> Search(
SearchDefinition<TResult> searchDefinition,
SearchOptions<TResult> searchOptions)
{
throw new NotImplementedException();
}
=> throw new NotImplementedException();

/// <inheritdoc />
public virtual IAggregateFluent<SearchMetaResult> SearchMeta(
SearchDefinition<TResult> searchDefinition,
string indexName = null,
SearchCountOptions count = null)
{
throw new NotImplementedException();
}
=> throw new NotImplementedException();

/// <inheritdoc />
public virtual IAggregateFluent<TResult> Set(SetFieldDefinitions<TResult> fields) => throw new NotImplementedException();
Expand Down
1 change: 1 addition & 0 deletions src/MongoDB.Driver/IAggregateFluent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like not necessary change.

using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
Expand Down
94 changes: 94 additions & 0 deletions src/MongoDB.Driver/IAggregateFluentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using MongoDB.Bson.Serialization;
using MongoDB.Driver.Core.Misc;
using MongoDB.Driver.GeoJsonObjectModel;
using MongoDB.Driver.Search;

namespace MongoDB.Driver
{
Expand Down Expand Up @@ -694,6 +695,99 @@ public static IAggregateFluent<TNewResult> ReplaceWith<TResult, TNewResult>(
return aggregate.AppendStage(PipelineStageDefinitionBuilder.ReplaceWith(newRoot));
}

/// <summary>
/// Appends a $search stage to the pipeline, returning documents from a nested scope.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <typeparam name="TNewResult">The type of the new result.</typeparam>
/// <param name="aggregate">The aggregate.</param>
/// <param name="searchDefinition">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="searchOptions">The search options.</param>
/// <returns>The fluent aggregate interface.</returns>
public static IAggregateFluent<TNewResult> Search<TResult, TNewResult>(
this IAggregateFluent<TResult> aggregate,
SearchDefinition<TResult> searchDefinition,
FieldDefinition<TResult, IEnumerable<TNewResult>> returnScope,
SearchOptions<TResult> searchOptions = null)
{
Ensure.IsNotNull(aggregate, nameof(aggregate));
Ensure.IsNotNull(searchDefinition, nameof(searchDefinition));
Ensure.IsNotNull(returnScope, nameof(returnScope));

return aggregate.AppendStage(
PipelineStageDefinitionBuilder.Search(searchDefinition, returnScope, searchOptions));
}

/// <summary>
/// Appends a $search stage to the pipeline, returning documents from a nested scope.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <typeparam name="TNewResult">The type of the new result.</typeparam>
/// <param name="aggregate">The aggregate.</param>
/// <param name="searchDefinition">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="searchOptions">The search options.</param>
/// <returns>The fluent aggregate interface.</returns>
public static IAggregateFluent<TNewResult> Search<TResult, TNewResult>(
this IAggregateFluent<TResult> aggregate,
SearchDefinition<TResult> searchDefinition,
Expression<Func<TResult, IEnumerable<TNewResult>>> returnScope,
SearchOptions<TResult> searchOptions = null)
=> Search(
aggregate,
searchDefinition,
new ExpressionFieldDefinition<TResult, IEnumerable<TNewResult>>(returnScope),
searchOptions);

/// <summary>
/// Appends a $searchMeta stage to the pipeline.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="aggregate">The aggregate.</param>
/// <param name="searchDefinition">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="indexName">The index name.</param>
/// <param name="count">The count options.</param>
/// <returns>The fluent aggregate interface.</returns>
public static IAggregateFluent<SearchMetaResult> SearchMeta<TResult>(
this IAggregateFluent<TResult> aggregate,
SearchDefinition<TResult> searchDefinition,
FieldDefinition<TResult> returnScope,
string indexName = null,
SearchCountOptions count = null)
{
Ensure.IsNotNull(aggregate, nameof(aggregate));
Ensure.IsNotNull(searchDefinition, nameof(searchDefinition));
Ensure.IsNotNull(returnScope, nameof(returnScope));

return aggregate.AppendStage(
PipelineStageDefinitionBuilder.SearchMeta(searchDefinition, returnScope, indexName, count));
}

/// <summary>
/// Appends a $searchMeta stage to the pipeline.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="aggregate">The aggregate.</param>
/// <param name="searchDefinition">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="indexName">The index name.</param>
/// <param name="count">The count options.</param>
/// <returns>The fluent aggregate interface.</returns>
public static IAggregateFluent<SearchMetaResult> SearchMeta<TResult>(
this IAggregateFluent<TResult> aggregate,
SearchDefinition<TResult> searchDefinition,
Expression<Func<TResult, object>> returnScope,
string indexName = null,
SearchCountOptions count = null)
=> SearchMeta(
aggregate,
searchDefinition,
new ExpressionFieldDefinition<TResult>(returnScope),
indexName,
count);

/// <summary>
/// Appends a $set stage to the pipeline.
/// </summary>
Expand Down
43 changes: 43 additions & 0 deletions src/MongoDB.Driver/PipelineDefinitionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,27 @@ public static PipelineDefinition<TInput, TOutput> Search<TInput, TOutput>(
return pipeline.AppendStage(PipelineStageDefinitionBuilder.Search(searchDefinition, searchOptions));
}

/// <summary>
/// Appends a $search stage to the pipeline.
/// </summary>
/// <typeparam name="TInput">The type of the input documents.</typeparam>
/// <typeparam name="TIntermediate">The type of the intermediate documents.</typeparam>
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
/// <param name="pipeline">The pipeline.</param>
/// <param name="searchDefinition">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="searchOptions">The search options.</param>
/// <returns>A new pipeline with an additional stage.</returns>
public static PipelineDefinition<TInput, TOutput> Search<TInput, TIntermediate, TOutput>(
this PipelineDefinition<TInput, TIntermediate> pipeline,
SearchDefinition<TIntermediate> searchDefinition,
FieldDefinition<TIntermediate, IEnumerable<TOutput>> returnScope,
SearchOptions<TIntermediate> searchOptions)
{
Ensure.IsNotNull(pipeline, nameof(pipeline));
return pipeline.AppendStage(PipelineStageDefinitionBuilder.Search(searchDefinition, returnScope, searchOptions));
}

/// <summary>
/// Appends a $searchMeta stage to the pipeline.
/// </summary>
Expand All @@ -1237,6 +1258,28 @@ public static PipelineDefinition<TInput, SearchMetaResult> SearchMeta<TInput, TO
return pipeline.AppendStage(PipelineStageDefinitionBuilder.SearchMeta(query, indexName, count));
}

/// <summary>
/// Appends a $searchMeta stage to the pipeline.
/// </summary>
/// <typeparam name="TInput">The type of the input documents.</typeparam>
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
/// <param name="pipeline">The pipeline.</param>
/// <param name="query">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="indexName">The index name.</param>
/// <param name="count">The count options.</param>
/// <returns>A new pipeline with an additional stage.</returns>
public static PipelineDefinition<TInput, SearchMetaResult> SearchMeta<TInput, TOutput>(
this PipelineDefinition<TInput, TOutput> pipeline,
SearchDefinition<TOutput> query,
FieldDefinition<TOutput> returnScope,
string indexName = null,
SearchCountOptions count = null)
{
Ensure.IsNotNull(pipeline, nameof(pipeline));
return pipeline.AppendStage(PipelineStageDefinitionBuilder.SearchMeta(query, returnScope, indexName, count));
}

/// <summary>
/// Appends a $set stage to the pipeline.
/// </summary>
Expand Down
58 changes: 56 additions & 2 deletions src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,16 +1437,53 @@ public static PipelineStageDefinition<TInput, TInput> Search<TInput>(
public static PipelineStageDefinition<TInput, TInput> Search<TInput>(
SearchDefinition<TInput> searchDefinition,
SearchOptions<TInput> searchOptions)
=> Search<TInput, TInput>(searchDefinition, returnScope: null, searchOptions);

/// <summary>
/// Creates a $search stage.
/// </summary>
/// <typeparam name="TInput">The type of the input documents.</typeparam>
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
/// <param name="searchDefinition">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="searchOptions">The search options.</param>
/// <returns>The stage.</returns>
public static PipelineStageDefinition<TInput, TOutput> Search<TInput, TOutput>(
SearchDefinition<TInput> searchDefinition,
FieldDefinition<TInput, IEnumerable<TOutput>> returnScope,
SearchOptions<TInput> searchOptions)
{
Ensure.IsNotNull(searchDefinition, nameof(searchDefinition));

searchOptions ??= new SearchOptions<TInput>();

const string operatorName = "$search";
var stage = new DelegatedPipelineStageDefinition<TInput, TInput>(
var stage = new DelegatedPipelineStageDefinition<TInput, TOutput>(
operatorName,
args =>
{
ClientSideProjectionHelper.ThrowIfClientSideProjection(args.DocumentSerializer, operatorName);
var renderedSearchDefinition = searchDefinition.Render(args);

IBsonSerializer<TOutput> outputSerializer;
if (returnScope == null)
{
if (typeof(TOutput) != typeof(TInput))
{
throw new InvalidOperationException(
$"The search output type '{typeof(TOutput).Name}' must be the same as the input type '{typeof(TInput).Name}' when 'returnScope' is not specified. Use the overload that specifies 'returnScope' to return documents of a nested collection type.");
}

outputSerializer = (IBsonSerializer<TOutput>)args.DocumentSerializer;
}
else
{
outputSerializer = args.SerializerRegistry.GetSerializer<TOutput>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understood the returnScope is a path inside of the parent document. In such case we have a little more info to resolve the proper serializer. Instead of Lookup, we can try to get member info if DocumentSerializer is IBsonDocumentSerializer. See TryGetMemberSerializationInfo method.

renderedSearchDefinition.Add("returnScope", new BsonDocument { { "path", returnScope.Render(args).FieldName } });
searchOptions = searchOptions.Clone();
searchOptions.ReturnStoredSource = true;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I like the idea to alter user's provided options silently. I know it's a clone. But may be we should simply document it "if your search request uses returnScope, make sure to set ReturnStoredSource to true." BTW, why do we need to set it to true? Does it described somewhere?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also am I right to think that we need all this clone-and-update stuff, only to use in like a 10 lines of code under? May be we can have a local variable instead?

}

renderedSearchDefinition.Add("highlight", () => searchOptions.Highlight.Render(args), searchOptions.Highlight != null);
renderedSearchDefinition.Add("count", () => searchOptions.CountOptions.Render(), searchOptions.CountOptions != null);
renderedSearchDefinition.Add("sort", () => searchOptions.Sort.Render(args), searchOptions.Sort != null);
Expand All @@ -1458,7 +1495,7 @@ public static PipelineStageDefinition<TInput, TInput> Search<TInput>(
renderedSearchDefinition.Add("searchBefore", () => searchOptions.SearchBefore, searchOptions.SearchBefore != null);

var document = new BsonDocument(operatorName, renderedSearchDefinition);
return new RenderedPipelineStageDefinition<TInput>(operatorName, document, args.DocumentSerializer);
return new RenderedPipelineStageDefinition<TOutput>(operatorName, document, outputSerializer);
});

return stage;
Expand All @@ -1476,6 +1513,22 @@ public static PipelineStageDefinition<TInput, SearchMetaResult> SearchMeta<TInpu
SearchDefinition<TInput> searchDefinition,
string indexName = null,
SearchCountOptions count = null)
=> SearchMeta(searchDefinition, returnScope: null, indexName, count);

/// <summary>
/// Creates a $searchMeta stage.
/// </summary>
/// <typeparam name="TInput">The type of the input documents.</typeparam>
/// <param name="searchDefinition">The search definition.</param>
/// <param name="returnScope">The level of nested documents to return.</param>
/// <param name="indexName">The index name.</param>
/// <param name="count">The count options.</param>
/// <returns>The stage.</returns>
public static PipelineStageDefinition<TInput, SearchMetaResult> SearchMeta<TInput>(
SearchDefinition<TInput> searchDefinition,
FieldDefinition<TInput> returnScope,
string indexName = null,
SearchCountOptions count = null)
{
Ensure.IsNotNull(searchDefinition, nameof(searchDefinition));

Expand All @@ -1488,6 +1541,7 @@ public static PipelineStageDefinition<TInput, SearchMetaResult> SearchMeta<TInpu
var renderedSearchDefinition = searchDefinition.Render(args);
renderedSearchDefinition.Add("count", () => count.Render(), count != null);
renderedSearchDefinition.Add("index", indexName, indexName != null);
renderedSearchDefinition.Add("returnScope", () => new BsonDocument { { "path", returnScope!.Render(args).FieldName } }, returnScope != null);

var document = new BsonDocument(operatorName, renderedSearchDefinition);
return new RenderedPipelineStageDefinition<SearchMetaResult>(
Expand Down
46 changes: 46 additions & 0 deletions src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,52 @@ private protected override BsonDocument RenderArguments(
new(_area.Render());
}

internal sealed class HasAncestorSearchDefinition<TDocument> : OperatorSearchDefinition<TDocument>
{
private readonly FieldDefinition<TDocument> _ancestorPath;
private readonly SearchDefinition<TDocument> _operator;

public HasAncestorSearchDefinition(
FieldDefinition<TDocument> ancestorPath,
SearchDefinition<TDocument> @operator,
SearchScoreDefinition<TDocument> score)
: base(OperatorType.HasAncestor, score)
{
_ancestorPath = Ensure.IsNotNull(ancestorPath, nameof(ancestorPath));
_operator = Ensure.IsNotNull(@operator, nameof(@operator));
}

private protected override BsonDocument RenderArguments(
RenderArgs<TDocument> args,
IBsonSerializer fieldSerializer)
=> new()
{
{ "ancestorPath", _ancestorPath.Render(args).FieldName },
{ "operator", _operator.Render(args with { PathRenderArgs = args.PathRenderArgs with { PathPrefix = null } }) }
};
}

internal sealed class HasRootSearchDefinition<TDocument> : OperatorSearchDefinition<TDocument>
{
private readonly SearchDefinition<TDocument> _operator;

public HasRootSearchDefinition(
SearchDefinition<TDocument> @operator,
SearchScoreDefinition<TDocument> score)
: base(OperatorType.HasRoot, score)
{
_operator = Ensure.IsNotNull(@operator, nameof(@operator));
}

private protected override BsonDocument RenderArguments(
RenderArgs<TDocument> args,
IBsonSerializer fieldSerializer)
=> new()
{
{ "operator", _operator.Render(args with { PathRenderArgs = args.PathRenderArgs with { PathPrefix = null } }) }
};
}

internal sealed class InSearchDefinition<TDocument, TField> : OperatorSearchDefinition<TDocument>
{
private readonly TField[] _values;
Expand Down
6 changes: 6 additions & 0 deletions src/MongoDB.Driver/Search/SearchCountOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public SearchCountType Type
set => _type = value;
}

/// <summary>
/// Creates a clone of the options.
/// </summary>
/// <returns>A clone of the options.</returns>
public SearchCountOptions Clone() => new() { Threshold = Threshold, Type = Type };

internal BsonDocument Render() =>
new()
{
Expand Down
2 changes: 2 additions & 0 deletions src/MongoDB.Driver/Search/SearchDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ private protected enum OperatorType
Facet,
GeoShape,
GeoWithin,
HasAncestor,
HasRoot,
In,
MoreLikeThis,
Near,
Expand Down
Loading