DRF: как создать пользовательский FilterSet для фильтрации ближайших пользователей по расстоянию
Я пытаюсь создать пользовательский FilterSet для фильтрации близлежащих пользователей по расстоянию.
Например, если я посылаю
GET /api/list/?distance=300, я хочу получить всех пользователей, которые находятся на расстоянии меньше или равном 300м от меня
В моей модели есть 2 поля:
latitude = models.DecimalField( # [-90.000000, 90.000000]
max_digits=8,
decimal_places=6,
null=True
)
longitude = models.DecimalField( # [-180.000000, 180.000000]
max_digits=9,
decimal_places=6,
null=True
)
objects = ClientManager()
Моя ClientManager имеет функцию для получения коорд из модели:
def get_geo_coordinates(self, pk):
"""
:param pk: - client id
:return: client's coords
"""
instance = self.get(pk=pk)
data = (instance.latitude, instance.longitude)
return data`
Мой GetListAPIView
class GetClientListAPIView(ListAPIView):
"""
Returns list with filtering capability
Available filter fields:
gender, first_name, last_name, distance
"""
serializer_class = ClientSerializer
queryset = Client.objects.all()
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filter_class = ClientFilter`
Мой ClientFilter
class ClientFilter(FilterSet):
distance = filters.NumberFilter(method='get_nearest_clients')
def get_nearest_clients(self, queryset, name, value):
sender_coords = Client.objects.get_geo_coordinates(pk=self.request.user.id)
test_coords = Client.objects.get_geo_coordinates(pk=31)
dist = get_great_circle_distance(sender_coords, test_coords)
class Meta:
model = Client
fields = ['gender', 'first_name', 'last_name']
Здесь я использую свою функцию для вычисления расстояния между двумя клиентами:
def get_great_circle_distance(first_coords, second_coords):
"""
:param first_coords: (first_client_latitude, first_client_longitude) in degrees
:param second_coords: (second_client_latitude, second_client_longitude) in degrees
:return: distance
"""
earth_radius = 6_400_000 # in metres
la_1, lo_1 = map(radians, first_coords)
la_2, lo_2 = map(radians, second_coords)
coefficient = acos(
cos(la_1) * cos(la_2) * cos(lo_1 - lo_2) +
sin(la_1) * sin(la_2)
)
distance = earth_radius * coefficient
return distance
Я не знаю, как фильтровать queryset и делать это оптимально со стороны доступа к базе данных.
Я бы рекомендовал использовать существующие инструменты, которые давно решили эту проблему. Выполнение точных (и эффективных) вычислений расстояния на сфере неправильной формы является более сложной задачей.
https://docs.djangoproject.com/en/4.0/ref/contrib/gis/install/postgis/
Поле ГИС в модели:
from django.contrib.gis.db.models import PointField
class Client(models.Model):
location = PointField()
Это дает вам соответствующие инструменты для вычисления расстояния непосредственно в наборе запросов, а вычисления производятся на стороне базы данных, насколько я знаю.(https://docs.djangoproject.com/en/4.0/ref/contrib/gis/tutorial/#spatial-queries)
Настраивать GIS должным образом немного накладнее, но это стоит усилий.
Список: Это можно сделать вручную с помощью queryset annotate() и выражений Q и F, но, как я уже сказал, это сложно сделать правильно. Фильтрация django-filter на стороне клиента, как вы пытались там, практически уничтожает цель использования django-filter в первую очередь. Надеюсь, это поможет лучше понять.
Я придумал возможное решение, но не уверен в оптимальности его работы и как передать поля latitude и longitude в моей функции для вычисления расстояния
def get_nearest_clients(self, queryset: QuerySet, name, value):
sender_id = self.request.user.id
sender_coords = Client.objects.get_geo_coordinates(pk=sender_id)
data = queryset.exclude(pk=sender_id).alias(
distance=get_great_circle_distance(sender_coords, (F('latitude'), F('longitude')))
).exculde(distance__lt=value).order_by('distance')