Introduction
Search is one of the most useful features in any web application. Whether you are building a blog, an e-commerce store, a school platform, a documentation website, or an admin dashboard, users often need a quick way to find specific content.
Instead of manually browsing page after page, search allows users to type a keyword and instantly narrow down the results.
In Django, search functionality is usually built using:
- forms
- query parameters
- QuerySets
- field lookups such as
icontains
In this tutorial, you will learn how to build a search feature step by step, starting with a simple search form and then moving to cleaner and more practical implementations.
What You Will Learn
By the end of this tutorial, you will understand:
- what search functionality is in Django
- how to read search terms from a request
- how to search using QuerySets
- how to use
icontains - how to build a search form
- how to display search results
- how to handle empty queries
- how to combine search with pagination
- best practices for clean search pages
Prerequisites
Before starting, you should already know:
- Django models
- QuerySets and ORM
- views and templates
- forms basics
- pagination basics
1. What Is Search Functionality?
Search functionality allows users to enter a word or phrase and retrieve matching results from the database.
For example:
- searching blog posts by title
- searching products by name
- searching users by username
- searching courses by title or description
In Django, search is usually done by filtering a QuerySet based on user input.
2. A Simple Model Example
Let us suppose we have a blog post model like this:
models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.titleWe will use this model for all examples in this tutorial.
3. The Simplest Search View
The easiest way to create search is:
- read a search term from the URL
- filter the database
- send results to a template
views.py
from django.shortcuts import render
from .models import Post
def search_posts(request):
query = request.GET.get('q', '')
results = Post.objects.filter(title__icontains=query)
return render(request, 'blog/search_results.html', {
'query': query,
'results': results,
})Explanation
request.GET.get('q', '')gets the search keyword from the URLtitle__icontains=querysearches titles containing the keywordresultscontains matching posts
4. The Search URL
When using GET search, URLs often look like this:
/search/?q=django
/search/?q=python
/search/?q=formsThis is useful because:
- URLs can be bookmarked
- users can share them
- pagination works more easily with GET parameters
5. Search Form in the Template
templates/blog/search_results.html
<h1>Search Posts</h1>
<form method="get">
<input type="text" name="q" value="{{ query }}" placeholder="Search posts...">
<button type="submit">Search</button>
</form>
<hr>
{% if query %}
<p>Results for: <strong>{{ query }}</strong></p>
{% endif %}
{% for post in results %}
<h2>{{ post.title }}</h2>
<p>{{ post.content|truncatewords:20 }}</p>
<hr>
{% empty %}
{% if query %}
<p>No results found.</p>
{% endif %}
{% endfor %}What this does
- displays the search box
- keeps the previous search term in the input
- shows the matching posts
- shows “No results found” only if a search was actually made
6. Why Use icontains?
The lookup icontains means:
contains→ partial matchi→ case-insensitive
So this will match:
djangoDjangoDJANGO
Example:
Post.objects.filter(title__icontains='django')This is one of the most common ways to build simple search in Django.
7. Other Useful Lookups for Search
Exact match
Post.objects.filter(title__exact='Django')This is stricter and less flexible.
Starts with
Post.objects.filter(title__istartswith='django')Ends with
Post.objects.filter(title__iendswith='guide')Search in another field
Post.objects.filter(content__icontains='django')You are not limited to the title field.
8. Search Across Multiple Fields
Often, you want to search in more than one field.
For example:
- title
- content
To do that, use Q objects.
views.py
from django.db.models import Q
from django.shortcuts import render
from .models import Post
def search_posts(request):
query = request.GET.get('q', '')
results = Post.objects.none()
if query:
results = Post.objects.filter(
Q(title__icontains=query) | Q(content__icontains=query)
)
return render(request, 'blog/search_results.html', {
'query': query,
'results': results,
})Explanation
Q(...) | Q(...)means OR- Django returns posts where:
- title contains the query
- OR content contains the query
This gives much better search results.
9. Avoid Showing All Objects on Empty Search
In many cases, if the query is empty, you should not return all posts.
Wrong approach:
results = Post.objects.filter(title__icontains=query)
If query is empty, this may match everything.
Better:
results = Post.objects.none()
if query:
results = Post.objects.filter(title__icontains=query)This avoids showing all posts when the search box is empty.
10. Using a Django Form for Search
Instead of reading raw GET data directly, you can use a form.
forms.py
from django import forms
class SearchForm(forms.Form):
q = forms.CharField(
required=False,
max_length=100,
label='Search',
widget=forms.TextInput(attrs={'placeholder': 'Search posts...'})
)Why use a form?
- cleaner validation
- easier template rendering
- more structured code
- easier to extend later
11. Search View with Form
views.py
from django.db.models import Q
from django.shortcuts import render
from .forms import SearchForm
from .models import Post
def search_posts(request):
form = SearchForm(request.GET or None)
results = Post.objects.none()
query = ''
if form.is_valid():
query = form.cleaned_data['q']
if query:
results = Post.objects.filter(
Q(title__icontains=query) | Q(content__icontains=query)
)
return render(request, 'blog/search_results.html', {
'form': form,
'query': query,
'results': results,
})12. Template with Search Form
search_results.html
<h1>Search Posts</h1>
<form method="get">
{{ form.as_p }}
<button type="submit">Search</button>
</form>
{% if query %}
<p>Results for: <strong>{{ query }}</strong></p>
{% endif %}
{% for post in results %}
<h2>{{ post.title }}</h2>
<p>{{ post.content|truncatewords:20 }}</p>
{% empty %}
{% if query %}
<p>No results found.</p>
{% endif %}
{% endfor %}This version is cleaner and easier to maintain.
13. Add the Search URL
urls.py
from django.urls import path
from .views import search_posts
urlpatterns = [
path('search/', search_posts, name='search_posts'),
]Now users can visit:
/search/and perform searches.
14. Search from Navbar or Header
In real projects, the search form is often placed in the navbar.
Example:
<form method="get" action="{% url 'search_posts' %}">
<input type="text" name="q" placeholder="Search...">
<button type="submit">Search</button>
</form>This means the user can search from any page.
The form submits to the search page and passes the query in the URL.
15. Search with Class-Based Views
You can also implement search using ListView.
views.py
from django.db.models import Q
from django.views.generic import ListView
from .models import Post
class PostSearchView(ListView):
model = Post
template_name = 'blog/search_results.html'
context_object_name = 'results'
def get_queryset(self):
query = self.request.GET.get('q', '')
if query:
return Post.objects.filter(
Q(title__icontains=query) | Q(content__icontains=query)
)
return Post.objects.none()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['query'] = self.request.GET.get('q', '')
return contextExplanation
get_queryset()defines the filtered result setget_context_data()adds the query to the template
16. URL for Class-Based Search View
urls.py
from django.urls import path
from .views import PostSearchView
urlpatterns = [
path('search/', PostSearchView.as_view(), name='search_posts'),
]17. Template for CBV Search
<h1>Search Posts</h1>
<form method="get">
<input type="text" name="q" value="{{ query }}" placeholder="Search posts...">
<button type="submit">Search</button>
</form>
{% if query %}
<p>Results for: <strong>{{ query }}</strong></p>
{% endif %}
{% for post in results %}
<h2>{{ post.title }}</h2>
<p>{{ post.content|truncatewords:20 }}</p>
{% empty %}
{% if query %}
<p>No results found.</p>
{% endif %}
{% endfor %}18. Search + Pagination
Search results often need pagination too.
views.py
from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import render
from .models import Post
def search_posts(request):
query = request.GET.get('q', '')
results = Post.objects.none()
if query:
results = Post.objects.filter(
Q(title__icontains=query) | Q(content__icontains=query)
).order_by('-created_at')
paginator = Paginator(results, 5)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'blog/search_results.html', {
'query': query,
'page_obj': page_obj,
})19. Template for Search + Pagination
<h1>Search Posts</h1>
<form method="get">
<input type="text" name="q" value="{{ query }}" placeholder="Search posts...">
<button type="submit">Search</button>
</form>
{% if query %}
<p>Results for: <strong>{{ query }}</strong></p>
{% endif %}
{% for post in page_obj %}
<h2>{{ post.title }}</h2>
<p>{{ post.content|truncatewords:20 }}</p>
{% empty %}
{% if query %}
<p>No results found.</p>
{% endif %}
{% endfor %}
{% if page_obj.paginator.num_pages > 1 %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?q={{ query }}&page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?q={{ query }}&page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
</div>
{% endif %}Important
Notice how the query is preserved:
?q={{ query }}&page=...Without this, the search keyword would disappear when changing pages.
20. Search by Category or Other Filters
Search often works together with filters.
Example:
- keyword search
- category filter
- date filter
A basic example:
query = request.GET.get('q', '')
category = request.GET.get('category', '')
results = Post.objects.all()
if query:
results = results.filter(title__icontains=query)
if category:
results = results.filter(category__name__iexact=category)This shows how search can grow into a more advanced filter system.
21. Common Beginner Mistakes
Forgetting to use request.GET
Search forms usually use GET, not POST.
Wrong:
request.POST.get('q')Correct:
request.GET.get('q')Returning all objects on empty query
This may confuse users and slow down the page.
Not preserving the query during pagination
Users lose their results when going to the next page.
Searching only one field when users expect more
Searching both title and content usually gives better results.
Not handling “no results found”
Always show a clear message.
22. Best Practices
- use GET for search forms
- use
icontainsfor beginner-friendly flexible matching - search multiple relevant fields when appropriate
- do not return everything when the query is empty unless that is your goal
- preserve the search term in the input box
- preserve the query string during pagination
- display a clear message when there are no results
23. Full Practical Example
models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.titleforms.py
from django import forms
class SearchForm(forms.Form):
q = forms.CharField(required=False, max_length=100)views.py
from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import render
from .forms import SearchForm
from .models import Post
def search_posts(request):
form = SearchForm(request.GET or None)
results = Post.objects.none()
query = ''
if form.is_valid():
query = form.cleaned_data['q']
if query:
results = Post.objects.filter(
Q(title__icontains=query) | Q(content__icontains=query)
).order_by('-created_at')
paginator = Paginator(results, 5)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'blog/search_results.html', {
'form': form,
'query': query,
'page_obj': page_obj,
})urls.py
from django.urls import path
from .views import search_posts
urlpatterns = [
path('search/', search_posts, name='search_posts'),
]search_results.html
<h1>Search Posts</h1>
<form method="get">
{{ form.as_p }}
<button type="submit">Search</button>
</form>
{% if query %}
<p>Results for: <strong>{{ query }}</strong></p>
{% endif %}
{% for post in page_obj %}
<h2>{{ post.title }}</h2>
<p>{{ post.content|truncatewords:20 }}</p>
<hr>
{% empty %}
{% if query %}
<p>No results found.</p>
{% endif %}
{% endfor %}
{% if page_obj.paginator.num_pages > 1 %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?q={{ query }}&page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<strong>{{ num }}</strong>
{% else %}
<a href="?q={{ query }}&page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?q={{ query }}&page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
</div>
{% endif %}24. Summary
In this tutorial, you learned that:
- Django search usually works with GET parameters and QuerySets
icontainsis the most common basic lookup for searchQobjects allow search across multiple fields- search forms can be built directly or with Django forms
- search can be combined with pagination
- a good search page preserves the keyword and handles empty results well
Search functionality is one of the most practical features you can add to a Django project, and even a simple implementation can greatly improve user experience.
25. Mini Quiz
1. Which HTTP method is most commonly used for search forms?
A. POST
B. PUT
C. GET
D. DELETE
2. Which lookup is commonly used for flexible case-insensitive search?
A. exact
B. icontains
C. startswith
D. range
3. Which Django object helps combine multiple search conditions?
A. P
B. R
C. Q
D. S
4. What is the main benefit of using GET for search?
A. It hides the query
B. It deletes old results
C. The URL can be bookmarked and shared
D. It is required by Django
5. What should be preserved in pagination links on a search page?
A. The CSRF token
B. The model name
C. The search query
D. The app name
26. What Comes Next?
The next ideal tutorial is:
Tutorial: Django Context Processors
Subject: sharing common data across all templates.
💬 Comments
No comments yet. Be the first to comment!
Login to comment.