Elastic Search with Django Rest Framework

Objective: Adding a full-text search in Django Rest Framework using elastic search.

Elasticsearch is a distributed, open-source search and analytics engine designed for handling large volumes of data. It is built on top of the Apache Lucene library and allows you to perform full-text searches, faceted searches, and geospatial searches, among other features.

One of the main advantages of Elasticsearch is its scalability. It can be easily scaled horizontally by adding more nodes to a cluster, which allows it to handle increasing amounts of data and search queries.


Project Setup

First, we will need a running elastic search database. For this, we will use the docker.

# dockerfile
FROM elasticsearch:8.5.3

ENV discovery.type single-node
ENV xpack.security.enabled=false

EXPOSE 9200 9300

CMD ["/usr/share/elasticsearch/bin/elasticsearch"]

Explanation

discovery.type single-node This configuration is useful in development or testing environments, where you may only need a single node for testing or small-scale projects.

xpack.security.enabled=false this disables the usage of X-Pack security features. X-Pack is a set of additional security features provided by Elasticsearch, such as authentication, authorization, and encryption.

Second, we will need a Django project configured with DRF and a simple model with some data in it. the packages we are going to use currently work with Python v3.6 to v3.9, so please make sure you are using the correct version.

mkdir es_with_drf && cd es_with_drf
conda create -n elastic-with-drf
conda activate elastic-with-drf
conda install python=3.9

pip install django djangorestframework
django-admin startproject config .
python manage.py startapp app1
# config/settings.py
INSTALLED_APPS = [
    ...
    "app1",
    "rest_framework",
]

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 50,
    'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer']
}
# app1/models.py
from django.db import models

class City(models.Model):
    name = models.CharField(max_length=100)
    state_code = models.CharField(max_length=100)
    state_name = models.CharField(max_length=100)
    country_code = models.CharField(max_length=100)
    country_name = models.CharField(max_length=100)

    def __str__(self):
        return "%s: %s: %s" % (self.name, self.state_code, self.country_code)

For the data, I will be using https://github.com/dr5hn/countries-states-cities-database. After importing the data. We will work on adding the elastic search to this project.

pip install django-elasticsearch-dsl-drf

then connect our project to the elastic search container that we started in docker.

# config/settings.py

INSTALLED_APPS = [
    ...
    "django_elasticsearch_dsl",
    "django_elasticsearch_dsl_drf",
]

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

create a document file in the app folder to specify the model which we want to index in our elastic search

# app1/documents.py

from django_elasticsearch_dsl import Document
from django_elasticsearch_dsl.registries import registry
from app1.models import City


@registry.register_document
class CityDocument(Document):
    class Index:
        name = 'city'

    settings = {
        'number_of_shards': 1,
        'number_of_replicas': 0
    }

    class Django:
        model = City
        fields = ["name", "state_code", "state_name", "country_code", "country_name"]

then we will need a serializer for the City document we just created.

# app1/serializers.py

from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
from app1.documents import CityDocument


class CityDocumentSerializer(DocumentSerializer):
    class Meta:
        document = CityDocument
        fields = "__all__"

finally we can create a view and add it to the URLs file

# app1/views.py

from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
from django_elasticsearch_dsl_drf.filter_backends import CompoundSearchFilterBackend
from elasticsearch.exceptions import NotFoundError

from app1.documents import CityDocument
from app1.serializers import CityDocumentSerializer
from rest_framework.exceptions import NotFound


class CityDocumentView(BaseDocumentViewSet):
    document = CityDocument
    serializer_class = CityDocumentSerializer

    filter_backends = [CompoundSearchFilterBackend]

    # Define search fields
    search_fields = {
        'name': {'fuzziness': 'AUTO'},
    }

    def list(self, request, *args, **kwargs):
        try:
            return super().list(request, *args, **kwargs)
        except NotFoundError:
            raise NotFound(f"{self.index} index not found")

You can look into this for more customizations https://django-elasticsearch-dsl-drf.readthedocs.io/

# config/urls.py

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

from app1.views import CityDocumentView

from rest_framework.routers import DefaultRouter

router = DefaultRouter()
books = router.register("city", CityDocumentView, basename="citydocument")

urlpatterns = [
    path("", include(router.urls)),
    path("admin/", admin.site.urls),
]

now if we hit the API, it will generate an exception index_not_found_exception because we haven't indexed the City data in elastic search. To resolve this, we will index the data by entering this command.

python manage.py search_index --populate

Now when we hit http://127.0.0.1:8000/city/ we will get this response (based on the stored data)

{
    "count": 10000,
    "next": "http://localhost:8000/city/?page=2",
    "previous": null,
    "facets": {},
    "results": [
        {
            "name": "Departamento de Sarmiento",
            "country_code": "AR",
            "state_code": "G",
            "state_name": "Santiago del Estero",
            "country_name": "Argentina",
            "id": 1501
        },
        {
            "name": "Farah",
            "country_code": "AF",
            "state_code": "FRA",
            "state_name": "Farah",
            "country_name": "Afghanistan",
            "id": 22
        },
        ....
    ]
}

and to perform a search http://localhost:8000/city/?search=delih, it will return the correct data.

{
    "count": 19,
    "next": null,
    "previous": null,
    "facets": {},
    "results": [
        {
            "name": "Delhi",
            "country_code": "IN",
            "state_code": "DL",
            "state_name": "Delhi",
            "country_name": "India",
            "id": 45445
        },
        {
            "name": "Delia",
            "country_code": "IT",
            "state_code": "82",
            "state_name": "Sicily",
            "country_name": "Italy",
            "id": 60369
        },
        ....
    ]
}

Source Code