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;