My first few months of working with Django Rest Framework was a delight because so much functionality comes out of the box. For example, let's say you have a user model and you want to search for a user via their username. You can add this functionality by defining a filtering backend (in our case, using DRF's SearchFilter) and a search field specifying which field to search upon like this:
class UserViewSet(viewsets.ReadOnlyModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer filter_backends = (filters.SearchFilter,) search_fields = ('username',)
Now if you want to search for users who’s username contains the word ‘john’ can just make GET request to the endpoint, /users?search=john.
Easy enough. But what if you want a single endpoint that will make queries over multiple models? Because the search functionality we just introduced is applied per view level, we have to make some slight customizations in order to get global search over many models.
Global Search Over Multiple Models
In this blog post we’ll base our example off the Snippets tutorial from the Django Rest Framework website. The snippets app lets a user create and save code snippets as well as highlight them. What we want to add is a custom endpoint that will search across all our user and snippet models and return our list of model instances that matches the input query. So, if we make a GET request on the endpoint /search/?query=your_query, it will search and return serialized json data from both the user and snippet models that will match our query.
Customizing Django Rest Framework
To begin, we’ve set up a basic /search/ endpoint that will list all our search results.
urlpatterns = patterns('', ... url(r'^search/$', GlobalSearchList.as_view(), name="search"), )
Then we built a GobalSearchList view set that returns the appropriate data. GlobalSearchList inherits from ListAPIView, one of many of DRF’s view classes, that provides us with a read-only endpoint on a collection of model instances. We define this collection within our get_queryset method. Within this method, we capture the query parameter and make two separate queries, filtering against fields in both our snippets and users model instances, and the results are then returned in a combined list.
class GlobalSearchList(generics.ListAPIView): serializer_class = GlobalSearchSerializer def get_queryset(self): query = self.request.QUERY_PARAMS.get('query', None) snippets = Snippet.objects.filter(Q(code__icontains=query) | Q(highlighted__icontains=query) | Q(language__icontains=query)) users = User.objects.filter(username__icontains=query) all_results = list(chain(snippets, users)) all_results.sort(key=lambda x: x.created) return all_results
In the above example, you may have also noticed that we specified a custom serializer class called GlobalSearchSerializer. When we return our data from our GlobalSearchList viewset, it's important to apply the appropriate serializer to the model instances returned. We can customize this behavior by overriding the to_native method so that Snippet instances are serialized using a SnippetSerializer, and User instances are serialized using a UserSerializer.
Here’s an example of a global search serializer that may do this:
class GlobalSearchSerializer(serializers.ModelSerializer): class Meta: model = User def to_native(self, obj): if isinstance(obj, Snippet): serializer = SnippetSerializer(obj) elif isinstance(obj, User): serializer = UserSerializer(obj) else: raise Exception("Neither a Snippet nor User instance!") return serializer.data
If you'd like to follow along, you can start with the snippets tutorial from the Django Rest Framework website here.
Also, special thanks to Baylee for the original code and for being an amazing mentor.