Home > database >  DRF: how to create custom FilterSet to filter nearest users by distance
DRF: how to create custom FilterSet to filter nearest users by distance

Time:03-30

I'm trying to create custom FilterSet for filtering nearby users by distance using django-filter For example if I send
GET /api/list/?distance=300, I want to get all nearby users who are lower or equals to 300m far away

My model has 2 fields:

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

My ClientManager has function for getting coords from model:

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

My 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

My 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']

Here I'm using my function for calculating distance between two clients:

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

I do not know how to filter the queryset and do it optimally from the database accesses side.

CodePudding user response:

I would recommend using the existing tools that solved this problem a long time ago. Doing accurate (and efficient) distance calculations on an irregularly shaped sphere is more complicated than this.

https://docs.djangoproject.com/en/4.0/ref/contrib/gis/install/postgis/

GIS field on model:

from django.contrib.gis.db.models import PointField

class Client(models.Model):
    location = PointField()

This gives you the proper tools to do distance calculations directly on the queryset and the computation is done on the database side afaik.(https://docs.djangoproject.com/en/4.0/ref/contrib/gis/tutorial/#spatial-queries)

It is a bit more overhead to setup GIS properly but it's worth the effort.

Sidenote: It is possible to do it by hand via queryset annotate() and Q and F expressions but as I said it's tricky to get right. Filtering django-filter on the client-side, as you attempted there, pretty much defeats the purpose of using django-filter in the first place. Hope that helps getting a better understanding.

CodePudding user response:

I have come up with a possible solution, but I am not sure about the optimality of its work and how to pass latitude and longitude fields in my function for calculating distance

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')
  • Related