Using Django Q Objects for Complex Searches
Motivation
Django Q objects are a way to construct complex queries in Django, allowing developers to build more flexible and powerful queries than can be achieved with simple queries. They are used to combine different query expressions using logical operators such as AND, OR, and NOT. Q objects can be used with the filter() method of Django's QuerySet API, allowing developers to build complex queries with multiple conditions.
Q objects can be particularly useful in cases where complex filtering conditions are required, such as when searching for records that match multiple criteria or when constructing dynamic queries based on user input. They can also be used to optimize queries by reducing the number of database hits required to retrieve the desired data.
In this article, I will show you how I used Q objects to perform complex queries in my fourth portfolio project for Code Institute: BookShelf - A Blog About Books.
The Search Requirements
On the front end, the user was presented with a search form which contained check box fields where they could select which genres of books they would like to retrieve. In addition, they were presented with three text fields to enter search terms for author, title, and description respectively. Upon clicking the submit button on the form, a django functional view called perform_search(request)
which extracted the search terms from the form, generated Q objects from those terms, and then used those Q objects to execute the query.
The code for the function can be found here: q_objects.py Gist I will take you through each of the sections step by step.
Let us take a look at the beginning of the function:
def perform_search(request): list = request.POST.getlist('genres') genre_query = Q() author_query = Q() title_query = Q() description_query = Q() liked_books = []
list = request.POST.getlist('genres')
simply gets the list of genres that the user selected from the form. liked_books = []
is an array for keeping track of which books the user has liked. This is so that the front end knows which entries to mark as liked. Keeping track of the liked books is not central to this article so I will not be discussing it.
The interesting bit is this:
genre_query = Q() author_query = Q() title_query = Q() description_query = Q()
Here, I declare four Q objects which will be used to search by genre, author, title, and description.
The next bit sees this line of code:
for gen in list: genre_query = genre_query | Q(genre__iexact=gen)
This bit loops through each selected genre in the list (labelled gen
) and adds it to the genre_query
object. Notice the genre_query
object is constructed using the OR operator | since that is what we expect - when a user selects multiple genres, we want it to be an OR search. genre__iexact=gen
means that it is searching the genre field defined in the model and the search is to be exact.
The following code:
title_search_terms = request.POST['title-search-input'] author_search_terms = request.POST['author-search-input'] description_search_terms = request.POST['description-search-input']
Simply extracts the search terms from the request and stores them into appropriately named variables.
The following snippet creates the Q objects for the title search terms.
if title_search_terms: terms = title_search_terms.split() for term in terms: title_query = title_query | Q(title__icontains=term)
- Get the title_search_terms from the request.
- Split the search terms in the title_search_terms string on spaces.
- Iterate through the search terms and construct a query object for each term.
- We add the query object for each term to the title_query object. We add them with the OR operator because that is the nature of search - we want to search for titles that contain any of the words in the list.
In a similar fashion, we make the query objects for the author and description search parameters.
if author_search_terms: terms = author_search_terms.split() for term in terms: author_query = author_query | Q(author__icontains=term) if description_search_terms: terms = description_search_terms.split() for term in terms: description_query = description_query | Q(description__icontains=term)
Now we actually perform the query:
books = Book.objects.filter(author_query, title_query, genre_query, description_query)
All you have to do is pass your query objects into the filter()
method. This is an AND search - it will search for Book objects whose author meets the query criteria AND whose title meets the query criteria AND whose genre meets the query criteria AND whose description meets the query criteria.
The next bit loops through the returned books to find which ones have been liked. This is not important for this article.
for book in books: if book.likes.filter(id=request.user.id).exists(): liked_books.append(book.id)
After the search, the template for displaying those books is loaded and a context object is created to pass the data to the template.
template = loader.get_template('index.html') context = { 'books': books, 'liked_books': liked_books, 'heading_label': 'Search Result', 'genres': Book.GENRES }
Finally, the template is rendered if the user is authenticated otherwise they are redirected into the login page.
if request.user.is_authenticated: return HttpResponse(template.render(context, request)) else: return redirect('accounts/login')
That's it! I hope you found this article useful.