【问题标题】:Django sort by distanceDjango按距离排序
【发布时间】:2013-11-11 07:24:40
【问题描述】:

我有以下型号:

class Vacancy(models.Model):
    lat = models.FloatField('Latitude', blank=True)
    lng = models.FloatField('Longitude', blank=True)

我应该如何进行查询以按距离排序(距离无穷大)?

如果需要,可以使用 PosgreSQL、GeoDjango。

【问题讨论】:

  • 什么距离。距离什么?
  • 无限制。只需从最近到远排序。
  • @FallenAngel 更新文档链接!

标签: python django geodjango


【解决方案1】:

.distance(ref_location) 已在 django >=1.9 中删除,您应该改用注释。

from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.measure import D
from django.contrib.gis.geos import Point

ref_location = Point(1.232433, 1.2323232, srid=4326)
yourmodel.objects.filter(location__distance_lte=(ref_location, D(m=2000)))                                                     
    .annotate(distance=Distance("location", ref_location))                                                                
    .order_by("distance")

您还应该使用使用空间索引的dwithin 运算符缩小搜索范围,距离不使用会减慢查询速度的索引:

yourmodel.objects.filter(location__dwithin=(ref_location, 0.02))
    .filter(location__distance_lte=(ref_location, D(m=2000)))
    .annotate(distance=Distance('location', ref_location))
    .order_by('distance')

有关location__dwithin=(ref_location, 0.02)的解释,请参阅this post

【讨论】:

  • 注意:D 实际上是 django.contrib.gis.measure.Distance,与模型函数同名
  • 我阅读了这个和链接的响应,你如何定义geom 变量?
  • 如果geography=True 在位置上(应该是纬度/经度),那么dwithin 在球体表面上的单位是米,而不是度数。这意味着您不需要distance_lte
  • 正确如果你使用geography而不是geometry postgis可以使用索引来表示距离。
  • @KaleabWoldemariam ST_Within() 通过存储的 EPSG 单位测量距离。例如,我正在使用精度为 1 米的 EPSG:3067,因此我使用filter(location__dwithin=(ref_location, 200.0)) 来查询“200 米内”。如有必要,您可以使用变量。
【解决方案2】:

这是一个不需要 GeoDjango 的解决方案。

from django.db import models
from django.db.models.expressions import RawSQL


class Location(models.Model):
    latitude = models.FloatField()
    longitude = models.FloatField()
    ...


def get_locations_nearby_coords(latitude, longitude, max_distance=None):
    """
    Return objects sorted by distance to specified coordinates
    which distance is less than max_distance given in kilometers
    """
    # Great circle distance formula
    gcd_formula = "6371 * acos(least(greatest(\
    cos(radians(%s)) * cos(radians(latitude)) \
    * cos(radians(longitude) - radians(%s)) + \
    sin(radians(%s)) * sin(radians(latitude)) \
    , -1), 1))"
    distance_raw_sql = RawSQL(
        gcd_formula,
        (latitude, longitude, latitude)
    )
    qs = Location.objects.all() \
    .annotate(distance=distance_raw_sql))\
    .order_by('distance')
    if max_distance is not None:
        qs = qs.filter(distance__lt=max_distance)
    return qs

使用如下:

nearby_locations = get_locations_nearby_coords(48.8582, 2.2945, 5)

如果您使用的是 sqlite,则需要在某处添加

import math
from django.db.backends.signals import connection_created
from django.dispatch import receiver


@receiver(connection_created)
def extend_sqlite(connection=None, **kwargs):
    if connection.vendor == "sqlite":
        # sqlite doesn't natively support math functions, so add them
        cf = connection.connection.create_function
        cf('acos', 1, math.acos)
        cf('cos', 1, math.cos)
        cf('radians', 1, math.radians)
        cf('sin', 1, math.sin)
        cf('least', 2, min)
        cf('greatest', 2, max)

【讨论】:

  • 与在 PostGIS 中执行此操作的性能相比如何?我猜它会更慢,但计算将在 EC2 服务器而不是 RDS 上完成,对吧?如果我们只需要一个八边形甚至正方形的粗略距离,那么使用自定义管理器会比我假设的 PostGIS 好得多?
  • 真的很喜欢这个解决方案,不需要 django GIS,不需要安装库,只是一些数学运算。我想知道这是否适用于任何数据库?对我来说,它在 mysql 上完美运行。到目前为止没有性能问题,但只查询了大约 100 条记录。
  • 唯一的问题是 .extra() 似乎已被弃用?!
  • 在 lat 50,8120466 lng 19,113213 遇到问题,并显示消息:“错误:输入超出范围”。也许这与stackoverflow.com/questions/2533386/…有关?
  • 可能相关确实,我可以在acos函数中添加least(greatest(...),-1),1)
【解决方案3】:

注意:请查看下面提到的 cleder 的回答 Django 版本中的弃用问题(距离 -> 注释)。

首先,最好做一个点域,而不是把lat和lnt分开:

from django.contrib.gis.db import models

location = models.PointField(null=False, blank=False, srid=4326, verbose_name='Location')

然后,你可以像这样过滤它:

