Сортировка Django по расстоянию

У меня есть следующие модели:

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

Как сделать запрос для сортировки по расстоянию (расстояние бесконечно) ?
Работает на PosgreSQL, GeoDjango, если это необходимо.

спасибо.

5 ответов


прежде всего, лучше сделать поле точки, а не делать 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 *
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')

на .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 с помощью пользовательского менеджера.

class LocationManager(models.Manager):
    def nearby(self, latitude, longitude, proximity):
        """
        Return all object which distance to specified coordinates
        is less than proximity given in kilometers
        """
        # Great circle distance formula
        gcd = """
              6371 * acos(
               cos(radians(%s)) * cos(radians(latitude))
               * cos(radians(longitude) - radians(%s)) +
               sin(radians(%s)) * sin(radians(latitude))
              )
              """
        return self.get_queryset()\
                   .exclude(latitude=None)\
                   .exclude(longitude=None)\
                   .annotate(distance=RawSQL(gcd, (latitude,
                                                   longitude,
                                                   latitude)))\
                   .filter(distance__lt=proximity)\
                   .order_by('distance')


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

использовать как следовать:

eiffel_tower_5k = Location.objects.nearby(48.8582, 2.2945, 5)

Если вы используете sqlite, вам нужно добавить где-то

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)

Если вы не хотите/не имеете возможности использовать ГИС, вот sollution (haversine расстояние fomula writter в django orm sql):

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, измените ключевое слово и parametrize

ПС2 для sqlite3 вы должны убедиться, что есть доступные функции SIN, COS, RADIANS, ATAN2, SQRT


много информации устарело, поэтому я отвечу с тем, что я думаю, является наиболее актуальной информацией.

используя geography=True С GeoDjango делает это намного проще. Это означает, что все хранится в СПГ / лат, но расчеты расстояния производятся в метрах на поверхности сферы. посмотреть документы

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)

вы можете отсортировать всю таблицу, используя следующий запрос, но он использует ST_Distance, который может быть медленным, если это делается на каждой записи и там много записей. Обратите внимание, что" сортировка по расстоянию " неявно требует distance С что-то. Первый аргумент Point - долгота, а вторая-широта (противоположность нормальной конвенции).

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

ref_location = Point(140.0, 40.0, srid=4326)
Vacancy.objects.annotate(distance=Distance("location", ref_location))\
    .order_by("distance")

вы можете оптимизировать свой запрос, если есть максимальное расстояние, для которого вы хотите получить результаты. The dwithin запрос django использует ST_DWithin, что означает, что он очень быстрый. установка geography=True означает, что этот расчет выполняется в метрах, а не степени. это означает, что вам никогда не нужно использовать distance_lte, который использует ST_Distance и будет медленным. Окончательный запрос для всего в пределах 50 км будет:

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

ref_location = Point(140.0, 40.0, srid=4326)
Vacancy.objects.filter(location__dwithin=(ref_location, 50000))\
    .annotate(distance=Distance("location", ref_location))\
    .order_by("distance")

второй аргумент dwithin принимает django.contrib.gis.measure.D объекты, которые он преобразует в метры, так вместо 50000 метров, вы могли бы просто использовать D(km=50).