LINQ to Entities 搜索多列,按匹配列的总权重排序

LINQ to Entities search multiple columns, order by total weight of the matches columns

我正在实施一种搜索算法,我需要通过多列搜索数据库。然后算法将return一个"best match"。例如,假设我有一个实体:

public class Person{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Title { get; set; }
}

我的搜索方法需要接受对姓名、年龄和职务的搜索,所有这些都是可选的,其中任何组合都是可能的。所有字段都有一个权重,我将在代码中对其进行微调以获得更好的结果。结果应按 score 排序,其中 score 是:

matchedColumn1Weight + matchedColumn2Weight + ... + matchedColumnNWeight

让我们假设我有这个 table 人:

Name     Age    Title
-------------------------
Alice    20     Manager
Bob      21     Friend
James    20     Friend
Will     22     Manager

我们假设 Name 的权重为 1Age 的权重为 1Title 的权重为 1.1。如果我搜索字段 name = null, age = 20, title = Friend,它应该首先 return James,然后是 Bob,然后是 Alice,然后是 Will。

如何在 LINQ-to-Entities 中实现此类功能?换句话说,我需要 a LINQ,我在其中查询多个可选字段,将数据库中的每个项目映射到匹配的列的总分(其中列具有固定的预设权重),然后按此排序score. 怎么做到的?

您可以使 class IComparable 类似于下面的代码,Sort 方法使用该代码。您可以使用类似代码创建更复杂的排序算法。 CompareTo returns -1(更少),0 等于,+1(更多)。

    public class Person : IComparable 
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Title { get; set; }
        public List<string> order { get; set; }


        public int CompareTo(object _other)
        {
            Person other = (Person)_other;
            int results = 0;

            if (this.Name != other.Name)
            {
                results = this.Name.CompareTo(other.Name);
            }
            else
            {
                if (this.Age != other.Age)
                {
                    results = this.Age.CompareTo(other.Age);
                }
                else
                {
                    results = this.Title.CompareTo(other.Title);
                }
            }

            return results;
        }

让我们从查询开始:

const decimal nameWeight = 1, ageWeight = 1, titleWeight = 1.1m;

string name = null;
int? age = 20;
string title = (string)"Friend";

var query = from p in db.Persons
            let nameMatch = name == null || p.Name == name
            let ageMatch = age == null || p.Age == age.Value
            let titleMatch = title == null || p.Title == title
            let score = (nameMatch ? nameWeight : 0) + (ageMatch ? ageWeight : 0) + (titleMatch ? titleWeight : 0)
            where nameMatch || ageMatch || titleMatch
            orderby score descending
            select p;

这会起作用,但是 SQL 查询不是最优的,因为其中嵌入了最优参数。例如,使用上述示例参数,SQL 查询如下所示:

SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[Name] AS [Name], 
    [Project1].[Age] AS [Age], 
    [Project1].[Title] AS [Title]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[Age] AS [Age], 
        [Extent1].[Title] AS [Title], 
        (CASE WHEN ((CASE WHEN (@p__linq__0 IS NULL OR [Extent1].[Name] = @p__linq__1) THEN cast(1 as bit) WHEN ( NOT (@p__linq__0 IS NULL OR [Extent1].[Name] = @p__linq__1)) THEN cast(0 as bit) END) = 1) THEN cast(1 as decimal(18)) ELSE cast(0 as decimal(18)) END) + (CASE WHEN ((CASE WHEN (@p__linq__2 IS NULL OR [Extent1].[Age] = @p__linq__3) THEN cast(1 as bit) WHEN ( NOT (@p__linq__2 IS NULL OR [Extent1].[Age] = @p__linq__3)) THEN cast(0 as bit) END) = 1) THEN cast(1 as decimal(18)) ELSE cast(0 as decimal(18)) END) + (CASE WHEN ((CASE WHEN (@p__linq__4 IS NULL OR [Extent1].[Title] = @p__linq__5) THEN cast(1 as bit) WHEN ( NOT (@p__linq__4 IS NULL OR [Extent1].[Title] = @p__linq__5)) THEN cast(0 as bit) END) = 1) THEN 1.1 ELSE cast(0 as decimal(18)) END) AS [C1]
        FROM [dbo].[People] AS [Extent1]
        WHERE ((CASE WHEN (@p__linq__0 IS NULL OR [Extent1].[Name] = @p__linq__1) THEN cast(1 as bit) WHEN ( NOT (@p__linq__0 IS NULL OR [Extent1].[Name] = @p__linq__1)) THEN cast(0 as bit) END) = 1) OR ((CASE WHEN (@p__linq__2 IS NULL OR [Extent1].[Age] = @p__linq__3) THEN cast(1 as bit) WHEN ( NOT (@p__linq__2 IS NULL OR [Extent1].[Age] = @p__linq__3)) THEN cast(0 as bit) END) = 1) OR ((CASE WHEN (@p__linq__4 IS NULL OR [Extent1].[Title] = @p__linq__5) THEN cast(1 as bit) WHEN ( NOT (@p__linq__4 IS NULL OR [Extent1].[Title] = @p__linq__5)) THEN cast(0 as bit) END) = 1)
    )  AS [Project1]
    ORDER BY [Project1].[C1] DESC

