WDTUTORIALS
menu
Django - The Easy Way Django - The Easy Way
Samuli Natri 2018.02.28
Samuli Natri is a software developer who enjoys programming games and web applications. He attended Helsinki University Of Technology (Computer Science) and Helsinki University (Social Sciences).

Django - Fast Search With Elasticsearch

Tutorial on how to implement a search feature with Elasticsearch. Elasticsearch is an open source search engine written in Java.

Java Installation

First make sure you have at least Java version 8 installed in your system.

You can check the Java version with java -version

To install the latest version, go to Oracle website and search for Java SE Downloads.

Select the JDK (Java Standard Edition Development Kit) package.

Download the installer for your operating system and run it.

Elasticsearch Installation

Go to elastic.co, click downloads and Elasticsearch. Download the package, extract it somewhere and run the executable in the bin directory.

At the moment Elasticsearch 6 is not supported by the Django package I'm using but there is a pull request for the support. Let's use version 5:

Go to past releases to find it.

curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.8.tar.gz
tar -xvf elasticsearch-5.6.8.tar.gz
cd elasticsearch-5.6.8/bin
./elasticsearch

If you get an error like low disk watermark exceeded, disable the disk allocation threshold in elasticsearch.yml:

cluster.routing.allocation.disk.threshold_enabled: false

Visit the localhost port 9200 to see if the engine is working:

http://127.0.0.1:9200/

You should see something like this:

{
  "name" : "kyLIDxu",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "p799wd-rSAi4ZDLcsy8xlw",
  "version" : {
    "number" : "5.6.8",
    "build_hash" : "688ecce",
    "build_date" : "2018-02-16T16:46:30.010Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.1"
  },
  "tagline" : "You Know, for Search"
}

Django Integration

I'm using the django-elasticsearch-dsl package in this tutorial.

It's built on top of elasticsearch-dsl-py, which itself is built on the official low-level client: elasticsearch-py

So if you want to build some kind of custom solution, start with the low-level client.

Install the package:

pip install django-elasticsearch-dsl

Create an app for the search functionality:

python manage.py startapp search

Edit the settings file and add your app, and the django_elasticsearch_dsl to the INSTALLED_APPS list:

'django_elasticsearch_dsl',
'search',

Specify the search engine host with this code:

ELASTICSEARCH_DSL = {
    'default': {
        'hosts': 'localhost:9200'
    },
}

Create a file documents.py inside the search app folder and fill it with this information:

from django_elasticsearch_dsl import DocType, Index
from blog.models import Post

posts = Index('posts')

@posts.doc_type
class PostDocument(DocType):
    class Meta:
        model = Post
        
        fields = [
            'title',
            'id',
            'slug',
            'image',
            'description',
        ]

In here we create an index called posts and connect the Post model with the engine by subclassing DocType.

We also specify the fields we want to index from the Post model.

In this tutorial we will be searching only by the title, but I also index other fields so we can use them in the result page without accessing the site database. Like the id or slug for creating a link and image for the image path.

The actual Post model looks like this in the example site:

from django.db import models
from django.utils.text import slugify

class Post(models.Model):

    title = models.CharField(max_length=255, blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    image = models.ImageField(upload_to="post_images")
    body = models.TextField(blank=True, null=True)
    order = models.IntegerField(blank=True, null=True)
    
    slug = models.SlugField(default='', blank=True)
    
    def save(self):
        self.slug = slugify(self.title)
        super(Post, self).save()
    
    def __str__(self):
        return '%s' % self.title


Run this command to index the blog posts:

python manage.py search_index --rebuild

Search Page

Open the search app views.py file and write these lines in it:

from django.shortcuts import render

from search.documents import PostDocument

def search(request):

    q = request.GET.get('q')

    if q:
        posts = PostDocument.search().query("match", title=q)
    else:
        posts = ''
    
    return render(request, 'search/search.html', {'posts': posts})

In here we get the search word q and find titles matching that word.

Add url pattern for the search page. In here I add the pattern to the main urls.py file:

from search import views as search_views # < here

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('base.urls')),
    url(r'^search/', search_views.search, name='search'), # < here
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Create a template in search/templates/search/search.html.

