diff --git a/src/EFCore.Analyzers/EFDiagnostics.cs b/src/EFCore.Analyzers/EFDiagnostics.cs index c3021424089..13d885f600c 100644 --- a/src/EFCore.Analyzers/EFDiagnostics.cs +++ b/src/EFCore.Analyzers/EFDiagnostics.cs @@ -19,4 +19,5 @@ public static class EFDiagnostics public const string MetricsExperimental = "EF9101"; public const string PagingExperimental = "EF9102"; public const string CosmosVectorSearchExperimental = "EF9103"; + public const string CosmosFullTextSearchExperimental = "EF9104"; } diff --git a/src/EFCore.Cosmos/EFCore.Cosmos.csproj b/src/EFCore.Cosmos/EFCore.Cosmos.csproj index e5189d571d3..fe1ba24c8a0 100644 --- a/src/EFCore.Cosmos/EFCore.Cosmos.csproj +++ b/src/EFCore.Cosmos/EFCore.Cosmos.csproj @@ -12,6 +12,7 @@ $(NoWarn);EF9101 $(NoWarn);EF9102 $(NoWarn);EF9103 + $(NoWarn);EF9104 diff --git a/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs index 3dc681450be..c34b68cd07b 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs @@ -52,6 +52,60 @@ public static T CoalesceUndefined( T expression2) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CoalesceUndefined))); + /// + /// Checks if the specified property contains the given keyword using full-text search. + /// + /// The instance. + /// The property to search. + /// The keyword to search for. + /// if the property contains the keyword; otherwise, . + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static bool FullTextContains(this DbFunctions _, string property, string keyword) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextContains))); + + /// + /// Checks if the specified property contains all the given keywords using full-text search. + /// + /// The instance. + /// The property to search. + /// The keywords to search for. + /// if the property contains all the keywords; otherwise, . + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static bool FullTextContainsAll(this DbFunctions _, string property, params string[] keywords) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextContainsAll))); + + /// + /// Checks if the specified property contains any of the given keywords using full-text search. + /// + /// The instance. + /// The property to search. + /// The keywords to search for. + /// if the property contains any of the keywords; otherwise, . + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static bool FullTextContainsAny(this DbFunctions _, string property, params string[] keywords) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextContainsAny))); + + /// + /// Returns the full-text search score for the specified property and keywords. + /// + /// The instance. + /// The property to score. + /// The keywords to score by. + /// The full-text search score. + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static double FullTextScore(this DbFunctions _, string property, params string[] keywords) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FullTextScore))); + + /// + /// Combines scores provided by two or more specified functions. + /// + /// The instance. + /// The functions to compute the score for. + /// The combined score. + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public static double Rrf(this DbFunctions _, params double[] functions) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Rrf))); + /// /// Returns the distance between two vectors, using the distance function and data type defined using /// + /// Ordering based on scoring function is not supported inside '{orderByDescending}'. Use '{orderBy}' instead. + /// + public static string OrderByDescendingScoringFunction(object? orderByDescending, object? orderBy) + => string.Format( + GetString("OrderByDescendingScoringFunction", nameof(orderByDescending), nameof(orderBy)), + orderByDescending, orderBy); + + /// + /// Only one ordering using scoring function is allowed. Use 'EF.Functions.{rrf}' method to combine multiple scoring functions. + /// + public static string OrderByMultipleScoringFunctionWithoutRrf(object? rrf) + => string.Format( + GetString("OrderByMultipleScoringFunctionWithoutRrf", nameof(rrf)), + rrf); + + /// + /// Ordering using a scoring function is mutually exclusive with other forms of ordering. + /// + public static string OrderByScoringFunctionMixedWithRegularOrderby + => GetString("OrderByScoringFunctionMixedWithRegularOrderby"); + /// /// The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index 8f9a875524b..09182e651e6 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -283,6 +283,15 @@ Exactly one of '{param1}' or '{param2}' must be set. + + Ordering based on scoring function is not supported inside '{orderByDescending}'. Use '{orderBy}' instead. + + + Only one ordering using scoring function is allowed. Use 'EF.Functions.{rrf}' method to combine multiple scoring functions. + + + Ordering using a scoring function is mutually exclusive with other forms of ordering. + The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs index 352e3d443e1..000c449a7c0 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs @@ -36,7 +36,8 @@ public CosmosMethodCallTranslatorProvider( new CosmosRegexTranslator(sqlExpressionFactory), new CosmosStringMethodTranslator(sqlExpressionFactory), new CosmosTypeCheckingTranslator(sqlExpressionFactory), - new CosmosVectorSearchTranslator(sqlExpressionFactory, typeMappingSource) + new CosmosVectorSearchTranslator(sqlExpressionFactory, typeMappingSource), + new CosmosFullTextSearchTranslator(sqlExpressionFactory, typeMappingSource) //new LikeTranslator(sqlExpressionFactory), //new EnumHasFlagTranslator(sqlExpressionFactory), //new GetValueOrDefaultTranslator(sqlExpressionFactory), diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 67b1437208a..5da126d0827 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -14,6 +14,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// public class CosmosQuerySqlGenerator(ITypeMappingSource typeMappingSource) : SqlExpressionVisitor { + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + private readonly IndentedStringBuilder _sqlBuilder = new(); private IReadOnlyDictionary _parameterValues = null!; private List _sqlParameters = null!; @@ -341,6 +344,15 @@ protected override Expression VisitSelect(SelectExpression selectExpression) { _sqlBuilder.AppendLine().Append("ORDER BY "); + var orderByScoringFunction = selectExpression.Orderings is [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }]; + if (!UseOldBehavior35476 && orderByScoringFunction) + { + _sqlBuilder.Append("RANK "); + } + + Check.DebugAssert(UseOldBehavior35476 || orderByScoringFunction || selectExpression.Orderings.All(x => x.Expression is not SqlFunctionExpression { IsScoringFunction: true }), + "Scoring function can only appear as first (and only) ordering, or not at all."); + GenerateList(selectExpression.Orderings, e => Visit(e)); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs index 98563fd0a2a..e23cb211fe9 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs @@ -9,48 +9,129 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public partial class CosmosShapedQueryCompilingExpressionVisitor { - private sealed class InExpressionValuesExpandingExpressionVisitor( + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + + private sealed class ParameterInliner( ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary parametersValues) : ExpressionVisitor { protected override Expression VisitExtension(Expression expression) { - if (expression is InExpression inExpression) + if (!UseOldBehavior35476) { - IReadOnlyList values; + expression = base.VisitExtension(expression); + } - switch (inExpression) + switch (expression) + { + // Inlines array parameter of InExpression, transforming: 'item IN (@valuesArray)' to: 'item IN (value1, value2)' + case InExpression inExpression: { - case { Values: IReadOnlyList values2 }: - values = values2; - break; - - // TODO: IN with subquery (return immediately, nothing to do here) + IReadOnlyList values; - case { ValuesParameter: SqlParameterExpression valuesParameter }: + switch (inExpression) { - var typeMapping = valuesParameter.TypeMapping; - var mutableValues = new List(); - foreach (var value in (IEnumerable)parametersValues[valuesParameter.Name]) + case { Values: IReadOnlyList values2 }: + values = values2; + break; + + // TODO: IN with subquery (return immediately, nothing to do here) + + case { ValuesParameter: SqlParameterExpression valuesParameter }: { - mutableValues.Add(sqlExpressionFactory.Constant(value, value?.GetType() ?? typeof(object), typeMapping)); + var typeMapping = valuesParameter.TypeMapping; + var mutableValues = new List(); + foreach (var value in (IEnumerable)parametersValues[valuesParameter.Name]) + { + mutableValues.Add(sqlExpressionFactory.Constant(value, value?.GetType() ?? typeof(object), typeMapping)); + } + + values = mutableValues; + break; } - values = mutableValues; - break; + default: + throw new UnreachableException(); } - default: - throw new UnreachableException(); + return values.Count == 0 + ? sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(false)) + : sqlExpressionFactory.In((SqlExpression)Visit(inExpression.Item), values); } - return values.Count == 0 - ? sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(false)) - : sqlExpressionFactory.In((SqlExpression)Visit(inExpression.Item), values); - } + // Converts Offset and Limit parameters to constants when ORDER BY RANK is detected in the SelectExpression (i.e. we order by scoring function) + // Cosmos only supports constants in Offset and Limit for this scenario currently (ORDER BY RANK limitation) + case SelectExpression { Orderings: [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }], Limit: var limit, Offset: var offset } hybridSearch + when !UseOldBehavior35476 && (limit is SqlParameterExpression || offset is SqlParameterExpression): + { + if (hybridSearch.Limit is SqlParameterExpression limitPrm) + { + hybridSearch.ApplyLimit( + sqlExpressionFactory.Constant( + parametersValues[limitPrm.Name], + limitPrm.TypeMapping)); + } + + if (hybridSearch.Offset is SqlParameterExpression offsetPrm) + { + hybridSearch.ApplyOffset( + sqlExpressionFactory.Constant( + parametersValues[offsetPrm.Name], + offsetPrm.TypeMapping)); + } + + return base.VisitExtension(expression); + } - return base.VisitExtension(expression); + // Inlines array parameter of full-text functions, transforming FullTextContainsAll(x, @keywordsArray) to FullTextContainsAll(x, keyword1, keyword2)) + case SqlFunctionExpression + { + Name: "FullTextContainsAny" or "FullTextContainsAll", + Arguments: [var property, SqlParameterExpression { TypeMapping: { ElementTypeMapping: var elementTypeMapping }, Type: Type type } keywords] + } fullTextContainsAllAnyFunction + when !UseOldBehavior35476 && type == typeof(string[]): + { + var keywordValues = new List(); + foreach (var value in (IEnumerable)parametersValues[keywords.Name]) + { + keywordValues.Add(sqlExpressionFactory.Constant(value, typeof(string), elementTypeMapping)); + } + + return sqlExpressionFactory.Function( + fullTextContainsAllAnyFunction.Name, + [property, .. keywordValues], + fullTextContainsAllAnyFunction.Type, + fullTextContainsAllAnyFunction.TypeMapping); + } + + // Inlines array parameter of full-text score, transforming FullTextScore(x, @keywordsArray) to FullTextScore(x, [keyword1, keyword2])) + case SqlFunctionExpression + { + Name: "FullTextScore", + IsScoringFunction: true, + Arguments: [var property, SqlParameterExpression { TypeMapping: { ElementTypeMapping: not null } typeMapping } keywords] + } fullTextScoreFunction + when !UseOldBehavior35476: + { + var keywordValues = new List(); + foreach (var value in (IEnumerable)parametersValues[keywords.Name]) + { + keywordValues.Add((string)value); + } + + return new SqlFunctionExpression( + fullTextScoreFunction.Name, + isScoringFunction: true, + [property, sqlExpressionFactory.Constant(keywordValues, typeMapping)], + fullTextScoreFunction.Type, + fullTextScoreFunction.TypeMapping); + } + + default: + return expression; + } } } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs index e90c24664a5..6e84ffa7827 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs @@ -75,7 +75,7 @@ public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken canc private CosmosSqlQuery GenerateQuery() => _querySqlGeneratorFactory.Create().GetSqlQuery( - (SelectExpression)new InExpressionValuesExpandingExpressionVisitor( + (SelectExpression)new ParameterInliner( _sqlExpressionFactory, _cosmosQueryContext.ParameterValues) .Visit(_selectExpression), diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 07b0c22115c..26c7d885cdc 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -71,7 +71,7 @@ IEnumerator IEnumerable.GetEnumerator() private CosmosSqlQuery GenerateQuery() => _querySqlGeneratorFactory.Create().GetSqlQuery( - (SelectExpression)new InExpressionValuesExpandingExpressionVisitor( + (SelectExpression)new ParameterInliner( _sqlExpressionFactory, _cosmosQueryContext.ParameterValues) .Visit(_selectExpression), diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs index cdb0bbff323..3efd431ca2f 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/FragmentExpression.cs @@ -23,6 +23,14 @@ public class FragmentExpression(string fragment) : Expression, IPrintableExpress /// public virtual string Fragment { get; } = fragment; + /// + public override ExpressionType NodeType + => base.NodeType; + + /// + public override Type Type + => typeof(object); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => this; diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index 3f31abdf5a8..5b735680b43 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Cosmos.Extensions; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Internal; @@ -16,6 +17,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; [DebuggerDisplay("{PrintShortSql(), nq}")] public sealed class SelectExpression : Expression, IPrintableExpression { + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + private IDictionary _projectionMapping = new Dictionary(); private readonly List _sources = []; private readonly List _projection = []; @@ -381,6 +385,12 @@ public void ApplyOffset(SqlExpression sqlExpression) /// public void ApplyOrdering(OrderingExpression orderingExpression) { + if (!UseOldBehavior35476 && orderingExpression is { Expression: SqlFunctionExpression { IsScoringFunction: true }, IsAscending: false }) + { + throw new InvalidOperationException( + CosmosStrings.OrderByDescendingScoringFunction(nameof(Queryable.OrderByDescending), nameof(Queryable.OrderBy))); + } + _orderings.Clear(); _orderings.Add(orderingExpression); } @@ -393,6 +403,19 @@ public void ApplyOrdering(OrderingExpression orderingExpression) /// public void AppendOrdering(OrderingExpression orderingExpression) { + if (!UseOldBehavior35476 && _orderings.Count > 0) + { + var existingScoringFunctionOrdering = _orderings is [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }]; + var appendingScoringFunctionOrdering = orderingExpression.Expression is SqlFunctionExpression { IsScoringFunction: true }; + if (appendingScoringFunctionOrdering || existingScoringFunctionOrdering) + { + throw new InvalidOperationException( + appendingScoringFunctionOrdering && existingScoringFunctionOrdering + ? CosmosStrings.OrderByMultipleScoringFunctionWithoutRrf(nameof(CosmosDbFunctionsExtensions.Rrf)) + : CosmosStrings.OrderByScoringFunctionMixedWithRegularOrderby); + } + } + if (_orderings.FirstOrDefault(o => o.Expression.Equals(orderingExpression.Expression)) == null) { _orderings.Add(orderingExpression); @@ -752,6 +775,11 @@ private void PrintSql(ExpressionPrinter expressionPrinter, bool withTags = true) if (Orderings.Any()) { expressionPrinter.AppendLine().Append("ORDER BY "); + if (!UseOldBehavior35476 && Orderings is [{ Expression: SqlFunctionExpression { IsScoringFunction: true } }]) + { + expressionPrinter.Append("RANK "); + } + expressionPrinter.VisitCollection(Orderings); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs index 91b53ca7039..960b2c6f0eb 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs @@ -3,6 +3,8 @@ // ReSharper disable once CheckNamespace +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// @@ -24,10 +26,28 @@ public SqlFunctionExpression( IEnumerable arguments, Type type, CoreTypeMapping? typeMapping) + : this(name, isScoringFunction: false, arguments, type, typeMapping) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public SqlFunctionExpression( + string name, + bool isScoringFunction, + IEnumerable arguments, + Type type, + CoreTypeMapping? typeMapping) : base(type, typeMapping) { Name = name; Arguments = arguments.ToList(); + IsScoringFunction = isScoringFunction; } /// @@ -38,6 +58,15 @@ public SqlFunctionExpression( /// public virtual string Name { get; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [Experimental(EFDiagnostics.CosmosFullTextSearchExperimental)] + public virtual bool IsScoringFunction { get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -63,7 +92,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) } return changed - ? new SqlFunctionExpression(Name, arguments, Type, TypeMapping) + ? new SqlFunctionExpression(Name, IsScoringFunction, arguments, Type, TypeMapping) : this; } @@ -74,7 +103,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SqlFunctionExpression ApplyTypeMapping(CoreTypeMapping? typeMapping) - => new(Name, Arguments, Type, typeMapping ?? TypeMapping); + => new(Name, IsScoringFunction, Arguments, Type, typeMapping ?? TypeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -85,7 +114,7 @@ public virtual SqlFunctionExpression ApplyTypeMapping(CoreTypeMapping? typeMappi public virtual SqlFunctionExpression Update(IReadOnlyList arguments) => arguments.SequenceEqual(Arguments) ? this - : new SqlFunctionExpression(Name, arguments, Type, TypeMapping); + : new SqlFunctionExpression(Name, IsScoringFunction, arguments, Type, TypeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosFullTextSearchTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosFullTextSearchTranslator.cs new file mode 100644 index 00000000000..11c4209100e --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosFullTextSearchTranslator.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Extensions; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class CosmosFullTextSearchTranslator(ISqlExpressionFactory sqlExpressionFactory, ITypeMappingSource typeMappingSource) + : IMethodCallTranslator +{ + private static readonly bool UseOldBehavior35476 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35476", out var enabled35476) && enabled35476; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (UseOldBehavior35476 || method.DeclaringType != typeof(CosmosDbFunctionsExtensions)) + { + return null; + } + + return method.Name switch + { + nameof(CosmosDbFunctionsExtensions.FullTextContains) + when arguments is [_, var property, var keyword] => sqlExpressionFactory.Function( + "FullTextContains", + [ + property, + keyword, + ], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + nameof(CosmosDbFunctionsExtensions.FullTextScore) + when arguments is [_, var property, var keywords] => BuildScoringFunction( + sqlExpressionFactory, + "FullTextScore", + [ + property, + keywords, + ], + typeof(double), + typeMappingSource.FindMapping(typeof(double))), + + nameof(CosmosDbFunctionsExtensions.Rrf) + when arguments is [_, ArrayConstantExpression functions] => BuildScoringFunction( + sqlExpressionFactory, + "RRF", + functions.Items, + typeof(double), + typeMappingSource.FindMapping(typeof(double))), + + nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) or nameof(CosmosDbFunctionsExtensions.FullTextContainsAll) + when arguments is [_, SqlExpression property, SqlConstantExpression { Type: var keywordClrType, Value: string[] values } keywords] + && keywordClrType == typeof(string[]) => sqlExpressionFactory.Function( + method.Name == nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) ? "FullTextContainsAny" : "FullTextContainsAll", + [property, .. values.Select(x => sqlExpressionFactory.Constant(x))], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) or nameof(CosmosDbFunctionsExtensions.FullTextContainsAll) + when arguments is [_, SqlExpression property, SqlParameterExpression { Type: var keywordClrType } keywords] + && keywordClrType == typeof(string[]) => sqlExpressionFactory.Function( + method.Name == nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) ? "FullTextContainsAny" : "FullTextContainsAll", + [property, keywords], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) or nameof(CosmosDbFunctionsExtensions.FullTextContainsAll) + when arguments is [_, SqlExpression property, ArrayConstantExpression keywords] => sqlExpressionFactory.Function( + method.Name == nameof(CosmosDbFunctionsExtensions.FullTextContainsAny) ? "FullTextContainsAny" : "FullTextContainsAll", + [property, .. keywords.Items], + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))), + + _ => null + }; + } + + private SqlExpression BuildScoringFunction( + ISqlExpressionFactory sqlExpressionFactory, + string functionName, + IEnumerable arguments, + Type returnType, + CoreTypeMapping? typeMapping = null) + { + var typeMappedArguments = new List(); + + foreach (var argument in arguments) + { + typeMappedArguments.Add(argument is SqlExpression sqlArgument ? sqlExpressionFactory.ApplyDefaultTypeMapping(sqlArgument) : argument); + } + + return new SqlFunctionExpression( + functionName, + isScoringFunction: true, + typeMappedArguments, + returnType, + typeMapping); + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs index 9c6e62d02a2..d77ddbf4373 100644 --- a/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs +++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosVectorSearchTranslator.cs @@ -18,6 +18,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public class CosmosVectorSearchTranslator(ISqlExpressionFactory sqlExpressionFactory, ITypeMappingSource typeMappingSource) : IMethodCallTranslator { + private static readonly bool UseOldBehavior35853 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35853", out var enabled35853) && enabled35853; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -30,7 +33,15 @@ public class CosmosVectorSearchTranslator(ISqlExpressionFactory sqlExpressionFac IReadOnlyList arguments, IDiagnosticsLogger logger) { - if (method.DeclaringType != typeof(CosmosDbFunctionsExtensions) + if (!UseOldBehavior35853) + { + if (method.DeclaringType != typeof(CosmosDbFunctionsExtensions) + || method.Name != nameof(CosmosDbFunctionsExtensions.VectorDistance)) + { + return null; + } + } + else if (method.DeclaringType != typeof(CosmosDbFunctionsExtensions) && method.Name != nameof(CosmosDbFunctionsExtensions.VectorDistance)) { return null;