Implementing a search field and filters with forms in Django

Video thumbnail

Most of the applications we create or interact with have a lot of data. Therefore, it goes without saying that it's necessary to have a way to search and filter the data we need quickly and efficiently. Without further ado, let's look at the various ways we can implement search in Django, from simple Q search to full-text search. Let's create the search component.

What filtering data in Django means

How QuerySets and the .filter() method work

In Django, almost everything starts with QuerySets, which represent a collection of database objects. To filter a list, we use the filter() method, which returns only the records that meet certain conditions.

products = Product.objects.filter(category__contains='Laptops')

That example returns all products whose category contains "Laptops". It's simple, but limited: it's case-sensitive. To make it more flexible, we can use icontains.

When to use Q() for more flexible queries

When you need to combine multiple fields (for example, name, description, or category), Q() comes into play, a powerful object that allows constructing complex queries with logical operators.

from django.db.models import Q
products = Product.objects.filter(
   Q(name__icontains='laptop') |
   Q(description__icontains='laptop') |
   Q(category__icontains='laptop')
)

This is the simplest way to create a text search across multiple columns without yet relying on Postgres or advanced search.

Creating a form to filter lists

Using the GET method and a form without an action

When we want to filter data, the ideal is to use GET requests. This way, the filters are reflected in the URL and are easily shareable or reloadable.

In my case, I usually leave the action empty, so that the form submits to itself:

<form method="get" class="row">
   <div class="col-auto">
       <input type="text" name="search" class="form-control" placeholder="Search">
   </div>
   <div class="col-auto">
       <select name="category_id" class="form-select">
           <option value="">Categories</option>
           {% for c in categories %}
           <option value="{{ c.id }}">{{ c.title }}</option>
           {% endfor %}
       </select>
   </div>
   <div class="col-auto">
       <button type="submit" class="btn btn-primary">Search</button>
   </div>
   <div class="col-auto">
    	<a href="{% url 'elements:index' %}" class="btn btn-outline-secondary">Clean</a>
	</div>
</form>

The option to **clean the filter** is particularly useful for the usability of our form; having it point to the list view means the redirection automatically clears the filters.

A more discreet design, something like that. The idea of the design is for this button to look more **in the background**, and it is visually understood that the one that is more painted (more prominent) is **in the foreground**.

A row system is used, via Bootstrap or Tailwind rows, so they align in as many columns as we need.

Text fields and category selection

Here we filter by name (text field) and category (dropdown menu). If you want to add more filters, simply replicate the pattern for each model field.

Keeping filter values active after search

Something many forget: keeping the field value after submitting the form.
I solve it like this:

<input type="text" name="search" class="form-control" value="{{ search }}">

And for categories:

<option 
   {% if category_id == c.id %} selected {% endif %}
   value="{{ c.id }}">
   {{ c.title }}
</option>

This greatly improves the user experience, especially when the list is paginated.

⚙️ Implementing the filters in the view (views.py)

Getting search parameters with request.GET

In Django, accessing the submitted values is as easy as using request.GET.get():

search = request.GET.get('search') if request.GET.get('search') else ''
category_id = request.GET.get('category_id')
category_id = int(category_id) if category_id else ''

Applying simple conditionals for optional filters

Optional filters are not magic, they are simple conditionals; it's that simple, and this is the magic of client-side filters.

GET requests are used, as we are performing a query, not changing the data model. Therefore, you can add more filters or change the current ones simply by adding or changing the current conditionals and their respective form fields implemented previously.

elements = Element.objects
if search:
   elements = elements.filter(title__icontains=search)
if category_id:
   elements = elements.filter(category_id=category_id)
   elements = elements.filter(type=2).all()

Complete view example with Paginator

Finally, we paginate and send the data to the template, keeping the filter values:

from django.core.paginator import Paginator
paginator = Paginator(elements, 10)
page_number = request.GET.get('page')
return render(request, 'elements/index.html', {
   'elements': paginator.get_page(page_number),
   'search': search,
   'category_id': category_id,
   'categories': categories
})

Improving the user experience

Adding a button to clear the filters

I always add a **Clean** or **Reset** button that allows resetting the filters. It's as simple as this:

<div class="col-auto"> <a href="{% url 'elements:index' %}" class="btn btn-outline-secondary">Clean</a> </div>

Design and style of the search form

To achieve a cleaner design, I usually apply a **discreet style** to the secondary button.
The goal is for the user to visually understand which is the primary action (search) and which is the secondary one (clean).

⚠️ Common mistakes and best practices

  • Don't forget to convert IDs to integers
    • category_id = int(category_id) prevents errors when filtering foreign keys.
  • Avoid duplicating filter() queries
    • Use the same elements object and chain the filters.
  • When to use icontains instead of contains
    • icontains ignores case and improves search usability.

Conclusion and next steps

Filtering a list in Django doesn't require additional libraries: simply use request.GET, filter(), and a bit of logic.
In my case, this approach has been sufficient even in production projects.

If you need more powerful searches later on, you can integrate PostgreSQL's SearchVector and SearchQuery, or libraries like django-filter.

❓FAQs

How to apply multiple filters in Django?
Chain multiple .filter() calls or combine conditions with Q().

How to clear the filters?
Include a link that reloads the view without GET parameters.

How to keep filters when paginating?
Pass search and category_id in the context and display them in the form.

I agree to receive announcements of interest about this Blog.

We will see how to implement a search and filter field in Django from the GET form to the view, and its conditionals and advanced queries.

| 👤 Andrés Cruz

🇪🇸 En español