DRF:如何创建自定义 FilterSet 以按距离过滤最近的用户
DRF: how to create custom FilterSet to filter nearest users by distance
我正在尝试创建自定义 FilterSet
以使用 django-filter
按距离过滤附近的用户
例如,如果我发送
GET /api/list/?distance=300
,我想获取附近所有低于或等于300m远
的用户
我的模型有 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/
模型上的 GIS 字段:
from django.contrib.gis.db.models import PointField
class Client(models.Model):
location = PointField()
这为您提供了直接在查询集上进行距离计算的适当工具,并且计算是在数据库端 afaik 完成的。(https://docs.djangoproject.com/en/4.0/ref/contrib/gis/tutorial/#spatial-queries)
正确设置 GIS 的开销有点大,但值得付出努力。
旁注:可以通过查询集 annotate()
和 Q
以及 F
表达式手动完成,但正如我所说,要做到这一点很棘手。在 client-side 上过滤 django-filter
,正如您在那里尝试的那样,这几乎违背了最初使用 django-filter
的目的。希望这有助于更好地理解。
我解决了这个问题
class ClientFilter(FilterSet):
"""
Custom ClientFilter
"""
distance = filters.NumberFilter(method='get_nearest_clients')
def get_nearest_clients(self, queryset: QuerySet, name: str, dist_value: int):
sender_id = self.request.user.id
sender_la, sender_lo = map(radians, Client.objects.get_geo_coordinates(pk=sender_id))
earth_radius = 6_400_000
queryset = (
queryset.exclude(pk=sender_id)
.alias(
rad_lat=Radians('latitude'),
rad_long=Radians('longitude'),
distance=ExpressionWrapper(
ACos(
Cos('rad_lat') * Cos(sender_la) * Cos(F('rad_long') - sender_lo)
+ Sin('rad_lat') * Sin(sender_la)
) * earth_radius,
output_field=FloatField()
)
)
.exclude(distance__gte=dist_value)
.order_by('distance')
)
return queryset
我正在尝试创建自定义 FilterSet
以使用 django-filter
按距离过滤附近的用户
例如,如果我发送
GET /api/list/?distance=300
,我想获取附近所有低于或等于300m远
我的模型有 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/
模型上的 GIS 字段:
from django.contrib.gis.db.models import PointField
class Client(models.Model):
location = PointField()
这为您提供了直接在查询集上进行距离计算的适当工具,并且计算是在数据库端 afaik 完成的。(https://docs.djangoproject.com/en/4.0/ref/contrib/gis/tutorial/#spatial-queries)
正确设置 GIS 的开销有点大,但值得付出努力。
旁注:可以通过查询集 annotate()
和 Q
以及 F
表达式手动完成,但正如我所说,要做到这一点很棘手。在 client-side 上过滤 django-filter
,正如您在那里尝试的那样,这几乎违背了最初使用 django-filter
的目的。希望这有助于更好地理解。
我解决了这个问题
class ClientFilter(FilterSet):
"""
Custom ClientFilter
"""
distance = filters.NumberFilter(method='get_nearest_clients')
def get_nearest_clients(self, queryset: QuerySet, name: str, dist_value: int):
sender_id = self.request.user.id
sender_la, sender_lo = map(radians, Client.objects.get_geo_coordinates(pk=sender_id))
earth_radius = 6_400_000
queryset = (
queryset.exclude(pk=sender_id)
.alias(
rad_lat=Radians('latitude'),
rad_long=Radians('longitude'),
distance=ExpressionWrapper(
ACos(
Cos('rad_lat') * Cos(sender_la) * Cos(F('rad_long') - sender_lo)
+ Sin('rad_lat') * Sin(sender_la)
) * earth_radius,
output_field=FloatField()
)
)
.exclude(distance__gte=dist_value)
.order_by('distance')
)
return queryset