Как эффективно отфильтровать местоположения в базе данных, не перебирая их вручную?

В моем проекте DRF есть модель, структурированная следующим образом

class ServiceLocation(models.Model):
    '''
    Represents a location where an internet service is offered
    '''
    SERVICE_TYPES = [
        ("wifi", 'wifi'),
        ("fibre", "fibre"),
        ("p2p/ptmp", "p2p/ptmp")
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4,
                          editable=False, null=False, blank=False)
    description = models.TextField()


    # Location
    address = models.CharField(max_length=150, null=False, blank=False)
    latitude = models.DecimalField(max_digits=18, decimal_places=15)
    longitude = models.DecimalField(max_digits=18, decimal_places=15)

    # Service
    service = models.CharField(
        max_length=10, choices=SERVICE_TYPES, null=False, blank=False)
    speed = models.IntegerField()
    
    def __str__(self):
        return f"{self.service} by {self.operator}"

и я пытаюсь отфильтровать экземпляры этой модели по их относительной близости к заданной координате.

Мое представление структурировано следующим образом


class CloseServiceLocations(View):
    def get(self, request):
        lat = request.GET.get('lat', 6.748134)
        lng = request.GET.get('lng', 3.633301)
        distance = request.GET.get('distance', 10)  # Default distance to 10 if not provided

        # if lat is None or lng is None:
        #     # return JsonResponse({'error': 'Latitude and Longitude are required parameters.'}, status=400)


        try:
            lat = float(lat)
            lng = float(lng)
            distance = float(distance)
        except ValueError:
            return JsonResponse({'error': 'Invalid latitude, longitude, or distance provided.'}, status=400)

        # Create a Point object representing the provided latitude and longitude
        user_location = Point(lng, lat, srid=4326)

        

        # Calculate the distance in meters (Django's Distance function uses meters)
        distance_in_meters = distance * 1000  
        close_service_locations = ServiceLocation.objects.annotate(
            # Convert longitude and latitude fields to floats
            longitude_float=Cast('longitude', FloatField()),
            latitude_float=Cast('latitude', FloatField())
        ).annotate(
            # Create Point object using converted longitude and latitude
            location=Point(F('longitude_float'), F('latitude_float'), srid=4326)
        ).annotate(
            # Calculate distance
            distance=Distance('location', user_location)
        ).filter(distance__lte=distance_in_meters)
    # Serialize the queryset to JSON
        serialized_data = [{'id': location.id,
                            'description': location.description,
                            'operator': location.operator.name,
                            'address': location.address,
                            'latitude': location.latitude,
                            'longitude': location.longitude,
                            'service': location.service,
                            'speed': location.speed} for location in close_service_locations]

        return JsonResponse(serialized_data, safe=False)

    def post(self, request):
        return JsonResponse({'error': 'Method not allowed'}, status=405)

где я пытаюсь аннотировать новый атрибут "location", чтобы я мог воспользоваться методом Distance из from django.contrib.gis.db.models.functions для вычисления расстояния вместо того, чтобы зацикливаться и вычислять расстояние по Гаверсину вручную, что было моим первоначальным подходом.

Когда я запускаю это, я получаю сообщение об ошибке сервера, которое, я почти уверен, не из моих представлений.

В попытке исправить это, я разбил свои представления на секции и добавил операторы печати, чтобы увидеть, какая часть вызывает поломку

        print("Annotating QS with lon/lat float... ")
        close_service_locations = ServiceLocation.objects.annotate(
            # Convert longitude and latitude fields to floats
            longitude_float=Cast('longitude', FloatField()),
            latitude_float=Cast('latitude', FloatField())
        )
        print("LON/LAT float annotation complete")
       
        print("Annotating QS with location point... ")
        print("Size: ", len(close_service_locations))
       

        close_service_locations = close_service_locations.annotate(
            # Create Point object using converted longitude and latitude
            location=Point('longitude_float', 'latitude_float', srid=4326)
        )
        print("Location point annotation complete")

        print("Annotating QS with relative distance... ")
        close_service_locations = close_service_locations.annotate(
            # Calculate distance
            distance=Distance('location', user_location)
        )
        print("Distance annotation complete")

