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:
icontainsIn 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.
By the end of this tutorial, you will understand:
icontainsBefore starting, you should already know:
Search functionality allows users to enter a word or phrase and retrieve matching results from the database.
For example:
In Django, search is usually done by filtering a QuerySet based on user input.
Let us suppose we have a blog post model like this:
models.pyfrom 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.
The easiest way to create search is:
views.pyfrom 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,
})request.GET.get('q', '') gets the search keyword from the URLtitle__icontains=query searches titles containing the keywordresults contains matching postsWhen using GET search, URLs often look like this:
/search/?q=django
/search/?q=python
/search/?q=formsThis is useful because:
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 %}icontains?The lookup icontains means:
contains → partial matchi → case-insensitiveSo this will match:
djangoDjangoDJANGOExample:
Post.objects.filter(title__icontains='django')This is one of the most common ways to build simple search in Django.
Post.objects.filter(title__exact='Django')This is stricter and less flexible.
Post.objects.filter(title__istartswith='django')Post.objects.filter(title__iendswith='guide')Post.objects.filter(content__icontains='django')You are not limited to the title field.
Often, you want to search in more than one field.
For example:
To do that, use Q objects.
views.pyfrom 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,
})Q(...) | Q(...) means ORThis gives much better search results.
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.
Instead of reading raw GET data directly, you can use a form.
forms.pyfrom django import forms
class SearchForm(forms.Form):
q = forms.CharField(
required=False,
max_length=100,
label='Search',
widget=forms.TextInput(attrs={'placeholder': 'Search posts...'})
)views.pyfrom 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,
})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.
urls.pyfrom 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.
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.
You can also implement search using ListView.
views.pyfrom 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 contextget_queryset() defines the filtered result setget_context_data() adds the query to the templateurls.pyfrom django.urls import path
from .views import PostSearchView
urlpatterns = [
path('search/', PostSearchView.as_view(), name='search_posts'),
]<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 %}Search results often need pagination too.
views.pyfrom 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,
})<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 %}Notice how the query is preserved:
?q={{ query }}&page=...Without this, the search keyword would disappear when changing pages.
Search often works together with filters.
Example:
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.
request.GETSearch forms usually use GET, not POST.
Wrong:
request.POST.get('q')Correct:
request.GET.get('q')This may confuse users and slow down the page.
Users lose their results when going to the next page.
Searching both title and content usually gives better results.
Always show a clear message.
icontains for beginner-friendly flexible matchingmodels.pyfrom 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.pyfrom django import forms
class SearchForm(forms.Form):
q = forms.CharField(required=False, max_length=100)views.pyfrom 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.pyfrom 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 %}In this tutorial, you learned that:
icontains is the most common basic lookup for searchQ objects allow search across multiple fieldsSearch 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.
A. POST
B. PUT
C. GET
D. DELETE
A. exact
B. icontains
C. startswith
D. range
A. P
B. R
C. Q
D. S
A. It hides the query
B. It deletes old results
C. The URL can be bookmarked and shared
D. It is required by Django
A. The CSRF token
B. The model name
C. The search query
D. The app name
The next ideal tutorial is:
Tutorial: Django Context Processors
Subject: sharing common data across all templates.