Add instant database search with Django and HTMX 🕵️

We'll create a fast and simple database search using Django and HTMX. It's easy and fast to do with HTMX.

There'll be 6 steps, plus a bonus at the end.

Here's a optional video tutorial (with me 👋) that follows the written guide below.

If you include your bonus, your final product will look like below. Let's go 🚀

Your instant, simple database search

Project Setup

First, create a new Django project and a new app. I'm calling my app 'sim'.

django-admin startproject core .
python manage.py startapp sim

In settings.py, add the new app:

# settings.py

INSTALLED_APPS = [
    # ...
    'sim',
    # ...
]

1. Create the Model

# models.py in sim
from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()

    def __str__(self):
        return self.name

Run the migrations:

python manage.py makemigrations
python manage.py migrate

2. Create Views

Create the view for the search functionality.

# views.py in sim
from django.shortcuts import render
from django.template.loader import render_to_string
from django.http import JsonResponse
from .models import Person


def search_view(request):
    all_people = Person.objects.all()
    context = {'count': all_people.count()}
    return render(request, 'search.html', context)


def search_results_view(request):
    query = request.GET.get('search', '')
    print(f'{query = }')

    all_people = Person.objects.all()
    if query:
        people = all_people.filter(name__icontains=query)
    else:
        people = []

    context = {'people': people, 'count': all_people.count()}
    return render(request, 'search_results.html', context)

3. Create Templates

  • Create a new folder called templates inside sim
  • Add a file search.html inside templates:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>People directory</title>
    <script src="https://unpkg.com/htmx.org@1.9.5" integrity="sha384-xcuj3WpfgjlKF+FXhSQFQ0ZNr39ln+hwjN3npfM9VBnUskLolQAcN80McRIVOPuO" crossorigin="anonymous"></script>
</head>
<body id="container">

<h3>
    <span class="htmx-indicator">
      ...
   </span>
</h3>

<form>
    {% csrf_token %}
    <input class="form-control" type="search"
           name="search" placeholder="Begin Typing To Search Users..."
           hx-get="/search/results/"
           hx-trigger="keyup changed, search"
           hx-target="#search-results"
           hx-indicator=".htmx-indicator">
</form>

<div id="search-results">
    {% include "search_results.html" %}
</div>

</body>
</html>

Add a file search_results.html inside templates:

Matches {{people|length}} / {{ count }} people in the directory.
{% for person in people %}
<p>{{ person.name }}</p>
<p>{{ person.description }}</p>
{% endfor %}

4. Update your urls.py

# urls.py in sim
from django.urls import path
from . import views

urlpatterns = [
    path('search/', views.search_view, name='search_view'),
    path('search/results/', views.search_results_view, name='search_results_view'),
]

Include this in the project's main urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sim.urls')),
]

5. Time for testing - Add some entries to the database

Open your django shell from the terminal with:

python manage.py shell

Then in the shell:

from sim.models import Person

people_data = [
    ("James Smith", "Always has more expressions than an emoji keyboard."),
    ("Mary Johnson", "Could turn any conversation into a monologue about coffee beans."),
    ("John Williams", "Once sent a text to a microwave by accident."),
    ("Patricia Brown", "Turns every situation into a groan-worthy pun fest."),
    ("Robert Jones", "Owns more gadgets than Batman's utility belt."),
    ("Linda Davis", "Can make a dance party happen in a library."),
    ("Michael Miller", "Treats snack bags and dip jars like a science experiment."),
    ("Jennifer Garcia", "Conversation is basically a meme showcase."),
    ("William Martinez", "Crafting attempts end up looking like modern art."),
    ("Elizabeth Robinson", "Joint pains predict the weather better than meteorologists.")
]

for name, description in people_data:
    person = Person.objects.create(name=name, description=description)
    print('🧑 Created', person.name)

Run the server.

python manage.py runserver

Visit http://127.0.0.1:8000/search/ to see your instant search in action.

Congrats 🎉 Now here's a bonus to make it look better

  1. Add styling
  2. Highlight the matched text

1. Add styling to the html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>People directory</title>
    <script src="https://unpkg.com/htmx.org@1.9.5" integrity="sha384-xcuj3WpfgjlKF+FXhSQFQ0ZNr39ln+hwjN3npfM9VBnUskLolQAcN80McRIVOPuO" crossorigin="anonymous"></script>
</head>
<body id="container">

<h3>
    Search Directory
    <span class="htmx-indicator">
      ...
    </span>
</h3>

<form>
    {% csrf_token %}
    <input class="form-control" type="search"
           name="search" placeholder="Begin Typing To Search Users..."
           hx-get="/search/results/"
           hx-trigger="keyup changed, search"
           hx-target="#search-results"
           hx-indicator=".htmx-indicator">
</form>

<div id="search-results">
    {% include "search_results.html" %}
</div>

<style>
    #container {
        max-width: 800px;
        margin: auto;
        padding: 20px;
        box-shadow: 0 4px 8px rgba(0,0,0,0.1);
    }
    h3 {
        font-size: 24px;
        margin-bottom: 20px;
    }
    .htmx-indicator {
        display: inline-block;
        margin-left: 10px;
    }
    form {
        position: relative;
    }
    .form-control {
        width: 100%;
        padding: 10px;
        font-size: 18px;
        border: 1px solid #ccc;
        border-radius: 4px;
    }
    #search-results {
        margin-top: 20px;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 4px;
    }
    .highlight {  
        background-color: #46ffb3;  
    }
</style>

</body>
</html>

2. Highlight the text matching your search query

  • Update your views.py to insert a span that highlights the matched text.
# views.py

from django.shortcuts import render
from .models import Person
from django.utils.html import format_html


def search_view(request):
    all_people = Person.objects.all()
    context = {'count': all_people.count()}
    return render(request, 'search.html', context)


def search_results_view(request):
    query = request.GET.get('search', '')
    print(f'{query = }')

    all_people = Person.objects.all()
    if query:
        people = all_people.filter(name__icontains=query)
        highlighted_people = [{'name': highlight_matched_text(person.name, query), 'description': person.description}
                              for person in people]
    else:
        highlighted_people = []

    context = {'people': highlighted_people, 'count': all_people.count()}
    return render(request, 'search_results.html', context)


def highlight_matched_text(text, query):
    """
    Inserts html around the matched text.
    """
    start = text.lower().find(query.lower())
    if start == -1:
        return text
    end = start + len(query)
    highlighted = format_html('<span class="highlight">{}</span>', text[start:end])
    return format_html('{}{}{}', text[:start], highlighted, text[end:])

Finished

All done 🎉 You now know how to add instant search to Django 🐎 This is a simple type of search that's entirely unoptimised, but is great for starting.

My advice would be to avoid optimising your search completely until it starts to slow. I'd think about optimising only then to avoid premature optimisation (my wolf's bane).

P.S - Photon Designer

I'm building Photon Designer - an entirely visual editor for building Django frontend at the speed that light hits your eyes 💡

Let's get visual.

Do you want to create beautiful django frontends effortlessly?
Click below to book your spot on our early access mailing list (as well as early adopter prices).