Django Search Tutorial
Tags: django, python, tutorial, beginners
Published: 2020-02-15 02:05:48

In this tutorial we will implement basic search in a Django website and touch upon ways to improve it with more advanced options.

**Note**: I gave a version of this tutorial at DjangoCon US 2019. You can see the video here:

<div class="bookshelf">
  <iframe width="650" height="365" src="https://www.youtube.com/embed/is3R8d420D4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

Complete source code can be found [on Github](https://github.com/wsvincent/django-search-tutorial).

To start let's create a new Django project ([see here if you need help with this](https://djangoforbeginners.com/initial-setup/)). On your command line, enter the following commands to install the latest version with `Pipenv`, create a project called `citysearch_project`, set up the initial database via `migrate`, and then start the local web server with `runserver`.

```
$ pipenv install django==3.0.3
$ pipenv shell
$ django-admin startproject citysearch_project .
$ python manage.py migrate
$ python manage.py runserver
```

If you navigate to [http://127.0.0.1:8000/](http://127.0.0.1:8000/) you'll see the Django welcome page which confirms everything is configured properly.

![Django welcome page](https://dev-to-uploads.s3.amazonaws.com/i/iymq95xzycn02l9f02qy.png)

## Cities app

Now we'll create a single app called `cities` to store a list of city names. We're keeping things intentionally basic. Stop the local server with `Control+c` and use the `startapp` command to create this new app.

```
$ python manage.py startapp cities
```

Then update `INSTALLED_APPS` within our `settings.py` file to notify Django about the app.

```python
# citysearch_project/settings.py
INSTALLED_APPS = [
    ...
    'cities.apps.CitiesConfig', # new
]
```

Now for the models. We'll call our single model `City` and it will have just two fields: `name` and `state`. Since the Django admin will be default pluralize the app name to `Citys` we'll also set `verbose_name_plural`. And finally set `__str__` to display the name of the city.

```python
# cities/models.py
from django.db import models


class City(models.Model):
    name = models.CharField(max_length=255)
    state = models.CharField(max_length=255)

    class Meta:
      verbose_name_plural = "cities"

    def __str__(self):
        return self.name
```

Ok, all set. We can create a migrations file for this change, then add it to our database via `migrate`.

```
$ python manage.py makemigrations cities
$ python manage.py migrate
```

There are multiple ways to populate a database but the simplest, in my opinion, is via the admin. Create a superuser account so we can log in to the admin.

```
$ python manage.py createsuperuser
```

Now we need to update `cities/admin.py` to display our app within the admin.

```python
# cities/admin.py
from django.contrib import admin

from .models import City

class CityAdmin(admin.ModelAdmin):
    list_display = ("name", "state",)

admin.site.register(City, CityAdmin)
```

Start up the server again with `python manage.py runserver` and navigate to the admin at [http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin) and log in with your superuser account.

![Admin Homepage](https://dev-to-uploads.s3.amazonaws.com/i/rqjh696lbqbz5lsterb1.png)

Click on the `cities` section and add several entries. You can see my four examples here.

![Admin Cities](https://dev-to-uploads.s3.amazonaws.com/i/h4kdsboxnt2bwkeilk6l.png)

## Homepage and Search Results Page

We have a populated database but there are still a few steps before it can be displayed on our Django website. Ultimately we only need a homepage and search results page. Each page requires a dedicated view, url, and template. The order in which we create these doesn't really matter; all must be present for the site to work as intended.

Generally I prefer to start with the URLs, add the views, and finally the templates so that's what we'll do here.

First, we need to add a URL path for our `cities` app which can be done by importing `include` and setting a path for it.

```python
# citysearch_project/urls.py
from django.contrib import admin
from django.urls import path, include # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('cities.urls')), # new
]
```

Second, we need a `urls.py` file within the `cities` app however Django doesn't create one for us with the `startapp` command. No worries, we can create that now from the command line as well. Stop the server with `Control+c` if it is still running.

```
$ touch cities/urls.py
```

Within this file we'll import yet-to-be-created views for each--`HomePageView` and `SearchResultsView`--and set a path for each. Note as well that we set an optional [URL name](https://docs.djangoproject.com/en/3.0/topics/http/urls/#naming-url-patterns) for each.

Here's what it looks like:

```python
# cities/urls.py
from django.urls import path

from .views import HomePageView, SearchResultsView

urlpatterns = [
    path('search/', SearchResultsView.as_view(), name='search_results'),
    path('', HomePageView.as_view(), name='home'),
]
```

Third, we need to configure our two views. The homepage will just be a template with, eventually, a search box. Django's `TemplateView` works nicely for that. The search results page will list the results we want which is a good fit for `ListView`.

```python
# cities/views.py
from django.views.generic import TemplateView, ListView

from .models import City


class HomePageView(TemplateView):
    template_name = 'home.html'

class SearchResultsView(ListView):
    model = City
    template_name = 'search_results.html'
```

Fourth and finally, we need our templates. We could add the templates within our `cities` app but I find that creating a project-level `templates` folder instead is a simpler approach.

Create a `templates` folder and then both templates `home.html` and `search_results.html`.

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

Note that we also need to update our `settings.py` to tell Django to look for this project-level `templates` folder. This can be found in the `TEMPLATES` section.

```python
# citysearch_project/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # new
        ...
    }
]
```

The homepage will just be a title at this point.

```html
<!-- templates/home.html -->
<h1>HomePage</h1>
```

Start up the web server again with `python manage.py runserver` and we can see the homepage now at [http://127.0.0.1:8000/](http://127.0.0.1:8000/).

![Homepage](https://dev-to-uploads.s3.amazonaws.com/i/5kr7rcx4grngvvky86t2.png)

Now for the search results page which will loop over `object_list`, the default name for the context object `ListView` returns. Then we'll output both the `name` and `state` for each entry.

```html
<!-- templates/search_results.html -->
<h1>Search Results</h1>

<ul>
  {% for city in object_list %}
    <li>
      {{ city.name }}, {{ city.state }}
    </li>
  {% endfor %}
</ul>
```

And...we're done. Our search results page is available at [http://127.0.0.1:8000/search/](http://127.0.0.1:8000/search/).

![Search Results Page](https://dev-to-uploads.s3.amazonaws.com/i/bkzuinc5o8t7itfm8938.png)


## Forms and Querysets

Ultimately a basic search implementation comes down to a form that will pass along a user query--the actual search itself--and then a queryset that will filter results based on that query.

We could start with either one at this point but'll we configure the filtering first and then the form.

## Basic Filtering

In Django a [QuerySet](https://docs.djangoproject.com/en/3.0/topics/db/queries/#retrieving-objects) is used to filter the results from a database model. Currently our `City` model is outputting all its contents. Eventually we want to limit the search results page to filter the results outputted based upon a search query.

There are multiple ways to customize a queryset and in fact it's possible to do filtering via a [manager](https://docs.djangoproject.com/en/3.0/topics/db/managers/#django.db.models.Manager) on the model itself but...to keep things simple, we can add a filter with just one line. So let's do that!

Here it is, we're updating the `queryset` method of `ListView` and adding a hardcoded filter so that only a city with the name of "Boston" is returned. Eventually we will replace this with a variable representing the user search query!

```
# cities/views.py
class SearchResultsView(ListView):
    model = City
    template_name = 'search_results.html'
    queryset = City.objects.filter(name__icontains='Boston') # new
```

Refresh the search results page and you'll see only "Boston" is now visible.

![Boston](https://dev-to-uploads.s3.amazonaws.com/i/2v46pazyzvqgtzd857e3.png)

It's also possible to customize the queryset by overriding the `get_queryset()` method to change the list of cities returned. There's no real advantage to do so in our current case, but I find this approach to be more flexible than just setting `queryset` attributes.

```python
# cities/views.py
...
class SearchResultsView(ListView):
    model = City
    template_name = 'search_results.html'

    def get_queryset(self): # new
        return City.objects.filter(name__icontains='Boston')
```

Most of the time the [built-in QuerySet methods](https://docs.djangoproject.com/en/3.0/topics/db/queries/#other-queryset-methods) of `filter()`, `all()`, `get()`, or `exclude()` will be enough. However there is also a very robust and detailed [QuerySet API](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#queryset-api) available as well.


## Q Objects

Using `filter()` is powerful and it's even possible to [chain filters](https://docs.djangoproject.com/en/3.0/topics/db/queries/#chaining-filters) together. However often you'll want more complex lookups such as using "OR" which is when it's time to turn to [Q objects](https://docs.djangoproject.com/en/3.0/topics/db/queries/#complex-lookups-with-q-objects).

Here's an example where we set the filter to look for a result that matches a city name of "Boston" *or* a state name that contains with "NY". It's as simple as importing `Q` at the top of the file and then subtly tweaking our existing query.

```python
# cities/views.py
from django.db.models import Q # new
...

class SearchResultsView(ListView):
    model = City
    template_name = 'search_results.html'

    def get_queryset(self): # new
        return City.objects.filter(
            Q(name__icontains='Boston') | Q(state__icontains='NY')
        )
```

Refresh your search results page and we can see the result.

![Boston & New York](https://dev-to-uploads.s3.amazonaws.com/i/7g7x84c3ytddzlddc8k4.png)


Now let's turn to our search form to replace the current hardcoded values with search query variables.

## Forms

Fundamentally a web form is simple: it takes user input and sends it to a URL via either a `GET` or `POST` method. However in practice this fundamental behavior of the web can be monstrously complex.

The first issue is sending the form data: where does the data actually go and how do we handle it once there? Not to mention there are numerous security concerns whenever we allow users to submit data to a website.

There are only two options for "how" a form is sent: either via `GET` or `POST` HTTP methods.

A `POST` bundles up form data, encodes it for transmission, sends it to the server, and then receives a response. Any request that changes the state of the database--creates, edits, or deletes data--should use a `POST`.

A `GET` bundles form data into a string that is added to the destination URL. `GET` should only be used for requests that do not affect the state of the application, such as a search where nothing within the database is changing, we're just doing a filtered list view basically.

If you look at the URL after visiting Google.com you'll see your search query in the actual search results page URL itself.

For more information, Mozilla has detailed guides on both [sending form data](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data) and [form data validation](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation) that are worth reviewing if you're not already familiar with form basics.

## Search Form

But for our purposes, we can add a basic search form to our existing homepage right now. Here's what it looks like. We'll review each part below.

```html
<!-- templates/home.html -->
<h1>HomePage</h1>

<form action="{% url 'search_results' %}" method="get">
  <input name="q" type="text" placeholder="Search...">
</form>
```

For the form the `action` specifies where to redirect the user after submission of the form. We're using the URL name for our search results page here. Then we specify the use of `get` as our `method`.

Within our single input--it's possible to have multiple inputs or to add a button here if desired--we give it a name `q` which we can refer to later. Specify the type which is `text`. And then add a `placeholder` value to prompt the user.

That's really it! On the homepage now try inputting a search, for example for "san diego".

![San Diego](https://dev-to-uploads.s3.amazonaws.com/i/wlglhlnqzzupp86nrr5v.png)

Upon hitting Return you are redirected to the search results page. Note in particular the URL contains our search query `http://127.0.0.1:8000/search/?q=san+diego`.

![San Diego Search Result](https://dev-to-uploads.s3.amazonaws.com/i/n0tdh2r0j6ghm1u54lxi.png)

However the results haven't changed! And that's because our `SearchResultsView` still has the hardcoded values from before. The last step is to take the user's search query, represented by `q` in the URL, and pass it in.

```python
# cities/views.py
...
class SearchResultsView(ListView):
    model = City
    template_name = 'search_results.html'

    def get_queryset(self): # new
        query = self.request.GET.get('q')
        object_list = City.objects.filter(
            Q(name__icontains=query) | Q(state__icontains=query)
        )
        return object_list
```

We added a `query` variable that takes the value of `q` from the form submission. Then update our filter to use `query` on both a city name and state. That's it! Refresh the search results page--it still has the same URL with our query--and the result is expected.

![San Diego](https://dev-to-uploads.s3.amazonaws.com/i/bn1ma4rtd6zllaogqres.png)

If you want to compare your code with the official source code, it can be found [on Github](https://github.com/wsvincent/django-search-tutorial).

## Next Steps

Our basic search is now complete! Maybe you want to add a button on the search form that could be clicked in addition to hitting return? Or perhaps add some form validation?

Beyond filtering with ANDs and ORs there are other factors if we want a Google-quality search, things like relevancy and much more. This talk [DjangoCon 2014: From \__icontains to search](https://youtu.be/Dz1hATSSSiQ) is a good taste of how deep the search rabbit hole can go!