【问题标题】:LINQ to Entities search multiple columns, order by total weight of the matches columnsLINQ to Entities 搜索多个列,按匹配列的总权重排序
【发布时间】:2016-05-14 12:28:21
【问题描述】:

我正在实现一个搜索算法,我需要通过多列搜索数据库。然后该算法将返回“最佳匹配”。例如,假设我有一个实体:

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

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

matchedColumn1Weight + matchedColumn2Weight + ... + matchedColumnNWeight

假设我有一张名为 people 的表:

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 进行搜索,它应该首先返回 James,然后是 Bob,然后是 Alice,然后是 Will。

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

【问题讨论】:

    标签: c# entity-framework linq linq-to-entities


    【解决方案1】:

    让我们从查询开始:

    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
    

    动态查询部分可以通过使用我最近编写并发布在"Nullable object must have a value" exception after checking for null on a non-primitive/non-struct objectHow to write dynamic where clause for join range varible 的帮助方法来简单地优化。你只需要在最后说:

    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
    

    附:以下是所用方法的源代码:

    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);
            }
        }
    }
    

    【讨论】:

    • 这很棒!我怎么完全忘了let关键字:)
    • 您确定这适用于导航属性吗?我有一个IQueryable,其中包含导航属性,并且导航属性上的let 条件在没有明显原因的情况下失败。非导航属性(例如直接字符串属性上的字符串匹配)工作正常。
    • 我不能说 100%,但如果你给我一个例子,我可以检查一下 :) 像 Person.Department.Name 之类的东西?
    • 没错。但事实证明这是我身边的一个偷偷摸摸的错误。 是的,它完美地工作:)
    【解决方案2】:

    您可以使您的类 IComparable 类似于下面的 Sort 方法使用的代码。您可以使用类似的代码创建更复杂的排序算法。 CompareTo 返回 -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;
            }
    

    【讨论】:

    • 这会不会被翻译成SQL没有问题?
    • 是的。 sort 方法使用 Net 库中内置的默认 CompareTo() 方法。使用自定义 CompareTo() 添加 IComparable 会覆盖默认方法。所以它适用于任何方法,如排序、比较、相等。
    • 哦,那么也许我可以编写一个辅助方法而不是直接写入类,因为“this”排序算法并非总是如此。谢谢,我试试看。
    • 但这只是排序部分,我首先要如何编写复合查询本身?
    • 肯定不会被翻译成SQL
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-22
    • 2011-05-23
    • 1970-01-01
    相关资源
    最近更新 更多