Как найти близкие объекты в django?

У меня есть модель поставщика услуг. В ней есть два поля с именами: lat & lon

class Provider(models.Model):
    name = models.CharField(max_length=100)
    lat = models.CharField(max_length=20)
    lon = models.CharField(max_length=20)
    address = models.TextField(null=True , blank=True)

    def save(self, *args, **kwargs):
        try:
            data = getaddress(self.lat , self.lon)
            self.address = data['formatted_address']
        except:
            pass
        super(Provider, self).save(*args, **kwargs)

Как я могу получить местоположение пользователя и найти несколько поставщиков услуг, которые находятся рядом с моим пользователем? И это должно быть из поля lat и lon, потому что возможно, что поставщик услуг находится в другом городе! также важна скорость работы!

Алиреза! У меня есть рабочая идея, как решить вашу проблему. Итак, я вижу решение с помощью библиотеки geoip2.

Здесь вы можете найти информацию об использовании этой библиотеки с django. Это из документации по django.

А вот одна интересная статья о связанной с этим проблеме. Она может быть полезной. А еще я нашел пару похожих проблем, которые уже обсуждались. Это может быть полезно для вас.

Итак, если коротко, то решение следующее:

# Let's assume that you have already created a django project and an application

from django.contrib.gis.geoip2 import GeoIP2

g = GeoIP2()


# You`ll need to user IP. For example it possible to get this via some way like that:

def get_client_ip(request):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip

# After that put response of that function to fallowing:

g.city("72.14.207.99")

{'accuracy_radius': 1000,
 'city': 'Mountain View',
 'continent_code': 'NA',
 'continent_name': 'North America',
 'country_code': 'US',
 'country_name': 'United States',
 'is_in_european_union': False,
 'latitude': 37.419200897216797,
 'longitude': -122.05740356445312,
 'metro_code': 807,
 'postal_code': '94043',
 'region_code': 'CA',
 'region_name': 'California',
 'time_zone': 'America/Los_Angeles',
 'dma_code': 807,
 'region': 'CA'}

Вы видите, что в ответе есть широта и долгота. Далее вам нужно будет только сравнить полученные ширину и долготу с шириной и долготой провайдера. Например, можно подключить модуль django-filter и сделать запрос с фильтрацией по полям width и longitude.

Надеюсь, что мой ответ немного помог вам.

Добавление к ответу:

import geopy.distance

def find_nearest(user_lat, user_lon):

    # We get all the providers' objects and calculate the distance to them
    return Provider.objects.filter(
        # you can add field city or country and use here to immediately reduce the number of objects being checked
    ).annotate(
        distance=Coalesce(# you can use Coalesce or not, the main thing here is the essence of the idea
            geopy.distance.geodesic((lat, lon), (user_lat, user_lon)).km,
            Value(0), output_field=FloatField()
        )
    ).order_by('-distance')[0]

Это зависит от ряда факторов. Чтобы сделать это быстро, вам нужно, чтобы ваша база данных могла индексировать координаты широты/долготы. Это означает, что ваша база данных должна понимать вашу систему координат. Поэтому забудьте на секунду о Django. Вам нужно знать, какой бэкенд базы данных (СУБД) вы используете. Поскольку вы не назвали ни одной, я просто буду ссылаться на PostgreSQL в качестве ссылки до конца ответа. Некоторые другие базы данных также имеют систему географической реализации (GIS), поэтому остальная часть ответа в целом справедлива для этих СУБД. Однако, как правило, они не устанавливаются по умолчанию.

В PostgreSQL можно установить расширение под названием postgis. Postgis понимает системы координат. Он может точно рассчитать расстояние между любыми двумя точками на Земле (т. е. с учетом того, что Земля - сфероид, а не идеальная сфера). Кроме того, он может обеспечить индексы для точек, чтобы вы могли быстро найти точки, расположенные близко друг к другу. Пример (с использованием psql - клиента командной строки для PostgreSQL):

user=# CREATE EXTENSION postgis;
CREATE EXTENSION
user=# SELECT st_distance(
    --                  E/W     N/S
    'SRID=4326;POINT(-118.4079 33.9434)'::GEOGRAPHY, -- Los Angeles (LAX)
    'SRID=4326;POINT(   2.5559 49.0083)'::GEOGRAPHY  -- Paris (CDG)
    ) as "distance (meters)";
 distance (meters)
-------------------
  9124665.27317673

Здесь SRID=4326 сообщает postgres, что координаты взяты из определенной системы координат (WGS 84 - стандарт для GPS).

