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

У меня такая модель:

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

Как мне сделать запрос для сортировки по расстоянию (расстояние бесконечно)?

Работаем на PosgreSQL, GeoDjango при необходимости.


person Rukomoynikov    schedule 31.10.2013    source источник
comment
Какое расстояние. Расстояние до чего?   -  person Odif Yltsaeb    schedule 31.10.2013
comment
Без ограничений. Просто отсортируйте от ближайшего к дальнему.   -  person Rukomoynikov    schedule 31.10.2013
comment
@FallenAngel Обновите ссылку на документы!   -  person Drachenfels    schedule 20.10.2016


Ответы (8)


Примечание: пожалуйста, проверьте ответ 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'
)
person cem    schedule 31.10.2013
comment
Я хочу понять, почему в этом фильтре запросов, когда я не могу просто: res = mymodel.objects.order_by('location') - person Rukomoynikov; 31.10.2013
comment
Если вы хотите произвести сортировку по локациям, должна быть ссылка на локацию. Вы можете отсортировать записи по определенному месту. - person cem; 31.10.2013
comment
И в вашем запросе: D (m = расстояние), расстояние переменное? - person Rukomoynikov; 31.10.2013
comment
а да, я тоже добавляю, это самое дальнее место в метрах. - person cem; 31.10.2013
comment
вы также должны изменить импорт в модели, для этого я отредактировал пример. - person cem; 31.10.2013
comment
Есть ли способ сделать это без использования полей PostGIS, таких как Point - person Robert Johnstone; 20.03.2014
comment
@ Sevenearthsü для этого, вы можете произвести те же вычисления тригонометрии с sin и cos, выполните поиск в Google об этом. - person cem; 01.04.2014
comment
Поделитесь, пожалуйста, процессом установки зависимостей geodjango - person Irfan wani; 24.05.2021
comment
@RobertJohnstone Вот как это сделать без POSTGIS stackoverflow.com/a/64746573/13789135 - person Irfan wani; 23.06.2021

.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)

person cleder    schedule 09.03.2016
comment
Примечание: D на самом деле django.contrib.gis.measure.Distance, который, к сожалению, имеет то же имя, что и функция модели. - person Ian Clark; 01.02.2017
comment
Я прочитал это и связанный ответ, как вы определяете переменную geom? - person Harry Moreno; 25.08.2018
comment
Если geography=True на месте (что должно быть для широты / долготы), то dwithin находится в метрах на поверхности сферы, а не в градусах. Это означает, что вам не нужно distance_lte. - person Jonathan Richards; 10.09.2018
comment
правильно, если вы используете geography вместо geometry postgis может использовать индекс для расстояния. - person cleder; 11.09.2018
comment
Используйте свой код, у меня ValueError: невозможно использовать объект с типом кортеж для параметра пространственного поиска. В MySQL. - person goodgrief; 18.06.2019
comment
Что, если он динамический, как местоположение пользователя, как мне сравнить местоположение пользователя, скажем, магазины в пределах 2 км? - person Kaleab Woldemariam; 10.08.2020
comment
Передать текущее местоположение ref_location - person cleder; 12.08.2020
comment
@KaleabWoldemariam ST_Within() измеряет расстояние по сохраненным единицам EPSG. В качестве примера я работаю с EPSG: 3067, точность которого составляет 1 метр, поэтому я использую filter(location__dwithin=(ref_location, 200.0)) для запросов в пределах 200 метров. При необходимости вы можете использовать переменную. - person Jari Turkia; 26.04.2021

Вот решение, для которого не требуется 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)
person rphlo    schedule 06.10.2014
comment
Как производительность этого метода сравнивается с производительностью выполнения этого в PostGIS? Я предполагаю, что это будет медленнее, но вычисления будут выполняться на сервере EC2, а не на RDS, верно? Что, если нам нужно только приблизительное расстояние, на котором подойдет восьмиугольник или даже квадрат, тогда использование настраиваемого менеджера будет значительно лучше, чем PostGIS, как я полагаю? - person davidtgq; 10.01.2016
comment
На самом деле, как это решение, не требуется django GIS, не нужно устанавливать библиотеки, просто немного математики. Интересно, работает ли это на любом БД? Для меня он отлично работает с mysql. Пока нет проблем с производительностью, но запрашивается только около 100 записей. - person benzkji; 16.04.2017
comment
Единственная проблема в том, что .extra (), кажется, устарел ?! - person benzkji; 16.04.2017
comment
@rphlo Это расстояние в милях или километрах? - person Eduardo; 07.08.2018
comment
Расстояние в километрах. - person rphlo; 15.08.2018
comment
Проблемы с lat 50,8120466 lng 19,113213 с сообщением: ERROR: ввод вне допустимого диапазона. Возможно, это связано с stackoverflow.com/questions/2533386/? - person Robert; 03.07.2019
comment
Наверное, связано Действительно, я могу добавить least(greatest(...),-1),1) в функцию acos - person rphlo; 07.08.2019
comment
@ Роберт, вы смогли отсортировать ввод вне диапазона? Я получаю сообщение об ошибке «Широта не существует», возможно, это связано. - person willieswanjala; 02.06.2020
comment
Для SQLite3, куда мы должны добавить код? Вы упомянули, что где-то нужно добавить, но на какой странице вы можете сказать? - person Zahid Wadiwale; 13.06.2021
comment
Вы можете добавить код, специфичный для sql, например, в файл models.py, поскольку django будет просматривать этот файл при запуске. - person rphlo; 14.06.2021

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

С GeoDjango

Использование 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)

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

Если есть максимальное расстояние, на котором вы хотите получить результаты, вы должны использовать 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 2.X

Если у вас нет Django 3.0, вы все равно можете отсортировать всю таблицу, используя Distance вместо GeometryDistance, но он использует ST_Distance, что может быть медленным, если это делается для каждой записи и есть много записей. В этом случае вы можете использовать dwithin, чтобы сузить результаты.

Обратите внимание, что нарезка будет не быстрой, потому что Distance необходимо вычислить точное расстояние для всего, чтобы отсортировать результаты.

Без GeoDjango

Если у вас нет GeoDjango, вам понадобится формула sql для расчета расстояния. Эффективность и правильность варьируются от ответа к ответу (особенно вокруг полюсов / линии дат), но в целом это будет довольно медленно.

Один из способов ускорить запросы - это проиндексировать lat и lng и использовать минимальные / максимальные значения для каждого перед аннотированием расстояния. Математика довольно сложна, потому что ограничивающий «прямоугольник» - это не совсем прямоугольник. См. Здесь: Как рассчитать ограничивающий поле для данного местоположения широты / долготы?

person Jonathan Richards    schedule 10.09.2018

В Django 3.0 будет _ 1_, которая работает так же, как 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)
person Francisco C    schedule 20.03.2019

Если вы не хотите / не имеете возможности использовать gis, вот решение (haversine distance 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, изменить ключевое слово и параметризовать

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

person 404pio    schedule 13.06.2017

БЕЗ 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 функция принимает первый параметр как поле из базы данных, по которому мы должны вычислить расстояние (здесь координаты). 2-й параметр - это координаты, от которых должно быть рассчитано расстояние.

Spheroid указывает точность расстояния. Установив для этого параметра значение True, он даст более точное расстояние, иначе будет менее точным, как для Spheroid = False, он рассматривает точки как точки на сфере (что неверно для Земли).

person Irfan wani    schedule 09.11.2020

в 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
person Mahmoud Masri    schedule 13.12.2020