from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D

distance = 2000 
ref_location = Point(1.232433, 1.2323232)

res = YourModel.objects.filter(
    location__distance_lte=(
        ref_location,
        D(m=distance)
    )
).distance(
    ref_location
).order_by(
    'distance'
)

【讨论】:

  • 那是我想了解的,为什么在这个查询过滤器中,当我不能只是:res = mymodel.objects.order_by('location')
  • 如果你想对位置进行排序,应该有一个参考位置。您可以围绕特定地点对记录进行排序。
  • 在你的查询中:D(m=distance), distance is it variable ?
  • 是的,我也在添加它,它是最远的地方,以米为单位。
  • @RobertJohnstone 以下是没有 POSTGIS 的方法stackoverflow.com/a/64746573/13789135
【解决方案4】:

此方面的最佳实践变化很快,因此我将回答我认为截至 2020 年 1 月 18 日最新的内容。

使用 GeoDjango

geography=True 与GeoDjango 一起使用会使这变得更容易。这意味着一切都存储在 lng/lat 中,但距离计算在球体表面上以米为单位进行。 See the docs

from django.db import models
from django.contrib.gis.db.models import PointField

class Vacancy(models.Model):
    location = PointField(srid=4326, geography=True, blank=True, null=True)

Django 3.0

如果您有 Django 3.0,则可以使用以下查询对整个表进行排序。它使用 postgis 的 <-> 运算符,这意味着排序将使用空间索引,并且带注释的距离将是准确的(对于 Postgres 9.5+)。请注意,“按距离排序”隐含地需要距离 一些东西。 Point 的第一个参数是经度,第二个是纬度(与正常约定相反)。

from django.contrib.gis.db.models.functions import GeometryDistance
from django.contrib.gis.geos import Point

ref_location = Point(140.0, 40.0, srid=4326)
Vacancy.objects.order_by(GeometryDistance("location", ref_location))

如果您想以任何方式使用与参考点的距离,则需要对其进行注释:

Vacancy.objects.annotate(distance=GeometryDistance("location", ref_location))\
    .order_by("distance")

如果您有很多结果,计算每个条目的确切距离仍然会很慢。您应该使用以下方法之一减少结果数量:

使用查询集切片限制结果数

<-> 运算符不会计算它不会返回的(大多数)结果的精确距离,因此对结果进行切片或分页会很快。要获得前 100 个结果:

Vacancy.objects.annotate(distance=GeometryDistance("location", ref_location))\
    .order_by("distance")[:100]

dwithin只能得到一定距离内的结果

如果您想要获得结果的最大距离,您应该使用dwithindwithin django 查询使用 ST_DWithin,这意味着它非常快。 设置 geography=True 表示此计算以米为单位,而不是度数。 对 50 公里内的所有内容的最终查询将是:

Vacancy.objects.filter(location__dwithin=(ref_location, 50000))\
    .annotate(distance=GeometryDistance("location", ref_location))\
    .order_by("distance")

这可以稍微加快查询速度,即使您将结果分成几个部分。

dwithin 的第二个参数也接受 django.contrib.gis.measure.D 对象,它将其转换为米,因此您可以使用 D(km=50) 代替 50000 米。

过滤距离

您可以直接对带注释的distance 进行过滤,但它会复制<-> 调用,并且比dwithin 慢很多。

Vacancy.objects.annotate(distance=GeometryDistance("location", ref_location))\
    .filter(distance__lte=50000)\
    .order_by("distance")

Django 2.X

如果您没有 Django 3.0,您仍然可以使用 Distance 而不是 GeometryDistance 对整个表进行排序,但它使用 ST_Distance,如果对每个条目都进行排序可能会很慢并且有很多的条目。如果是这种情况,您可以使用dwithin 来缩小结果范围。

请注意,切片不会很快,因为Distance 需要计算所有事物的确切距离以便对结果进行排序。

没有 GeoDjango

如果您没有 GeoDjango,则需要一个 sql 公式来计算距离。效率和正确性因答案而异(尤其是在极点/日期变更线附近),但总的来说会相当慢。

加快查询速度的一种方法是对latlng 进行索引,并在标注距离之前为每个使用最小值/最大值。数学相当复杂,因为边界“框”并不完全是一个框。见这里:How to calculate the bounding box for a given lat/lng location?