А с помощью индекса (чтобы найти ближайший аэропорт в таблице к лондонскому Хитроу):

user=# CREATE TABLE airports (name TEXT, coords GEOGRAPHY);
CREATE TABLE
user=# INSERT INTO airports VALUES
    ('Los Angles (LAX)', 'POINT(-118.4079 33.9434)'),
    ('Iceland (KEF)',    'POINT( -22.6056 63.9850)'),
    ('Paris (CDG)',      'POINT(   2.5559 49.0083)');
    -- bulk insert these rows (except Paris) multiple times and you'll still 
    -- have a fast query thanks to index that is created. Around a couple ms
    -- when there are 100k+ rows in the table.
INSERT 0 3
user=# CREATE INDEX ON airports USING GIST (coords);
CREATE INDEX

user=# WITH input(coords) AS (
    SELECT 'POINT(-0.454295 51.470020)'::GEOGRAPHY -- London (LHR)
)
SELECT
    name,
    st_astext(airports.coords) AS coords,
    airports.coords <-> input.coords AS "distance (meters)"
FROM airports CROSS JOIN input
ORDER BY "distance (meters)"
LIMIT 1;
    name     |        coords         | distance (meters)
-------------+-----------------------+-------------------
 Paris (CDG) | POINT(2.5559 49.0083) | 347441.4699732649

Использование проецируемых систем координат

AKA Более быстрые, менее точные приближения.

Хотя обращение с Землей как со сферой дает точные результаты для любых двух точек на планете, оно также может быть медленным (поскольку требует широкого использования тригонометрических функций). Если вместо этого спроецировать лат/лон в декартово пространство (плоскую плоскость) и сохранить эти координаты в базе данных, то результаты можно вычислить гораздо быстрее, но за счет снижения точности на больших расстояниях. Так, в пределах одного города использование проекции даст результаты, практически неотличимые от реальных значений. Для расстояний между городами в штате результаты будут достаточно точными для большинства целей. Однако сравнивать следует только те точки, которые находятся в пределах вашей проекции.

Приведенный ниже пример показывает, что при использовании проекции вы теряете всего 11 метров на расстоянии 33 км - менее 1/10 процента.

user=# CREATE TABLE nyc_airports (
    name TEXT,
    local_coords GEOMETRY,
    global_coords GEOGRAPHY
);
CREATE TABLE
users=# INSERT INTO nyc_airports (name, global_coords) VALUES
    ('JFK', 'POINT(-73.780968 40.641766)'), -- lat/lon pairs
    ('EWR', 'POINT(-74.174538 40.689491)');
INSERT 0 2
users=# UPDATE nyc_airports SET
    local_coords = st_transform(global_coords::GEOMETRY, 26918);
-- convert to NAD83, a projected strip about as wide New York State that stretches
-- from the southern tip of the USA up to just north of the last bit of Canada.
UPDATE 2

users=# SELECT
    DISTINCT ON (b.name)
    a.name AS "from", b.name AS "to",
    st_distance(a.global_coords, b.global_coords) AS "global distance",
    st_distance(a.local_coords, b.local_coords) AS "local distance"
FROM nyc_airports AS a CROSS JOIN nyc_airports AS b;
 from | to  | global distance |   local distance
------+-----+-----------------+--------------------
 JFK  | EWR |  33699.17253248 | 33688.828503315126
 JFK  | JFK |               0 |                  0

Возвращаясь к Django

Django поставляется с предустановленной библиотекой geodjango, которая помогает ему работать с пространственными базами данных. Вы можете посмотреть, с какими базами данных она совместима здесь. Вам также нужно убедиться, что установлены все необходимые библиотеки. То есть для его работы требуются некоторые библиотеки на языке C. После того как у вас есть совместимая пространственная база данных и необходимые библиотеки geodjango, использовать его становится относительно просто. Например,

from django.contrib.gis.db import models

class Provider(models.Model):
    name = models.CharField(max_length=100)
    coord = models.PointField()  # defaults to lat/lon coordinate system

Создание провайдера

from django.contrib.gis.geos import Point

provider = Provider(name='LAX', coord=Point(-118.4079, 33.9434))

Поиск ближайшего поставщика услуг

Вычисление расстояния выполняется на сервере базы данных, где сервер может использовать индекс на coords (который был показан ранее), чтобы минимизировать количество строк, которые ему нужно исследовать.

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

user_location = Point(-118.4079, 33.9434)
result = Provider.objects.annotate(
    distance=Distance('coord', user_location),
).order_by('distance').first()
Вернуться на верх