我有以下模型:
class Vacancy(models.Model):
lat = models.FloatField('Latitude', blank=True)
lng = models.FloatField('Longitude', blank=True)
我应该如何进行查询以按距离排序(距离无穷大)?
如果需要,可以使用 PosgreSQL、GeoDjango。
已.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)
这是一个不需要 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)
注意:请查看下面 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'
)
这种情况的最佳实践变化很快,所以我会回答我认为截至 2020 年 1 月 18 日最新的内容。
geography=True
与 GeoDjango 一起使用使这更容易。这意味着一切都存储在 lng/lat 中,但距离计算在球体表面上以米为单位进行。查看文档
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,则可以使用以下查询对整个表进行排序。它使用 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
如果有您想要结果的最大距离,您应该使用dwithin
. dwithin
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
对象,它转换成米,所以50000
你可以用 . 代替米,而不是米D(km=50)
。
您可以直接在注释上进行过滤distance
,但它会重复<->
调用并且比dwithin
.
Vacancy.objects.annotate(distance=GeometryDistance("location", ref_location))\
.filter(distance__lte=50000)\
.order_by("distance")
如果您没有 Django 3.0,您仍然可以使用Distance
而不是 对整个表进行排序GeometryDistance
,但它使用 ST_Distance,如果对每个条目都进行排序并且有很多条目,这可能会很慢。如果是这种情况,您可以使用dwithin
来缩小结果范围。
请注意,切片不会很快,因为Distance
需要计算所有内容的确切距离才能对结果进行排序。
如果您没有 GeoDjango,则需要一个用于计算距离的 sql 公式。效率和正确性因答案而异(尤其是在极点/日期变更线附近),但总的来说它会相当慢。
一种加快查询速度的方法是在注释距离之前为每个索引lat
和lng
使用最小值/最大值。数学相当复杂,因为边界“框”并不完全是一个框。请参见此处:如何计算给定 lat/lng 位置的边界框?
在 Django 3.0 上会有一个GeometryDistance
函数,它的工作方式与 相同Distance
,但使用<->
运算符代替,它在ORDER BY
查询中使用空间索引,从而无需dwithin
过滤器:
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)
如果您不想/没有机会使用 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 for sqlite3, 你应该确保有可用的函数 SIN, COS, RADIANS, ATAN2, SQRT
没有 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
,它将点视为球体上的点(这对地球来说是错误的)。
在 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