Introduction
When building APIs with Django Rest Framework, one of the essential features you might want to include is a search functionality. This allows clients to query the API and filter results based on specific criteria, making it much more user-friendly and flexible.
Django provides us with a powerful tool for our APIs called django-filters, this gives us access to;
- The DjangoFilterBackend class which supports highly customizable field filtering for REST framework.
- The SearchFilter class supports simple single query parameter based searching which are case-insensitive partial matches, and is based on the Django admin's search functionality.
- The OrderingFilter class supports simple query parameter controlled ordering of results.
We are going to focus on the SearchFilter for this tutorial as we are trying to implement search functionality.
1. Setting Up a Sample API
To demonstrate search functionality, we'll first set up a simple model, serializer, and view.
Step 1: Create a Model
Let's say we have a model for Category, JobSkills and PostAJob. Category is a ForeignKey to PostAJob and JobSkills is a ManyToManyField relationship to PostAJob.
class Category(models.Model):
name = models.CharField(max_length=50, unique=True, null=True, blank=True)
def __str__(self):
return self.name
class JobSkills(models.Model):
title = models.CharField(max_length=20, unique=True)
category = models.ManyToManyField(Category)
def __str__(self):
return self.title
class PostAJob(models.Model):
job_title = models.CharField(max_length=200)
job_category = models.ForeignKey(Category, on_delete=models.CASCADE)
job_skills = models.ManyToManyField(JobSkills, blank=True)
job_salary_range = models.IntegerField(blank=True)
job_description = models.TextField()
def __str__(self):
return self.job_title
Step 2: Create a Serializer
Create a serializer for the models.
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name']
class JobSkillsSerializer(serializers.ModelSerializer):
category = CategorySerializer(many=True, read_only=True)
class Meta:
model = JobSkills
fields = ['id', 'title', 'category']
class JobSerializer(serializers.ModelSerializer):
job_category = CategorySerializer(read_only=True)
job_skills = JobSkillsSerializer(many=True, read_only=True)
class Meta:
model = PostAJob
fields = (
'id',
'job_title',
'job_category',
'job_skills',
'job_salary_range',
'job_description',
)
def create(self, validated_data):
request = self.context['request']
job_category_pk = request.data.get('job_category')
validated_data['job_category_id'] = job_category_pk
job_skills_data = request.data.get('job_skills')
validated_data['job_skills'] = job_skills_data
instance = super().create(validated_data)
return instance
Step 3: Setup Django-Filter
To use SearchFilter, first install django-filter.
pip install django-filter
Then add 'django_filters' to Django's INSTALLED_APPS in your settings.py:
INSTALLED_APPS = [
...
'django_filters',
...
]
Step 4: Create a View and URL
You should now add the search filter to an individual View or ViewSet.
The SearchFilter class will only be applied if the view has a search_fields attribute set. The search_fields attribute should be a list of names of text type fields on the model, such as CharField or TextField.
Here is what’s happening, we are adding a SearchFilter to the filter_backends and a search_fields which tells the filter class what fields it should query i.e if the user searches for a text and it is in the job_title or category or skills db section, if that string is available, the system should return what the user searched for.
If you have a ForeignKey or a ManyToMany relational fields like the below category and skills field example, you need to specify what the related lookup is by using the lookup API double-underscore notation __
, this is what you can see in the model of the Category and JobSKills seen above. That is why in searchFields we have an underscore for job_category as the lookup we are using is the name - 'job_category_name’ and for job_skills, we are looking for title - 'job_skills_title',.
from rest_framework import filters
from rest_framework.pagination import PageNumberPagination
class PostAJobListView(GenericAPIView):
serializer_class = JobSerializer
queryset = (
PostAJob.objects.select_related('job_category')
.prefetch_related('job_skills')
.all()
)
filter_backends = [filters.SearchFilter]
search_fields = [
'job_title',
'job_category__name',
'job_skills__title',
]
def get(self, request, *args, **kwargs):
# Get the filtered queryset
queryset = self.filter_queryset(self.get_queryset())
# Paginate the queryset
page = self.paginate_queryset(queryset)
if page is not None:
# If pagination is applied, get the paginated response
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
# If no pagination, return the full response
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
urlpatterns = [
path('post-a-job/', PostAJobListView.as_view(), name='post_a_job_list'),
]
Step 5: Testing the Search
Now, you can use the ?search= query parameter in the URL to search for books by title or author. For example:
http://127.0.0.1:8000/post-a-job/?search=Machine
This will return all jobs with "Machine" in the title or category or skills.
2. Advanced Search Customization
You can further customize the search functionality in various ways.
The search behavior may be specified by prefixing field names in search_fields with one of the following characters (which is equivalent to adding __ to the field):
- Implementing Custom Search Functionality In some cases, you may need more control over the search behavior. You can override the get_queryset method in the view.
def get_queryset(self):
"""
Optionally restricts the returned jobs to query parameter in the URL.
"""
filter_query = Q()
job_title = self.request.query_params.get(title)
if job_title is not None:
filter_query &= Q(job_title__contains=job_title)
user_id = self.request.query_params.get('user_id')
if user_id is not None:
filter_query &= Q(created_by=user_id)
salary = self.request.query_params.get('salary')
if salary is not None:
filter_query &= Q(job_salary_range__gte=salary)
queryset = self.queryset.filter(filter_query)
return queryset
In this example, we manually filter the queryset based on the search query parameter, allowing for more complex search logic.
Reference
https://www.django-rest-framework.org/api-guide/filtering/#searchfilter
Conclusion
Adding search functionality to your Django Rest Framework APIs can significantly enhance the user experience by allowing for flexible and efficient data retrieval. Using DRF's built-in SearchFilter, you can easily add basic search capabilities, and with a bit of customization, create more advanced and tailored search options. For even more performance, consider integrating specialized search tools.
Feel free to extend this example by implementing different filtering strategies or integrating third-party search services as per your application needs!
Top comments (0)