我正在尝试在 Django 中处理基本商店定位器的邻近搜索。我不想在我的应用程序中使用 PostGIS 来使用 GeoDjango 的距离过滤器,而是想在模型查询中使用余弦球面定律距离公式。为了提高效率,我希望所有计算都在数据库中通过一个查询完成。
来自互联网的 MySQL 查询示例,实现余弦球面定律,如下所示:
SELECT id, (
3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) *
cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) *
sin( radians( lat ) ) )
)
AS distance FROM stores HAVING distance < 25 ORDER BY distance LIMIT 0 , 20;
查询需要引用每个商店的纬度/经度值的邮政编码外键。如何在 Django 模型查询中完成所有这些工作?
可以在 Django 中执行原始 SQL 查询。
我的建议是,编写查询来提取 ID 列表(看起来就像您现在正在做的那样),然后使用 ID 来提取关联的模型(在常规的非原始 SQL Django 查询中)。尽量保持 SQL 与方言无关,这样当您需要切换数据库时就不必再担心一件事了。
为了澄清这一点,这里有一个如何操作的示例:
def get_models_within_25 (self):
from django.db import connection, transaction
cursor = connection.cursor()
cursor.execute("""SELECT id, (
3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) *
cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) *
sin( radians( lat ) ) ) )
AS distance FROM stores HAVING distance < 25
ORDER BY distance LIMIT 0 , 20;""")
ids = [row[0] for row in cursor.fetchall()]
return MyModel.filter(id__in=ids)
作为免责声明,我不能保证这段代码,因为我已经有几个月没有编写任何 Django 了,但它应该是正确的。
跟进 Tom 的回答,默认情况下它在 SQLite 中不起作用,因为 SQLite 默认情况下缺乏数学函数。没问题,添加起来非常简单:
class LocationManager(models.Manager):
def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True):
if use_miles:
distance_unit = 3959
else:
distance_unit = 6371
from django.db import connection, transaction
from mysite import settings
cursor = connection.cursor()
if settings.DATABASE_ENGINE == 'sqlite3':
connection.connection.create_function('acos', 1, math.acos)
connection.connection.create_function('cos', 1, math.cos)
connection.connection.create_function('radians', 1, math.radians)
connection.connection.create_function('sin', 1, math.sin)
sql = """SELECT id, (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) )
AS distance FROM location_location WHERE distance < %d
ORDER BY distance LIMIT 0 , %d;""" % (distance_unit, latitude, longitude, latitude, int(radius), max_results)
cursor.execute(sql)
ids = [row[0] for row in cursor.fetchall()]
return self.filter(id__in=ids)
跟进 Tom,如果你想要一个在 postgresql 中也能工作的查询,你不能使用 AS,因为你会得到一个错误,说“距离”不存在。
您应该将整个球面定律表达式放在 WHERE 子句中,如下所示(它也适用于 mysql):
import math
from django.db import connection, transaction
from django.conf import settings
from django .db import models
class LocationManager(models.Manager):
def nearby_locations(self, latitude, longitude, radius, use_miles=False):
if use_miles:
distance_unit = 3959
else:
distance_unit = 6371
cursor = connection.cursor()
sql = """SELECT id, latitude, longitude FROM locations_location WHERE (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) ) < %d
""" % (distance_unit, latitude, longitude, latitude, int(radius))
cursor.execute(sql)
ids = [row[0] for row in cursor.fetchall()]
return self.filter(id__in=ids)
请注意,您必须选择纬度和经度,否则无法在 WHERE 子句中使用它。
为了跟进 jboxer 的答案,以下是作为自定义管理器一部分的整个内容,其中一些硬编码的内容变成了变量:
class LocationManager(models.Manager):
def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True):
if use_miles:
distance_unit = 3959
else:
distance_unit = 6371
from django.db import connection, transaction
cursor = connection.cursor()
sql = """SELECT id, (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) )
AS distance FROM locations_location HAVING distance < %d
ORDER BY distance LIMIT 0 , %d;""" % (distance_unit, latitude, longitude, latitude, int(radius), max_results)
cursor.execute(sql)
ids = [row[0] for row in cursor.fetchall()]
return self.filter(id__in=ids)
关注 jboxer 的回复
def find_cars_within_miles_from_postcode(request, miles, postcode=0):
# create cursor for RAW query
cursor = connection.cursor()
# Get lat and lon from google
lat, lon = getLonLatFromPostcode(postcode)
# Gen query
query = "SELECT id, ((ACOS(SIN("+lat+" * PI() / 180) * SIN(lat * PI() / 180) + COS("+lat+" * PI() / 180) * COS(lat * PI() / 180) * COS(("+lon+" - lon) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) AS distance FROM app_car HAVING distance<='"+miles+"' ORDER BY distance ASC"
# execute the query
cursor.execute(query)
# grab all the IDS form the sql result
ids = [row[0] for row in cursor.fetchall()]
# find cars from ids
cars = Car.objects.filter(id__in=ids)
# return the Cars with these IDS
return HttpResponse( cars )
这可以让我的汽车行驶 x 英里,效果很好。然而,原始查询返回了它们距某个位置的距离,我认为字段名称是“距离”。
如何用我的汽车对象返回此字段“距离”?
使用上面提出的一些答案,我得到的结果不一致,所以我决定再次检查方程 使用[此链接]http://www.movable-type.co.uk/scripts/latlong.html作为参考,方程为
d = acos(sin(lat1)*sin(lat2) + cos(lat1)*cos(lat2)*cos(lon2-lon1) ) * 6371
其中 d
是要计算的距离,
lat1,lon1
是基点的坐标,lat2,lon2
是其他点的坐标,在我们的例子中是数据库中的点。
从上面的答案来看,
LocationManager
类看起来像这样
class LocationManager(models.Manager):
def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True):
if use_miles:
distance_unit = 3959
else:
distance_unit = 6371
from django.db import connection, transaction
from mysite import settings
cursor = connection.cursor()
if settings.DATABASE_ENGINE == 'sqlite3':
connection.connection.create_function('acos', 1, math.acos)
connection.connection.create_function('cos', 1, math.cos)
connection.connection.create_function('radians', 1, math.radians)
connection.connection.create_function('sin', 1, math.sin)
sql = """SELECT id, (acos(sin(radians(%f)) * sin(radians(latitude)) + cos(radians(%f))
* cos(radians(latitude)) * cos(radians(%f-longitude))) * %d)
AS distance FROM skills_coveragearea WHERE distance < %f
ORDER BY distance LIMIT 0 , %d;""" % (latitude, latitude, longitude,distance_unit, radius, max_results)
cursor.execute(sql)
ids = [row[0] for row in cursor.fetchall()]
return self.filter(id__in=ids)
使用网站[链接]http://www.movable-type.co.uk/scripts/latlong.html作为检查,我的结果一致。
也可以使用 Django 的数据库函数来完成此操作,这意味着您可以使用
distance_miles
调用添加 .annotate()
列,然后按其排序。这是一个例子:
from django.db.models import F
from django.db.models.functions import ACos, Cos, Radians, Sin
locations = Location.objects.annotate(
distance_miles = ACos(
Cos(
Radians(input_latitude)
) * Cos(
Radians(F('latitude'))
) * Cos(
Radians(F('longitude')) - Radians(input_longitude)
) + Sin(
Radians(input_latitude)
) * Sin(Radians(F('latitude')))
) * 3959
).order_by('distance_miles')[:10]
@classmethod def附近_位置(cls,纬度,经度,半径,max_results = 1000,use_miles = False): 如果使用_英里: 距离单位 = 3959 别的: 距离单位 = 6371000
from django.db import connection, transaction
from django.conf import settings
cursor = connection.cursor()
sql = """SELECT id, (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) )
AS distance FROM yourapp_yourmodel
GROUP BY id, latitude, longitude
HAVING (%f * acos( cos( radians(%f) ) * cos( radians( latitude ) ) *
cos( radians( longitude ) - radians(%f) ) + sin( radians(%f) ) * sin( radians( latitude ) ) ) ) < %d
ORDER BY distance OFFSET 0 LIMIT %d;""" % (distance_unit, latitude, longitude, latitude, distance_unit, latitude, longitude, latitude, int(radius), max_results)
cursor.execute(sql)
ids = [row[0] for row in cursor.fetchall()]
return cls.objects.filter(id__in=ids)