Implement Basic Search Into A Django Application

Nearly all web applications across the internet gives their users the ability to search through their contents for easy accessibility of resources.

In most cases, this is usually done by interacting and querying results from the database.

In this tutorial, we will implement a search functionality in Django website. Click this link to check the final source code of this tutorial.

Let’s start by creating a new Django project. Visit this beginner’s guide if you need help starting a Django project

Enter the command below to install the latest version Django on your machine.

$ pip install django

After a successful installation, let’s create a django project called ecommerce. Run this command:

$ django-admin startproject ecommerce

Run migrations and also start the development server

$ python manage.py migrate
$ python manage.py runserver

Let’s dive into the core concept of the program. We are going to create
an application named products. The products app will be used to store information about our products(in our case since we are creating an ecommerce project).

Stop the development server with ctrl + c. Then run the this command to create the products app

$ python manage.py startapp products

Open the settings.py file in the root directory and add the created products app to it. This is a way of making Django aware of the app:

ecommerce/settings.py

INSTALLED_APPS = [
...
'products', # newly created app
]

The next thing is to define the schema of the products database.

In the models.py file, let's create the models.

products/models.py

class Category(models.Model):
title = models.CharField(max_length=15, unique=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True, auto_now=False, null=True)

class Meta:
verbose_name_plural = "Categories"

def __str__(self):
return self.title


class Product(models.Model):
name = models.CharField(max_length=200)
price = models.FloatField()
category = models.ForeignKey('Category', blank=True, on_delete=models.CASCADE, null=True)

Up here, we created our models by defining two classes: Product and Category. We want each Product in the store to have a category. That calls for the category field defined on Product.

We need to run makemigrations since there have been some changes to the models.py file. Then make the actual changes in the database using the migrate command

$ python manage.py makemigrations products
$ python manage.py migrate

Now, let’s register the models created to the admin in order to have access to them

Open admin.py file and edit it as follow

products/admin.py

from django.contrib import admin

We need a way to populate the database with some data to work with. Let’s simply usin the admin provided by Django. To gain access, we will need to create a superuser for the website. Go the the command line and run the commands below to create a superuser.

$ python manage.py createsuperuser

Follow the prompt to create a username and password to gain access to the admin.

Start the server again with $ python manage.py runserver. Then visit http://127.0.0.1:8000/admin/. Login with the credentials you created recently.

Click on Categories and Products to add some data to the database. Here, I have added 5 different products and 3 categories

For now, we have successfully loaded some dummy data into the DB. We need to figure how we can display this on the website. We will need a page that will serve as our index page(Home Page) and another page to show our searched results.

Let’s start by defining urls to handle our products app. We do by making few changes to the auto-generated urls.py file created for us when we start our ecommerce project

ecommerce/urls.py

from django.contrib import admin
from django.urls import path
from django.urls import path, include # newly added

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('products.urls')), # newly added
]

Up here, we spedify the url path for our product app using include which we imported at the top of the file.

Next, we need to create a new urls.py file inside the product app

$ touch products/urls.py

products/urls.py

from django.urls import path
from . import views

In this products/urls.py file, we specified two paths. The first path query/ is used to handle the search results while the second path "" which is an empty string will be match to the base url(in this case, 127.0.0.1 or example.com in a real domain name).

Note:
We are using a Function-Based View here. Function query and home located inside products/views.py would handle the path to query/ and "" respectively.

Check this link to know more about Class-Based View and Function-Based View

Now, let’s define the views in the views.py file. Those views will handle paths specified in the products/urls.py

products/views.py

from django.shortcuts import render
# Create your views here.
def query(request):
return render(request, 'search_results.html')
def home(request):
return render(request, 'home.html')

The views here now return html files which are finally rendered when a user visits those urls.

We will put all our templates(html files) in a folder named templates. This folder should be located at the root directory(where use find the manage.py file) inside the ecommerce folder.

Create folder named templates. Then create two files(home.html and search_results.html)

$ mkdir templates
$ touch templates/home.html
$ touch templates/search_results.html