Simple example:

<form method="get">
 
  <input id="q" name="q" type="text" placeholder="your search...">
 
</form>

{% for item in posts %}

  {{ item.id }}
  {{ item.title }}
  {{ item.slug }}
  {{ item.image }}

  <br>

{% endfor %}

Try to search for title words and you should see results.

Also if we add content, it will be indexed automatically.

Theme Example

Here is more complete theming example.

Load Font Awesome library for the search icon in the head section:

<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

Template

{% extends 'base/base.html' %}

{% block content %}

<form class="search" method="get">

  <i class="fa fa-search search-icon"></i>
  <input class="search-input" id="q" {% if request.GET.q %}value="{{ request.GET.q }}"{% endif %} name="q" type="text" placeholder="your search...">
 
</form>

{% for item in posts %}

<a class="post" href="/blog/{{ item.slug }}/{{ item.id }}/">
  <div class="post__title post__item">
    {{ item.title }}
  </div>
  <div class="post__image post__item">
    <img class="post__image__img" src="{{ item.image }}">
  </div>
  <div class="post__description post__item">
    {{ item.description | truncatechars:400 | safe }}
  </div>
  <a class="post__read-more post__item">Read more</a>
</a>

{% endfor %}

{% endblock %}

CSS

.search {
  margin: 3em 0 1em 0;
  position: relative; }
  .search-icon {
    position: absolute;
    top: 0.8em;
    left: 1em;
    font-size: 20px;
    color: #C6C6C6; }
  .search-input {
    width: 100%;
    padding: 0.5em 0.8em;
    border-radius: 3px;
    border: 1px solid #ccc;
    padding-left: 2.2em;
    color: #1067BF;
    font-size: 1.5em;
    font-weight: 300; }

::-webkit-input-placeholder {
  /* Chrome/Opera/Safari */
  color: #ccc; }

::-moz-placeholder {
  /* Firefox 19+ */
  color: #ccc; }

:-ms-input-placeholder {
  /* IE 10+ */
  color: #ccc; }

:-moz-placeholder {
  /* Firefox 18- */
  color: #ccc; }

.post__title {
  font-size: 40px;
  font-weight: 400; }

.post__image__img {
  width: 100%;
  border-radius: 1px; }

.post__description {
  font-size: 1.2em;
  line-height: 1.4em; }

.post__read-more {
  display: inline-block;
  padding: 0.5em 0.8em;
  background-color: #4ECB4E;
  color: #fff;
  text-transform: uppercase;
  font-size: 14px;
  letter-spacing: 1px; }

.post__item {
  margin-top: 1em; }

Styling In SASS

.search {
  margin: 3em 0 1em 0;
  position: relative;

  &-icon {
    position: absolute;
    top: 0.8em;
    left: 1em;
    font-size: 20px;
    color: #C6C6C6;
  }

  &-input {
    width: 100%;
    padding: 0.5em 0.8em;
    border-radius: 3px;
    border: 1px solid #ccc;
    padding-left: 2.2em;
    color: #1067BF;
    font-size: 1.5em;
    font-weight: 300;
  }
}

$placeholder-fg: #ccc;

@mixin placehold {
  color: $placeholder-fg;
}

::-webkit-input-placeholder { /* Chrome/Opera/Safari */
  @include placehold;
}

::-moz-placeholder { /* Firefox 19+ */
  @include placehold;
}

:-ms-input-placeholder { /* IE 10+ */
  @include placehold;
}

:-moz-placeholder { /* Firefox 18- */
  @include placehold;
}

.post {
  &__title {
    font-size: 40px;
    font-weight: 400;
  }
  &__image {
    &__img {
      width: 100%;
      border-radius: 1px;
    }
  }
  &__description {
    font-size: 1.2em;
    line-height: 1.4em;
  }
  &__read-more {
    display: inline-block;
    padding: 0.5em 0.8em;
    background-color: #4ECB4E;
    color: #fff;
    text-transform: uppercase;
    font-size: 14px;
    letter-spacing: 1px;
  }
  &__item {
    margin-top: 1em;
  }
}