diff --git a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/OtherBuiltinSystemFunctions.cs b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/OtherBuiltinSystemFunctions.cs index a928f889e8..1987679df2 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/OtherBuiltinSystemFunctions.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/OtherBuiltinSystemFunctions.cs @@ -24,17 +24,21 @@ public RRFVisit() true, new List() { - new Type[]{typeof(double[])} + new Type[]{typeof(double[])}, + new Type[]{typeof(double[]), typeof(double[])} }) { } protected override SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context) { - if (methodCallExpression.Arguments.Count == 1 - && methodCallExpression.Arguments[0] is NewArrayExpression argumentsExpressions) + if (methodCallExpression.Arguments.Count != 1 && methodCallExpression.Arguments.Count != 2) + { + throw new DocumentQueryException("Invalid Argument Count."); + } + + if (methodCallExpression.Arguments[0] is NewArrayExpression argumentsExpressions) { - // For RRF, We don't need to care about the first argument, it is the object itself and have no relevance to the computation ReadOnlyCollection functionListExpression = argumentsExpressions.Expressions; List arguments = new List(); foreach (Expression argument in functionListExpression) @@ -65,10 +69,16 @@ protected override SqlScalarExpression VisitImplicit(MethodCallExpression method arguments.Add(ExpressionToSql.VisitNonSubqueryScalarExpression(argument, context)); } + // Append the weight if exists + if (methodCallExpression.Arguments.Count == 2) + { + arguments.Add(ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Arguments[1], context)); + } + return SqlFunctionCallScalarExpression.CreateBuiltin(SqlFunctionCallScalarExpression.Names.RRF, arguments.ToImmutableArray()); } - return null; + throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, "Method {0} is not supported with the given argument list.", methodCallExpression.Method.Name)); } protected override SqlScalarExpression VisitExplicit(MethodCallExpression methodCallExpression, TranslationContext context) diff --git a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqExtensions.cs b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqExtensions.cs index 56f05c9faa..127c6a0554 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqExtensions.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/CosmosLinqExtensions.cs @@ -470,6 +470,28 @@ public static double RRF(params double[] scoringFunctions) throw new NotImplementedException(ClientResources.ExtensionMethodNotImplemented); } + /// + /// This system function is used to combine two or more scores provided by other scoring functions. + /// For more information, see https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/rrf. + /// This method is to be used in LINQ expressions only and will be evaluated on server. + /// There's no implementation provided in the client library. + /// + /// the scoring functions to combine. Valid functions are FullTextScore and VectorDistance. + /// the weights to use for scoring functions + /// Returns the the combined scores of the scoring functions. + /// + /// + /// document.RRF(document.Name.FullTextScore(), document.Address.FullTextScore())); + /// ]]> + /// + /// + public static double RRF(double[] scoringFunctions, double[] weights) + { + // The reason for not defining "this" keyword is because this causes undesirable serialization when call Expression.ToString() on this method + throw new NotImplementedException(ClientResources.ExtensionMethodNotImplemented); + } + /// /// This method generate query definition from LINQ query. /// diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationBaselineTests.TestWeightedRRF.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationBaselineTests.TestWeightedRRF.xml new file mode 100644 index 0000000000..3b40b8d187 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationBaselineTests.TestWeightedRRF.xml @@ -0,0 +1,77 @@ + + + + + RRF(new [] {doc.StringField.FullTextScore(new [] {"test1"}), doc.StringField.FullTextScore(new [] {"test1", "text2"})}, new [] {1, 2})).Select(doc => doc.Pk)]]> + + + + + + + + + + RRF(new [] {doc.StringField.FullTextScore(new [] {"test1"}), doc.StringField.FullTextScore(new [] {"test1", "text2"})}, new [] {1, 2})).Select(doc => doc.Pk)]]> + + + + + + + + + + RRF(new [] {doc.StringField.FullTextScore(new [] {"test1"}), doc.StringField2.FullTextScore(new [] {"test1", "test2", "test3"}), 1, 2})).Select(doc => doc.Pk)]]> + + + + + + + + + + RRF(new [] {1, 2}, new [] {doc.StringField.FullTextScore(new [] {"test1"}), doc.StringField.FullTextScore(new [] {"test1", "text2"})})).Select(doc => doc.Pk)]]> + + + + + + + + + + RRF(new [] {1, doc.StringField.FullTextScore(new [] {"test1"})}, new [] {2, doc.StringField.FullTextScore(new [] {"test1", "text2"})})).Select(doc => doc.Pk)]]> + + + + + + + + + + RRF(new [] {doc.StringField.FullTextScore(new [] {"test1"}), doc.StringField.FullTextScore(new [] {"test1"})}, new [] {2, doc.StringField.FullTextScore(new [] {"test1", "text2"})})).Select(doc => doc.Pk)]]> + + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs index 85242ef652..0cfd83e2af 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs @@ -667,6 +667,95 @@ static DataObject createDataObj(Random random) this.ExecuteTestSuite(inputs); } + [TestMethod] + public void TestWeightedRRF() + { + const int Records = 2; + const int MaxStringLength = 100; + static DataObject createDataObj(Random random) + { + DataObject obj = new DataObject + { + StringField = LinqTestsCommon.RandomString(random, random.Next(MaxStringLength)), + IntField = 1, + Id = Guid.NewGuid().ToString(), + Pk = "Test" + }; + return obj; + } + Func> getQuery = LinqTestsCommon.GenerateTestCosmosData(createDataObj, Records, testContainer); + + List inputs = new List + { + // public static double RRF(double[][] scoringFunctions, double[] weights) + new LinqTestInput("Standard weighted RRF calls", b => getQuery(b) + .OrderByRank(doc => RRF(new double[] + { + doc.StringField.FullTextScore(new string[] { "test1" }), + doc.StringField.FullTextScore(new string[] { "test1", "text2" }) + }, + new double[] { 1.0, 2.0 } )) + .Select(doc => doc.Pk)), + + new LinqTestInput("Standard weighted RRF calls using anonymous types", b => getQuery(b) + .OrderByRank(doc => RRF(new [] + { + doc.StringField.FullTextScore(new string[] { "test1" }), + doc.StringField.FullTextScore(new string[] { "test1", "text2" }) + }, + new [] { 1.0, 2.0 } )) + .Select(doc => doc.Pk)), + + // Negative case: weights are not in an array + new LinqTestInput("Weighted RRF with weights and functions not in a list", b => getQuery(b) + .OrderByRank(doc => RRF(doc.StringField.FullTextScore(new string[] { "test1" }), + doc.StringField2.FullTextScore(new string[] { "test1", "test2", "test3" }), + 1.0, + 2.0)) + .Select(doc => doc.Pk)), + new LinqTestInput("Weighted RRF with weights array first", b => getQuery(b) + .OrderByRank(doc => RRF(new double[] { 1.0, 2.0 }, + new double[] + { + doc.StringField.FullTextScore(new string[] { "test1" }), + doc.StringField.FullTextScore(new string[] { "test1", "text2" }) + })) + .Select(doc => doc.Pk)), + new LinqTestInput("Weighted RRF with mixed and matched values/functions in array", b => getQuery(b) + .OrderByRank(doc => RRF(new double[] { + 1.0, + doc.StringField.FullTextScore(new string[] { "test1" }) }, + new double[] + { + 2.0, + doc.StringField.FullTextScore(new string[] { "test1", "text2" }) + })) + .Select(doc => doc.Pk)), + new LinqTestInput("Weighted RRF with mixed and matched values/functions in array 2", b => getQuery(b) + .OrderByRank(doc => RRF(new double[] { + doc.StringField.FullTextScore(new string[] { "test1" }), + doc.StringField.FullTextScore(new string[] { "test1" }) }, + new double[] + { + 2.0, + doc.StringField.FullTextScore(new string[] { "test1", "text2" }) + })) + .Select(doc => doc.Pk)), + + + }; + + foreach (LinqTestInput input in inputs) + { + // OrderBy are not supported client side. + // Therefore this method is verified with baseline only. + input.skipVerification = true; + input.serializeOutput = true; + } + + this.ExecuteTestSuite(inputs); + } + [TestMethod] public void TestOrderByRankFunctionComposeWithOtherFunctions() { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj index 36bab4a278..efc6c67076 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj @@ -250,6 +250,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json index 54e00c2a53..80efed4ee2 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json @@ -6366,6 +6366,11 @@ ], "MethodInfo": "Double FullTextScore[TSource](TSource, System.String[]);IsAbstract:False;IsStatic:True;IsVirtual:False;IsGenericMethod:True;IsConstructor:False;IsFinal:False;" }, + "Double RRF(Double[], Double[])": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Double RRF(Double[], Double[]);IsAbstract:False;IsStatic:True;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Double RRF(Double[])": { "Type": "Method", "Attributes": [],