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')
Вернуться на верх