【讨论】:

    【解决方案5】:

    在 Django 3.0 上会有一个 GeometryDistance 函数,它的工作方式与 Distance 相同,但使用 <-> operator 代替,它在 ORDER BY 查询上使用空间索引,从而无需 @987654327 @过滤器:

    from django.contrib.gis.db.models.functions import GeometryDistance
    from django.contrib.gis.geos import Point
    
    ref_location = Point(140.0, 40.0, srid=4326)
    Vacancy.objects.annotate(
        distance=GeometryDistance('location', ref_location)
    ).order_by('distance')
    

    如果你想在 Django 3.0 发布之前使用它,你可以使用这样的东西:

    from django.contrib.gis.db.models.functions import GeoFunc
    from django.db.models import FloatField
    from django.db.models.expressions import Func
    
    class GeometryDistance(GeoFunc):
       output_field = FloatField()
       arity = 2
       function = ''
       arg_joiner = ' <-> '
       geom_param_pos = (0, 1)
    
       def as_sql(self, *args, **kwargs):
           return Func.as_sql(self, *args, **kwargs)
    

    【讨论】:

      【解决方案6】:

      如果您不想/没有机会使用 gis,这里是解决方案(django orm sql 中的haversine distance fomula writter):

      lat = 52.100
      lng = 21.021
      
      earth_radius=Value(6371.0, output_field=FloatField())
      
      f1=Func(F('latitude'), function='RADIANS')
      latitude2=Value(lat, output_field=FloatField())
      f2=Func(latitude2, function='RADIANS')
      
      l1=Func(F('longitude'), function='RADIANS')
      longitude2=Value(lng, output_field=FloatField())
      l2=Func(longitude2, function='RADIANS')
      
      d_lat=Func(F('latitude'), function='RADIANS') - f2
      d_lng=Func(F('longitude'), function='RADIANS') - l2
      
      sin_lat = Func(d_lat/2, function='SIN')
      cos_lat1 = Func(f1, function='COS')
      cos_lat2 = Func(f2, function='COS')
      sin_lng = Func(d_lng/2, function='SIN')
      
      a = Func(sin_lat, 2, function='POW') + cos_lat1 * cos_lat2 * Func(sin_lng, 2, function='POW')
      c = 2 * Func(Func(a, function='SQRT'), Func(1 - a, function='SQRT'), function='ATAN2')
      d = earth_radius * c
      
      Shop.objects.annotate(d=d).filter(d__lte=10.0)
      

      PS 更改模型,将过滤器更改为 order_by,更改关键字和参数化

      PS2 对于 sqlite3,您应该确保有可用的函数 SIN、COS、RADIANS、ATAN2、SQRT

      【讨论】:

        【解决方案7】:

        没有 POSTGIS

        如果您不想更改模型,即,将 lat 和 lng 保留为单独的字段,甚至不想使用太多 Geodjango 并想用一些基本代码解决这个问题,那么这里就是解决方案;

        origin = (some_latitude, some_longitude) #coordinates from where you want to measure distance
        distance = {} #creating a dict which will store the distance of users.I am using usernames as keys and the distance as values.
        for m in models.objects.all():
            dest = (m.latitude, m.longitude)
            distance[m.username] = round(geodesic(origin, dest).kilometers, 2) #here i am using geodesic function which takes two arguments, origin(coordinates from where the distance is to be calculated) and dest(to which distance is to be calculated) and round function rounds off the float to two decimal places
        
        #Here i sort the distance dict as per value.So minimum distant users will be first.
        s_d = sorted(distance.items(), key=lambda x: x[1]) #note that sorted function returns a list of tuples as a result not a dict.Those tuples have keys as their first elements and vaues as 2nd.
        
        new_model_list = []
        for i in range(len(s_d)):
            new_model_list.append(models.objects.get(username=s_d[i][0]))
        

        现在 new_model_list 将包含所有按距离排序的用户。通过迭代它,您将根据距离对它们进行排序。

        使用 POSTGIS

        在模型中添加点字段;

        from django.contrib.gis.db import models
        
        class your_model(models.Model):
            coords = models.PointField(null=False, blank=False, srid=4326, verbose_name='coords')
        

        然后在views.py中;

        from django.contrib.gis.db.models.functions import Distance
        from .models import your_model
        
        user = your_model.objects.get(id=some_id) # getting a user with desired id
        
        sortedQueryset = your_model.objects.all().annotate(distance=Distance('coords', user.coords, spheroid=True)).order_by('distance')
        

        Distance 函数将第一个参数作为数据库中的字段,我们必须根据该字段计算距离(此处为坐标)。 第二个参数是计算距离的坐标。

        Spheroid 指定距离的精度。通过将其设置为True,它将提供更准确的距离,否则精度不如Spheroid = False,它将点视为球体上的点(这对地球来说是错误的)。

        【讨论】:

          【解决方案8】:

          在 views.py 中使用 CustomHaystackGEOSpatialFilter 作为 filter_backends :

          class LocationGeoSearchViewSet(HaystackViewSet):
              index_models = [yourModel]
              serializer_class = LocationSerializer
              filter_backends = [CustomHaystackGEOSpatialFilter]
          

          在 filters.py 中定义 CustomHaystackGEOSpatialFilter 并覆盖 apply_filters 方法,以便您可以对距离进行排序并限制结果计数,例如:

          class CustomHaystackGEOSpatialFilter(HaystackGEOSpatialFilter):
              # point_field = 'location'
             def apply_filters(self, queryset, applicable_filters=None, applicable_exclusions=None):
                  if applicable_filters:
                      queryset = queryset.dwithin(**applicable_filters["dwithin"]).distance(
                          **applicable_filters["distance"]).order_by("distance")[:100]
                  return queryset
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2020-09-29
            • 2018-02-02
            • 2015-04-27
            • 1970-01-01
            • 2013-12-05
            • 2014-05-08
            • 2017-07-29
            相关资源
            最近更新 更多