动态查询部分可以简单地使用我最近编写并发布在此处 and here How to write dynamic where clause for join range varibleReduceConstPredicates 辅助方法进行优化。你只需要在最后说:

query = query.ReduceConstPredicates();

生成的SQL变为:

SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[Name] AS [Name], 
    [Project1].[Age] AS [Age], 
    [Project1].[Title] AS [Title]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[Age] AS [Age], 
        [Extent1].[Title] AS [Title], 
        cast(1 as decimal(18)) + (CASE WHEN ((CASE WHEN ([Extent1].[Age] = @p__linq__0) THEN cast(1 as bit) WHEN ([Extent1].[Age] <> @p__linq__0) THEN cast(0 as bit) END) = 1) THEN cast(1 as decimal(18)) ELSE cast(0 as decimal(18)) END) + (CASE WHEN ((CASE WHEN ([Extent1].[Title] = @p__linq__1) THEN cast(1 as bit) WHEN ([Extent1].[Title] <> @p__linq__1) THEN cast(0 as bit) END) = 1) THEN 1.1 ELSE cast(0 as decimal(18)) END) AS [C1]
        FROM [dbo].[People] AS [Extent1]
    )  AS [Project1]
    ORDER BY [Project1].[C1] DESC

P.S。以下是所用方法的源代码:

public static class QueryableExtensions
{
    public static IQueryable<T> ReduceConstPredicates<T>(this IQueryable<T> source)
    {
        var reducer = new ConstPredicateReducer();
        var expression = reducer.Visit(source.Expression);
        if (expression == source.Expression) return source;
        return source.Provider.CreateQuery<T>(expression);
    }

    class ConstPredicateReducer : ExpressionVisitor
    {
        private int evaluateConst;
        private bool EvaluateConst { get { return evaluateConst > 0; } }
        private ConstantExpression TryEvaluateConst(Expression node)
        {
            evaluateConst++;
            try { return Visit(node) as ConstantExpression; }
            catch { return null; }
            finally { evaluateConst--; }
        }
        protected override Expression VisitUnary(UnaryExpression node)
        {
            if (EvaluateConst || node.Type == typeof(bool))
            {
                var operandConst = TryEvaluateConst(node.Operand);
                if (operandConst != null)
                {
                    var result = Expression.Lambda(node.Update(operandConst)).Compile().DynamicInvoke();
                    return Expression.Constant(result, node.Type);
                }
            }
            return EvaluateConst ? node : base.VisitUnary(node);
        }
        protected override Expression VisitBinary(BinaryExpression node)
        {
            if (EvaluateConst || node.Type == typeof(bool))
            {
                var leftConst = TryEvaluateConst(node.Left);
                if (leftConst != null)
                {
                    if (node.NodeType == ExpressionType.AndAlso)
                        return (bool)leftConst.Value ? Visit(node.Right) : Expression.Constant(false);
                    if (node.NodeType == ExpressionType.OrElse)
                        return !(bool)leftConst.Value ? Visit(node.Right) : Expression.Constant(true);
                    var rightConst = TryEvaluateConst(node.Right);
                    if (rightConst != null)
                    {
                        var result = Expression.Lambda(node.Update(leftConst, node.Conversion, rightConst)).Compile().DynamicInvoke();
                        return Expression.Constant(result, node.Type);
                    }
                }
            }
            return EvaluateConst ? node : base.VisitBinary(node);
        }
        protected override Expression VisitConditional(ConditionalExpression node)
        {
            if (EvaluateConst || node.Type == typeof(bool))
            {
                var testConst = TryEvaluateConst(node.Test);
                if (testConst != null)
                    return Visit((bool)testConst.Value ? node.IfTrue : node.IfFalse);
            }
            return EvaluateConst ? node : base.VisitConditional(node);
        }
        protected override Expression VisitMember(MemberExpression node)
        {
            if (EvaluateConst || node.Type == typeof(bool))
            {
                var expressionConst = node.Expression != null ? TryEvaluateConst(node.Expression) : null;
                if (expressionConst != null || node.Expression == null)
                {
                    var result = Expression.Lambda(node.Update(expressionConst)).Compile().DynamicInvoke();
                    return Expression.Constant(result, node.Type);
                }
            }
            return EvaluateConst ? node : base.VisitMember(node);
        }
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (EvaluateConst || node.Type == typeof(bool))
            {
                var objectConst = node.Object != null ? TryEvaluateConst(node.Object) : null;
                if (objectConst != null || node.Object == null)
                {
                    var argumentsConst = new ConstantExpression[node.Arguments.Count];
                    int count = 0;
                    while (count < argumentsConst.Length && (argumentsConst[count] = TryEvaluateConst(node.Arguments[count])) != null)
                        count++;
                    if (count == argumentsConst.Length)
                    {
                        var result = Expression.Lambda(node.Update(objectConst, argumentsConst)).Compile().DynamicInvoke();
                        return Expression.Constant(result, node.Type);
                    }
                }
            }
            return EvaluateConst ? node : base.VisitMethodCall(node);
        }
    }
}