How can I create a partial search filter in Django REST framework?
Question:
I’m working with the Django REST framework library and I am trying to make a filter that can filter by first_name, last_name, or by both of them.
This is my ContactViewSet.py:
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
filter_backends = (DjangoFilterBackend, )
filter_fields = ('first_name', 'last_name')
lookup_field = 'idContact'
My DRF’s settings.py
:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
}
My current request URL looks like:
http://localhost:8000/api/v1/contacts/?first_name=Clair&last_name=Test
But I’m looking for something like this:
http://localhost:8000/api/v1/contacts/?first_name=Cl**&last_name=Tes**
Answers:
I think the DjangoFilterBackend is mainly equality-based filtering. But you can customize the filtering method.
Also in DRF, for non exact filtering, there is the SearchFilter which makes case-insensitive partial matches searches by default.
I solved my problem by modifying my class ContactFilter like this:
import django_filters
from .models import Contact
class ContactFilter(django_filters.FilterSet):
class Meta:
model = Contact
fields = {
'first_name': ['startswith'],
'last_name': ['startswith'],
}
together = ['first_name', 'last_name']
And in my view I just had to do this:
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
filter_class = ContactFilter
My request URL looks like this:
http://localhost:8000/api/v1/contact/?first_name__contains=Cl&last_name__contains=Tes
But I still wonder if I can have something like this in Django:
http://localhost:8000/api/v1/contacts/?first_name=Cl**&last_name=Tes**
If your requests aren’t too complicated you can also use:
class YourModelViewSet(viewsets.ModelViewSet):
queryset = YourModel.objects.all()
serializer_class = YourModelSerializer
filter_fields = {'some_field': ['startswith']}
Which will enable ‘?some_field__starswith=text’ sintax support in request query params.
I suppose ‘startswith’ can be replaced with any django standart queryset filter param.
What I do, is write custom FilterBackend. Something like this:
# views.py
from rest_framework import filters
class ObjektFilterBackend(filters.BaseFilterBackend):
allowed_fields = ['objekt', 'naziv', 'kategorija', 'zadnja_sprememba']
def filter_queryset(self, request, queryset, view):
flt = {}
for param in request.query_params:
for fld in self.allowed_fields:
if param.startswith(fld):
flt[param] = request.query_params[param]
return queryset.filter(**flt)
class ObjektiViewSet(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
authentication_classes = (
authentication.TokenAuthentication,
authentication.SessionAuthentication)
permission_classes = (IsAuthenticated,)
queryset = models.Objekt.objects.all()
serializer_class = serializers.ObjektSerializer
filter_backends = (ObjektFilterBackend, ObjektOrderBackend,)
....
Besides basic filtering (fieldname=value pairs) I can use any Django queryset Field Lookups (__gt, __gte, __startswith,…) in my URLs like this:
http://localhost:8000/api/v2/objekti/?naziv__startswith=Apartma&zadnja_sprememba__gte=2018-01-01
And ObjektFilterBackend class could be easily adapted to support searching by pattern.
Just a little warning – this approach is potentially dangerous, because it allows end user to filter also by foreign key field. Something like this also works:
http://localhost:8000/api/v2/objekti/?kategorija__naziv__icontains=sobe
So restrict allowed_fields carefully and not include foreign keys that could lead to related User model.
For fuzzy search lookups I recommend using this approach:
filters.py
from django_filters import rest_framework as filters
from django.db.models import Q
from . import models
def filter_name(queryset, name, value):
"""
Split the filter value into separate search terms and construct a set of queries from this. The set of queries
includes an icontains lookup for the lookup fields for each of the search terms. The set of queries is then joined
with the OR operator.
"""
lookups = [name + '__icontains', ]
or_queries = []
search_terms = value.split()
for search_term in search_terms:
or_queries += [Q(**{lookup: search_term}) for lookup in lookups]
return queryset.filter(reduce(operator.or_, or_queries))
class ContactFilter(filters.FilterSet):
first_name = filters.CharFilter(method=filter_name, name='first_name')
last_name = filters.CharFilter(method=filter_name, name='last_name')
class Meta:
model = models.Contact
fields = [
'first_name',
'last_name',
]
api.py
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
filter_class = ContactFilter
...
You should add custom Filter for your viewset.
from django_filters.rest_framework import DjangoFilterBackend
import django_filters
from recipes.models import Ingredient
class MyModelFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
field_name='name', lookup_expr='icontains'
)
class Meta:
model = MyModel
fields = []
class MyViewSet(viewsets.ReadOnlyModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
permission_classes = (AllowAny,)
pagination_class = None
filter_backends = (DjangoFilterBackend,) # add this
filterset_class = MyModelFilter # add this
I’m working with the Django REST framework library and I am trying to make a filter that can filter by first_name, last_name, or by both of them.
This is my ContactViewSet.py:
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
filter_backends = (DjangoFilterBackend, )
filter_fields = ('first_name', 'last_name')
lookup_field = 'idContact'
My DRF’s settings.py
:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
}
My current request URL looks like:
http://localhost:8000/api/v1/contacts/?first_name=Clair&last_name=Test
But I’m looking for something like this:
http://localhost:8000/api/v1/contacts/?first_name=Cl**&last_name=Tes**
I think the DjangoFilterBackend is mainly equality-based filtering. But you can customize the filtering method.
Also in DRF, for non exact filtering, there is the SearchFilter which makes case-insensitive partial matches searches by default.
I solved my problem by modifying my class ContactFilter like this:
import django_filters
from .models import Contact
class ContactFilter(django_filters.FilterSet):
class Meta:
model = Contact
fields = {
'first_name': ['startswith'],
'last_name': ['startswith'],
}
together = ['first_name', 'last_name']
And in my view I just had to do this:
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
filter_class = ContactFilter
My request URL looks like this:
http://localhost:8000/api/v1/contact/?first_name__contains=Cl&last_name__contains=Tes
But I still wonder if I can have something like this in Django:
http://localhost:8000/api/v1/contacts/?first_name=Cl**&last_name=Tes**
If your requests aren’t too complicated you can also use:
class YourModelViewSet(viewsets.ModelViewSet):
queryset = YourModel.objects.all()
serializer_class = YourModelSerializer
filter_fields = {'some_field': ['startswith']}
Which will enable ‘?some_field__starswith=text’ sintax support in request query params.
I suppose ‘startswith’ can be replaced with any django standart queryset filter param.
What I do, is write custom FilterBackend. Something like this:
# views.py
from rest_framework import filters
class ObjektFilterBackend(filters.BaseFilterBackend):
allowed_fields = ['objekt', 'naziv', 'kategorija', 'zadnja_sprememba']
def filter_queryset(self, request, queryset, view):
flt = {}
for param in request.query_params:
for fld in self.allowed_fields:
if param.startswith(fld):
flt[param] = request.query_params[param]
return queryset.filter(**flt)
class ObjektiViewSet(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
authentication_classes = (
authentication.TokenAuthentication,
authentication.SessionAuthentication)
permission_classes = (IsAuthenticated,)
queryset = models.Objekt.objects.all()
serializer_class = serializers.ObjektSerializer
filter_backends = (ObjektFilterBackend, ObjektOrderBackend,)
....
Besides basic filtering (fieldname=value pairs) I can use any Django queryset Field Lookups (__gt, __gte, __startswith,…) in my URLs like this:
http://localhost:8000/api/v2/objekti/?naziv__startswith=Apartma&zadnja_sprememba__gte=2018-01-01
And ObjektFilterBackend class could be easily adapted to support searching by pattern.
Just a little warning – this approach is potentially dangerous, because it allows end user to filter also by foreign key field. Something like this also works:
http://localhost:8000/api/v2/objekti/?kategorija__naziv__icontains=sobe
So restrict allowed_fields carefully and not include foreign keys that could lead to related User model.
For fuzzy search lookups I recommend using this approach:
filters.py
from django_filters import rest_framework as filters
from django.db.models import Q
from . import models
def filter_name(queryset, name, value):
"""
Split the filter value into separate search terms and construct a set of queries from this. The set of queries
includes an icontains lookup for the lookup fields for each of the search terms. The set of queries is then joined
with the OR operator.
"""
lookups = [name + '__icontains', ]
or_queries = []
search_terms = value.split()
for search_term in search_terms:
or_queries += [Q(**{lookup: search_term}) for lookup in lookups]
return queryset.filter(reduce(operator.or_, or_queries))
class ContactFilter(filters.FilterSet):
first_name = filters.CharFilter(method=filter_name, name='first_name')
last_name = filters.CharFilter(method=filter_name, name='last_name')
class Meta:
model = models.Contact
fields = [
'first_name',
'last_name',
]
api.py
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
filter_class = ContactFilter
...
You should add custom Filter for your viewset.
from django_filters.rest_framework import DjangoFilterBackend
import django_filters
from recipes.models import Ingredient
class MyModelFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
field_name='name', lookup_expr='icontains'
)
class Meta:
model = MyModel
fields = []
class MyViewSet(viewsets.ReadOnlyModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
permission_classes = (AllowAny,)
pagination_class = None
filter_backends = (DjangoFilterBackend,) # add this
filterset_class = MyModelFilter # add this