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:

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:

Prerequisites

Before starting, you should already know:

1. What Is Search Functionality?

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.

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:

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

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:

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

6. Why Use icontains?

The lookup icontains means:

So this will match:

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:

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

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?

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

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:

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

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:

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.