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.title

We 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 URL
  • title__icontains=query searches titles containing the keyword
  • results contains matching posts

4. The Search URL

When using GET search, URLs often look like this:

/search/?q=django
/search/?q=python
/search/?q=forms

This 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 match
  • i → case-insensitive

So this will match:

  • django
  • Django
  • DJANGO

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 context

Explanation

  • get_queryset() defines the filtered result set
  • get_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 icontains for 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.title

forms.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
  • icontains is the most common basic lookup for search
  • Q objects 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.