Implementing Search Functionality in Django Rest Framework (DRF)

Theresa OkoroTheresa Okoro
5 min read

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'),
]

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

Advanced Search Customization

  1. 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!

0
Subscribe to my newsletter

Read articles from Theresa Okoro directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Theresa Okoro
Theresa Okoro