Now, we need tell Django where to look when searching for our templates. We specify this in the settings.py file.

ecommerce/settings.py

TEMPLATES = [
{
...
'DIRS': ['templates'], # newly added
...
}
]

Make these changes to both home.html and search_results.html:

templates/home.html

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home</title>
</head>
<body>
<h1>Home Page</h1>
</body>
</html>

templates/search_results.html

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search</title>
</head>
<body>
<h1>Search Page</h1>
</body>
</html>

Now, lets’s start handling the logic to display dynamic results based on the user input through a form. Most of the tasks will be done in theproducts/views.py file.

Before moving forward, we want to make sure that there is a form in the home page so that user can easily search products from the DB. By default, the home page list all products availbale.

Make these chanhes to the views.py and the home.html template

products/views.py

from django.shortcuts import render
from .models import * #newly added
# Create your views here.
def query(request):
return render(request, 'search_results.html')
def home(request):
products = Product.objects.all() #newly added
return render(request, 'home.html', {'products':products }) #newly added {'products':products }

We imported all models (Product and Category) from models.py file in line 2 with code snippet: from .models import *

Then, we get all products from the DB using objects.all() called on Product
i.e. Product.objects.all(). This query is thereafter set to a variable, products(i.e. products = Product.objects.all() ), which is then passed as a dictionary into the render()method.

Hopefully that explains a bit of what is going on under the hood.

Make a simple change to the home.html to include a form and a button

templates/home.html

<h1>Home Page</h1> 
<form style="text-align: center; margin: 20px;" action="{% url 'query_results' %}" method="get">
<input name="query" type="text" placeholder="Search Products"> <button type="submit">Search</button>
</form>
{% for product in products %}
<div style="text-align: center;">
{{product.name}} - {{product.price}} - {{product.category}}
<hr>
</div>
{%endfor%}

The new home page now looks like this with a search box

NOTE: We are using a get method in the form when it is submitted. We specified action the action url to be query_results, which is the name of the url we defined earlier.

Add this final touch to both views.py and search_results.html so that queries based on the search input can be generated when the button is submitted.

products/views.py

from django.db.models import Q # new
from django.shortcuts import render
from .models import *
# Create your views here.
def query(request):
query = request.GET.get('query') # new
results = Product.objects.filter( # new
Q(name__icontains=query) | Q(price__icontains=query) # new
| Q(category__title__icontains=query) # new
)
return render(request, 'search_results.html', # new
{ 'results':results, })

First, we imported the Q object in line 1. This is used to perform robust and complex lookups. Filter() works great but it is more advisable to use Q objects here.

We perform some queries using icontains.

In this query, we are looking for every single match based on the user input from the form. This means query results are based on name, price, and category of a product. Query result is then set to a variable named results. We can now have access to results in search_results.html .

templatess/search_results.html

<body style="text-align: center;">
<h1>Search Page</h1>
<hr>
{% for result in results %}
<div style="text-align: center;">
{{result.name}} - {{result.price}} - {{result.category}}
<hr>
</div>
{% empty %}
<h2 >No results found</h2>
<div >
<a href="{% url 'home' %}"> < Back Home</a>
</div>

Everything is working fine now. However, we get ValueErrorif we visit http://127.0.0.1:8000/query, the url defined for searching.

A quick fix to this is to check if the value of query is None in views.py and redirect to the home page.

Make this change to products/views.py:

products/views.py

from django.shortcuts import render,redirect # redirect
from django.db.models import Q
from .models import *
# Create your views here.
def query(request):
query = request.GET.get('query')
if query is None: # new
return redirect('home') # new
else:
results = Product.objects.filter(
Q(name__icontains=query) | Q(price__icontains=query)| Q(category__title__icontains=query))
return render(request, 'search_results.html',{ 'results':results, })
def home(request):
products = Product.objects.all()
return render(request, 'home.html',
{ 'products':products })

Check the final source code of this tutorial on Github.

l love writing code to solve problems 🛠️