如何基于双列“接近”某个值来查询数据集

本文关键字:查询 数据集 何基于 接近 | 更新日期: 2023-09-27 18:34:43

我有一个参考数据库,用于保存天体网格上物体的坐标。我想查询数据库并查找"接近"(在给定点的某个角距离内(的对象。

我试过这个查询:

const double WithinOneMinute = 1.0 / 60.0;    // 1 minute of arc
var db = CompositionRoot.GetTargetDatabase();
var targets = from item in db.Targets
              where item.RightAscension.IsCloseTo(ra.Value, WithinOneMinute) 
                    && item.Declination.IsCloseTo(dec.Value, WithinOneMinute)
              select item;
var found = targets.ToList();

此操作失败,因为 LINQ 查询提供程序无法理解我的IsCloseTo扩展方法,该方法实现为:

public static bool IsCloseTo(this double comparand, double comparison, double tolerance = EightDecimalPlaces)
{
    var difference = Math.Abs(comparand - comparison);
    return (difference <= tolerance); 
}

所以我目前被困在想法上。有人做过这样的事情吗?

如何基于双列“接近”某个值来查询数据集

正如您已经注意到的,自定义函数不能用作查询表达式树的一部分。因此,您要么必须手动将函数逻辑嵌入到查询中,从而引入大量代码重复,要么切换到方法语法并使用返回整个表达式的帮助程序方法。

后者可以使用System.Linq.Expressions的方法手动完成,但这并不自然,需要大量知识。让我向您介绍一种'更简单的方法。

目标是实现这样的扩展方法

public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces)
{
    return source.Where(...);
}

并按如下方式使用它

var targets = db.Targets
    .WhereIsCloseTo(item => item.RightAscension, ra.Value, WithinOneMinute)
    .WhereIsCloseTo(item => item.Declination, dec.Value, WithinOneMinute);

请注意,使用此方法时,不能使用 && ,但链接Where产生等效的结果。

首先,让我们提供原始函数的表达式等效项

public static Expression<Func<double, bool>> IsCloseTo(double comparison, double tolerance = EightDecimalPlaces)
{
    return comparand => Math.Abs(comparand - comparison) >= tolerance;
}

问题是它不能直接在我们的方法中使用,因为它需要 Expression<Func<T, bool>> .

幸运的是,这可以

通过使用我的答案中的一个小助手实用程序轻松完成,以将表达式的一部分定义为 c# 中的变量:

public static class ExpressionUtils
{
    public static Expression<Func<TOuter, TResult>> Bind<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> source, Expression<Func<TInner, TResult>> resultSelector)
    {
        var body = new ParameterExpressionReplacer { source = resultSelector.Parameters[0], target = source.Body }.Visit(resultSelector.Body);
        var lambda = Expression.Lambda<Func<TOuter, TResult>>(body, source.Parameters);
        return lambda;
    }
    public static Expression<Func<TOuter, TResult>> ApplyTo<TInner, TResult, TOuter>(this Expression<Func<TInner, TResult>> source, Expression<Func<TOuter, TInner>> innerSelector)
    {
        return innerSelector.Bind(source);
    }
    class ParameterExpressionReplacer : ExpressionVisitor
    {
        public ParameterExpression source;
        public Expression target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == source ? target : base.VisitParameter(node);
        }
    }
}

现在我们有了所需的一切,所以我们的方法实现起来很简单,如下所示:

public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces)
{
    return source.Where(IsCloseTo(comparison, tolerance).ApplyTo(comparand));
}

问题是实体框架不知道如何将其转换为SQL。与其在进入数据库后进行过滤,不如让查询包含过滤器,就像编写直接 SQL 一样。它会更冗长一些,但拖动大量数据的成本要低得多,这些数据一旦内置到内存中就会立即被过滤。

您需要做的是将每个时间与您正在寻找的高点和低点进行比较。

// I prefer to move these outside the query for clarity.
var raPlus = ra.Value.AddMinute(1);
var raMinus = ra.Value.AddMinute(-1);
var decPlus = dec.Value.AddMinute(1);
var decMinus = dec.Value.AddMinute(-1);
var targets = from item in db.Targets
              where item.RightAscension <= raPlus &&
                    item.RightAscension >= raMinus &&
                    item.Declination <= decPlus &&
                    item.Declination >= decMinus
              select item;

LINQ(到 EF(查询提供程序不知道如何在 SQL 中执行 IsCloseTo 方法。您需要先枚举您的项目,然后使用扩展方法对其进行过滤,如下所示:

var db = CompositionRoot.GetTargetDatabase();
var targets = from item in db.Targets
              select item;
//now targets will be enumarated and can be querable with LINQ to objects
var filteredTargets = from target in targets.ToList() 
               where target.RightAscension.IsCloseTo(ra.Value, WithinOneMinute) 
               && target.Declination.IsCloseTo(dec.Value, WithinOneMinute)
              select target; 
var filteredTargets = targets.ToList();

这就是我最终做到的:

        const double radius = 1.0;        
        const double radiusHours = radius / 15.0;
        var db = CompositionRoot.GetTargetDatabase();
        var ra = rightAscension.Value;      
        var dec = declination.Value;        
        var minRa = ra - radiusHours;
        var maxRa = ra + radiusHours;
        var minDec = dec - radius;
        var maxDec = dec + radius;
        var closeTargets = from target in db.Targets
                           where target.RightAscension >= minRa 
                                 && target.RightAscension <= maxRa
                                 && target.Declination >= minDec 
                                 && target.Declination <= maxDec
                           let deltaRa = Abs(target.RightAscension - ra) * 15.0 // in degrees
                           let deltaDec = Abs(target.Declination - dec)
                           let distanceSquared = deltaRa * deltaRa + deltaDec * deltaDec
                           orderby distanceSquared
                           select target;

一个轻微的复杂情况是,右升是以小时为单位(每小时 15 度(,而赤纬是以度为单位,所以我必须在几个地方进行调整。

我首先将列表缩小到小半径(在本例中为 1 度(内的物体。事实上,我使用的是 1 度正方形,但这是一个足够好的近似值。然后,我使用毕达哥拉斯按距所需点的距离对项目进行排序(我不取平方根,因为这会再次产生错误,但同样,只需获得正确的排序就足够了(。

最后,我将查询具体化,并将第一个元素作为我的答案。

这仍然不完美,因为它不能处理右升接近零的情况。我最终会与负 RA 进行比较,而不是接近 23:59 的东西 - 但我现在可以忍受它。

结果为语音合成器

提供动力,该语音合成器"宣布"望远镜指向的位置作为物体的名称。很酷的:)如果我偶尔错过一个,那也没关系。