Я заметил, что блок print("Annotating QS with lon/lat float... ") выполняется успешно и быстро, но он обрывается в блоке print("Annotating QS with location point... "), где я пытаюсь аннотировать QS с помощью атрибута location

В какой-то момент я получил ошибку "Invalid parameters given for Point initialization.", которая заставила меня добавить блок print("Annotating QS with lon/lat float... "), чтобы заставить все объекты Decimalfiled превратиться в floats.

Я также попробовал вручную просмотреть close_service_locations, чтобы узнать, есть ли у меня местоположение службы с недопустимой широтой или долготой, с помощью этого

for i in range(len(close_service_locations)):
            print(i)
            location=Point(close_service_locations[i].longitude_float, close_service_locations[i].latitude_float, srid=4326)

На удивление, все прошло успешно.

Но я все еще не знаю, как заставить мой вид успешно работать после этой точки.

Это ошибка, которую я продолжаю получать

Annotating QS with lon/lat float...
LON/LAT float annotation complete
Annotating QS with location point...

[13/Apr/2024 04:49:30] "GET /directory/close-service-locations/ HTTP/1.1" 500 145 

В браузере нет страницы ошибки Django с подробным описанием причины, только большая ошибка сервера

Я также попробовал вручную пройтись по сервисам-локациям, добавляя атрибут location и выводя индекс, возможно, мне удастся понять, что именно нарушает мой код или есть ли сервис-локация, чья пара долготы и высоты не может быть преобразована в объект Point, но я все равно получил ту же ошибку

Erro message

В следующем коде вы предоставляете строки (longitude_floatlatitude_float и latitude_float) классу Point.

close_service_locations = close_service_locations.annotate(
    # Create Point object using converted longitude and latitude
     location=Point('longitude_float', 'latitude_float', srid=4326)
   )

Вы можете подумать, что вы создаете это значение выше, но это в строке longitude_float=Cast('longitude', FloatField()),, а в следующей аннотации оно рассматривается как просто строка.

Вы можете попробовать следующее, чтобы заставить его работать.

from django.contrib.gis.db.models import F
close_service_locations = ServiceLocation.objects.annotate(
            # Convert longitude and latitude fields to floats
            longitude_float=Cast('longitude', FloatField()),
            latitude_float=Cast('latitude', FloatField()),
            location=Point(F('longitude_float'), F('latitude_float'), srid=4326)
            # in the same code.
        )

EDIT: Другой способ сделать это - использовать Python вместо Django.

close_service_locations = ServiceLocation.objects.annotate(
    # Convert longitude and latitude fields to floats
    longitude_float=Cast('longitude', FloatField()),
    latitude_float=Cast('latitude', FloatField())
)
service_location_mapping = {}
for service in close_service_locations:
    location = Point(service.longitude_float, service.latitude_float, srid=4326)
    distance = Distance(location, user_location)
    service_location_mapping[service] = {
        'distance': distance,
        'service': service
    }

Я придумал другой подход, основанный на том, насколько точны десятичные цифры в координатах на google maps, и он сработал.

    def get(self, request):
        lat = request.GET.get('lat', 8.464134)
        lng = request.GET.get('lng', 4.454301)
        distance = request.GET.get('distance', 10)  # Default distance to 10 km if not provided

        try:
            lat = float(lat)
            lng = float(lng)
            distance = float(distance)
        except ValueError:
            return JsonResponse({'error': 'Invalid latitude, longitude, or distance provided.'}, status=400)

        # Define the approximate distance difference in degrees (assuming 1 degree is approximately 111 km)
        diff = distance / 111    
        
        print("Getting SLs .....")
        close_locations = ServiceLocation.objects.filter(latitude__gte=(lat-diff), latitude__lte=(lat+diff), longitude__gte=(lng-diff), longitude__lte=(lng+diff), )

        print("Close locations length ", len(close_locations))
        serialized = ServiceLocationSerializer(close_locations, many=True)

        print(serialized.data)
        return Response(serialized.data, status=status.HTTP_200_OK) 
Вернуться на верх