This tutorial is written by MongoDB Champion Marko Aleksendrić
Introduction: Django MongoDB Backend functionality
In today's data-driven web applications, developers face an increasingly complex challenge: how to efficiently store, retrieve, and search through vast amounts of diverse data while maintaining the rapid development cycles that all modern businesses demand. Traditional relational databases, while reliable, often become bottlenecks when applications need to handle unstructured data, perform complex searches, or scale horizontally across distributed systems.
This is where three powerful technologies converge to create a truly superior solution: Elasticsearch, MongoDB, and Django.
Elasticsearch stands as the industry-leading distributed search and analytics engine, built on Apache Lucene and designed specifically to handle real-time search operations, complex queries, and large-scale data analysis. Unlike traditional databases that treat search as a secondary concern, Elasticsearch is built and optimized from the ground up for lightning-fast retrieval and sophisticated text analysis, making it the go-to choice for applications requiring advanced search capabilities, log analytics, and business intelligence.
MongoDB revolutionizes data storage by spearheading a document-oriented, NoSQL approach that mirrors how developers naturally think about data structures. Instead of forcing complex objects into rigid table schemas, MongoDB allows applications to store data as flexible JSON-like documents (BSON), enabling rapid prototyping, easier scaling, and more intuitive data modeling for modern applications dealing with varied and evolving data structures. On top of that, MongoDB's aggregation framework enables the building of complex data pipelines.
Django, Python's premier web framework, has traditionally been tightly coupled with relational databases, but the introduction of the specialized Django MongoDB Backend has opened new possibilities for developers who want to leverage Django's robust ecosystem while embracing NoSQL flexibility. This integration maintains Django's beloved features—its ORM-like query capabilities, admin interface, and rapid development tools—while unlocking MongoDB's document-based advantages.
The real magic happens when these three technologies work in harmony and synergy, creating an architecture that combines MongoDB's flexible data storage, Elasticsearch's powerful search capabilities, and Django's development efficiency into a single, cohesive platform that can handle both complex data relationships and demanding search requirements at scale.
In this tutorial, we will build together a very simple application that blends these technologies!
Installing Elasticsearch on a Windows or Linux machine, the easiest way to get going with Docker
We will begin by installing Elasticsearch on our machine. Since the goal of this tutorial is to get up to speed with a very simple yet extensible setup, we will start with the local development integration . For the local installation, you will need to download and install Docker and enable the Windows Linux Subsystem if you are on a Windows machine.
The installation itself couldn’t be more simple. Just run the command provided on the Elastic website and give it a couple of minutes, depending on your system speed, to finish:
curl -fsSL https://elastic.co/start-local | sh
After the command is executed successfully, you will be able to access an insecure, but operational, instance of Elastic with Kibana running on http://localhost:5601/ and Elasticsearch running on http://localhost:9200. All of these ports are fully customisable and they do not clash with our Django project or MongoDB ports. When working with this stack, we will need a lot of ports!
The part with the Elastic and Kibana installation is complete at this point, and you should explore the previously listed URLs in order to verify that everything is working. Elastic provides excellent documentation for all kinds of setups. Again, the aim of this tutorial is just to get you up to speed with the Django MongoDB Backend, so we will leave the setup intricacies for more advanced cases. Now, let’s create a local Atlas deployment!
Creating a local Atlas deployment on Docker with the Atlas CLI
In this project, we will use a local Atlas environment. After ensuring the required software is installed for your operating system, simply open a terminal and type the command:
atlas deployments setup
Select Local Database deployment.
The terminal will output the result of the deployment and the connection string (the name of your cluster might vary):
How do you want to set up your local Atlas deployment? default Creating your cluster local1524 1/3: Starting your local environment... 2/3: Downloading the latest MongoDB image to your local environment... 3/3: Creating your deployment local1524... Deployment created! Connection string: "mongodb://localhost:12404/?directConnection=true"
Copy this connection string, as we will use it throughout our project. Keep in mind that the connection string can point to an online Atlas instance with no modifications. Docker isn't necessary for this tutorial, but was a personal choice. Since our Elasticsearch and Kibana instances will also run on Docker containers and we are making a local development setup with emphasis on simplicity, it is a logical choice.
Setting up a Django MongoDB Backend project
We are now ready to create our Django MongoDB Backend project that will provide the backbone of the entire project. First, let’s create a virtual environment in our project folder:
python -m venv venv
Activate it:
source venv/bin/activate
In case you are running on Windows, the command for activating the virtual environment is:
./venv/bin/activate
Now, we are ready to install the dependencies: the back end and the package for integrating Elasticsearch into our project. Create a requirements.txt file and populate it:
django-elasticsearch-dsl==8.0 django-mongodb-backend==5.2.0b0
Install the packages with:
pip install -r requirements.txt
We are off to the races!
We are now ready to create a new Django MongoDB Backend application, by following the official quickstart and building a simple app for articles (title, category, content). The getting started guide is most useful.
Let’s create a new DMB project by cloning the official repo:
django-admin startproject elasticdjango--template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/5.2.x.zip
This command will create a brand new Django project, while taking care of the connection string in the settings.py file, and making sure that the migrations take into account the ObjectId. The created project is named elasticdjango, and this name will be used throughout the settings.py file.
The project structure is discussed in the quickstart, so we will immediately modify the settings.py file:
DATABASES = { "default": django_mongodb_backend.parse_uri( "mongodb://localhost:12703/elasticdb?directConnection=true" ), }
After creating the project, we are ready to start the development server and see if everything is working (and be greeted with the familiar Django rocket!):
cd elasticdjango python manage.py runserver
You should be greeted by the Django default screen and a message stating that there are unapplied migrations.
Now, we can create a simple application for hosting our articles. To create our application, we will again use another convenient template provided by the Django Mongo Backend team, making sure to set the name of the app to settings.py_. This template ensures that the Django app is compatible with the Backend. The magic lies in the _app.py file in which the default id field is set to the MongoDB ObjectId:
default_auto_field = 'django_mongodb_backend.fields.ObjectIdAutoField'
Now, we are ready to proceed with the data modeling. As stated earlier, the models.py will need to reflect a rather simple document structure: a title, a category, and the content. Open the models.py file in the articles folder and add the following:
from django.db import models class Article(models.Model): title = models.CharField(max_length=300) content = models.TextField(blank=False) category = models.TextField(blank=False) class Meta: db_table = "articles" managed = False def __str__(self): return f"{self.title} ({self.category})"
The models.py is probably as simple as it gets! After saving the models.py file, it is important to make Django aware of our new application. Open the settings.py file and edit the INSTALLED_APPS part:
INSTALLED_APPS = [ "articles.apps.ArticlesConfig", "elasticdjango.apps.MongoAdminConfig", "elasticdjango.apps.MongoAuthConfig", "elasticdjango.apps.MongoContentTypesConfig", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ]
Later on, we will add another application—the Django Elasticsearch DSL—but for now, let’s wrap up the articles app. Let us now hook up the admin.py file in order to have access to one of Django’s main selling points: the wonderful Django Admin site. Create a new file in the articles folder named admin.py , and enable the admin functionality for the articles:
from django.contrib import admin from .models import Article admin.site.register(Article)
All that is left to do at this point is to create and run the migrations for the articles app (and the other migrations that we haven’t run yet). At the command prompt, enter:
python manage.py makemigrations python manage.py migrate
We have now created a simple Django MongoDB Backend application and we are ready to prepare the Elastic setup.
Setting up Django Elasticsearch
At this point, we need to enable indexing Django models with Elastic, and our application indeed has Django models even though they aren’t mapped to relational tables but to MongoDB documents. The Django package that we will use is Django Elasticsearch for connecting ES to the Django application. The authors of the package describe it like a thin wrapper around another package: a part of the official Elasticsearch Python client. This means that we are using code from official packages and we will be able to create queries in a simple and idiomatic way.
Importing a dataset (articles from BBC, obtained on Kaggle) into a MongoDB collection
The dataset that will be used throughout this project is a set of articles by BBC. The dataset is rather old (2004-2005) but it is small enough (a little over 2,000 articles), and the articles have a simple structure that we will further simplify. After downloading the ZIP file, extract and save the articles_bbc.csv (comma separated values file) into the root of the project, alongside the virtual environment venv folder. You can use the file provided in this project's GitHub repository.
After firing up MongoDB Compass, select the database and collection articles and import the data from the CSV file. Your collection should now be populated with 2,225 article documents, each having a very basic structure: an ObjectID, a category, the title, and the actual content.
Creating the models for the application and the documents.py file for the DSL package
The first thing to do when working with the package is to add it to the INSTALLED_APPS in settings.py:
INSTALLED_APPS = [ "articles.apps.ArticlesConfig", "elasticdjango.apps.MongoAdminConfig", "elasticdjango.apps.MongoAuthConfig", "elasticdjango.apps.MongoContentTypesConfig", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django_elasticsearch_dsl", ]
It is time to now configure the Django Elasticsearch DSL package, and we will not stray from the official documentation. The basic principle is simple: All you need to do is provide a documents.py file inside the Django application and register all the types of documents that need to be indexed by Elastic.
Let’s begin populating the documents.py file and address the fields that need to be treated differently later—we are talking about the ObjectID field:
# documents.py from django_elasticsearch_dsl import Document, fields from django_elasticsearch_dsl.registries import registry from .models import Article from bson import ObjectId @registry.register_document class ArticleDocument(Document): # Explicitly define the id field to ensure it's treated as text id = fields.TextField() class Index: name = "articles" settings = {"number_of_shards": 1, "number_of_replicas": 0} class Django: model = Article fields = [ "title", "category", "content", ]
In this part of the documents.py file, we have defined which model we want Elasticsearch to index and specified the fields. We have, however, also told Elasticsearch that the id field will be of type text. The imports that we need are the following:
Document: the base class for defining the Django to Elasticsearch mapping
fields: corresponds to the Elasticsearch field types (TextField, IntegerField, and so on)
registry: the package’s decorator system that enables automatic syncing of the model (the Django article class, corresponding to the MongoDB document) to the Elasticsearch document
Finally, we imported the article model from Django and the ObjectId from the BSON package since we will need to handle the ObjectId.
The @registry.register document decorator registers the document with the signal system of the package and allows automatic updating in Elasticsearch each time an article object is saved or deleted.
After defining explicitly the id of the Elasticsearch document as a string (a TextField), the index is named (“articles”), along with some settings (1 shard, 0 replicas, in our case). Finally, the Django class links the document to the article model (in Django, hence the name) and allows synchronization.
For handling MongoDB's ObjectID field (the primary key), the Django Elasticsearch DSL package provides us with the prepare_field and prepare methods. As stated on the package's website:
Sometimes, you need to do some extra prepping before a field should be saved to Elasticsearch. You can add a prepare_foo(self, instance) method to a document (where foo is the name of the field), and that will be called when the field needs to be saved.
Let’s add the code for converting the ObjectID to a string:
def prepare_id(self, instance): """Convert ObjectId to string for the id field""" return str(instance.id) def get_queryset(self): """Return the queryset for indexing""" return self.Django.model.objects.all() def prepare(self, instance): """Override prepare to handle ObjectId conversion""" data = super().prepare(instance) # Ensure all ObjectId fields are converted to strings for key, value in data.items(): if isinstance(value, ObjectId): data[key] = str(value) return data
The documentation on the fields contains this useful bit of information as well:
The elasticsearch document id (id) is not strictly speaking a field, as it is not part of the document itself. The default behavior of django_elasticsearch_dsl is to use the primary key of the model as the document’s id (pk or id). Nevertheless, it can sometimes be useful to change this default behavior. For this, one can redefine the generate_id(cls, instance) class method of the _document class.
Well, that is exactly what we are doing in our code—we are preparing the id field by converting the ObjectID into a string value (in the prepare_id method, which is called each time the id field is being processed).
The get_queryset method is used for defining which set of object will be indexed in bulk operations. This can be further filtered using the Django filtering notation.
Finally, the prepare method is responsible for preparing the general data for Elasticsearch. In our case, it is used to check whether some ObjectId managed to slip through and convert all of them into strings.
There are just two more methods that have to be added in order to make the integration work:
def _prepare_action(self, object_instance, action): """Override to ensure ObjectId is converted to string in action metadata""" # Get the document data doc = self.prepare(object_instance) # Convert ObjectId to string for document ID doc_id = str(object_instance.pk) # Return the action with converted ID return { "_op_type": action, "_index": self._index._name, "_id": doc_id, "_source": doc, }
The prepare_action is used for handling the low-level Elasticsearch bulk actions. In our case, it ensures the ids are strings.
Finally, for retrieving documents from Elasticsearch and serializing them, we will add a to_dict method:
def to_dict(self, include_meta=False, skip_empty=True): """Override to_dict to handle ObjectId in meta""" data = super().to_dict(include_meta=False, skip_empty=skip_empty) if include_meta: meta_dict = {} if hasattr(self.meta, "id") and self.meta.id is not None: # Convert ObjectId to string in meta meta_dict["_id"] = str(self.meta.id) if hasattr(self.meta, "index"): meta_dict["_index"] = self.meta.index data["_meta"] = meta_dict return data
Connecting the ElasticSearch server
The last step is to connect our Elasticsearch server to the Django application. This is achieved through the settings file, so in a very djangonic way, to misquote the favorite Python adjective.
Open the settings.py file, and at the bottom, add this line:
ELASTICSEARCH_DSL = {"default": {"hosts": "http://localhost:9200"}}
The documents.py is the heart of the Elastic-Django integration. The Django project is aware of the Elastic server, and now, we are ready to index the data. For that purpose, the package provides handy management commands that allow creating, populating, and deleting Elasticsearch indices. Just like with any other standard Django management command, we can now create and populate the indices with:
python manage.py search_index --create python manage.py search_index --populate
Deleting indices is just as simple.
Creating the views
Now that we have Elasticsearch up and running, the articles in the MongoDB collection, and the admin application working, we can begin working on the fun part: creating some views and templates to showcase the searching functionality.
Let's begin with a simple view that will only display the first 10 articles, sorted alphabetically. Note that we haven't bothered with removing some hyphens and special characters, so the articles in this view will be those that begin with quotes and similar, but this is not important at this point. Create a file articles/views.py and begin coding:
from django.http import Http404 from django.shortcuts import get_object_or_404, render, redirect from .models import Article from bson import ObjectId from bson.errors import InvalidId def index(request): articles = Article.objects.order_by("title")[:10] return render(request, "articles/index.html", {"articles": articles})
Now, let's immediately create a view for viewing a single article by its ObjectId:
def detail(request, article_id): try: object_id = ObjectId(article_id) except InvalidId: raise Http404(f"Invalid article ID format: {article_id}") # Get the article or return 404 article = get_object_or_404(Article, id=object_id) # Create context with data context = {"article": article} return render(request, "articles/article_detail.html", context)
This view showcases the MongoDB version of the standard Django query by Id. We must ensure two things: that the article_id passed in the URL can be converted to a valid ObjectId and that said ObjectId exists in the collection.
For the templates, we will use PicoCSS, a minimalistic CSS framework for creating nicely styled web pages.
Create a folder, /articles/templates/articles, and inside of it, a file named base.html:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="color-scheme" content="light dark"> <!-- Pico CSS from CDN --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> <title>{% block title %}Django MongoDB Backend with ElasticSearch{% endblock %}</title> </head> <body> <!-- Header --> <header class="container"> <nav> <ul> <li><a href="{% url 'index' %}" class="contrast"><strong>Django MongoDB Backend with ElasticSearch</strong></a></li> </ul> <ul> <li><a href="/" class="contrast">Home</a></li> <li><a href="{% url 'test_search' %}" class="contrast">Search</a></li> </ul> </nav> </header> <!-- Main Content --> <main class="container"> {% block content %} <p>Base template</p> {% endblock %} </main> <!-- Footer --> <footer class="container"> <hr> <p><small>Django MongoDB BAckend with Elastic Search, 2025</small></p> </footer> </body> </html>
The templating is identical to the process used with classic Django. We defined a simple navigation, a footer, a title, and some space to put the actual content in the page. Let's create the templates for the two views defined so far. Create a file, /articles/templates/index.html:
{% extends 'articles/base.html' %} {% block title %}Articles - My Django App {% endblock %} {% block content %} <section> <article> <header style="display: flex; justify-content: space-between; align-items: center;"> <div> <h1>Articles</h1> {% if total_count %} <p>{{ total_count }} article{{ total_count|pluralize }} available</p> {% endif %} </div> <div> <a href="{% url 'add_article' %}" role="button">+ Add New Article</a> </div> </header> </article> </section> <!-- Display messages --> {% if messages %} {% for message in messages %} <div class="message-{{ message.tags }}" style=" padding: 1rem; margin: 1rem 0; border-radius: 0.5rem; {% if message.tags == 'success' %} background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; {% elif message.tags == 'error' %} background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; {% endif %} "> {{ message }} </div> {% endfor %} {% endif %} <!-- Articles List --> <section> {% if articles %} {% for article in articles %} <article> <header> <h3><a href="{% url 'detail' article.id %}">{{ article.title }}</a></h3> <p> <small> <mark>{{ article.category }}</mark> • ID: {{ article.id }} </small> </p> </header> <!-- Show content preview --> {% if article.content %} <p>{{ article.content|truncatewords:20 }}</p> {% endif %} <footer> <a href="{% url 'detail' article.id %}" role="button" class="secondary">Read More</a> </footer> </article> {% endfor %} {% else %} <article> <header> <h2>No Articles Yet</h2> </header> <p>Start by creating your first article!</p> <footer> <a href="{% url 'add_article' %}" role="button">Create Your First Article</a> </footer> </article> {% endif %} </section> {% endblock %}
The template is quite simple. It displays the articles and adds some quick button actions. Let's create the details page now. Create a new template, /articles/templates/articles/article_details.html:
{% extends 'articles/base.html' %} {% block title %}{{ article.title }}{% endblock %} {% block content %} <!-- Article Header --> <section> <article> <header> <h1>{{ article.title }}</h1> <p> <small> <mark>{{ article.category }}</mark> </small> </p> </header> </article> </section> <!-- Article Content --> <section> <article> <div> {{ article.content|linebreaks }} </div> </article> </section> <!-- Navigation --> <section> <article> <footer> <a href="{% url 'index' %}" role="button" class="secondary">← Back to Articles</a> </footer> </article> </section> {% endblock %}
Again, nothing particularly exciting here—just plain old Django templating for a single item.
Currently, the two URLs should look as they do in the following screenshots (attached images).
Now, we can go and create the heart of the system, the actual Elasticsearch view. Open the views.py file and edit it:
from django.http import Http404 from django.shortcuts import get_object_or_404, render, redirect from elasticsearch import Elasticsearch from django.conf import settings from django.contrib import messages from .forms import ArticleForm from .models import Article from bson import ObjectId from bson.errors import InvalidId # previous views remain unchanged... def test_search(request): """ Search view with form that accepts user query """ # Get search query from form query = request.GET.get("q", "").strip() # Connect to Elasticsearch es_config = settings.ELASTICSEARCH_DSL["default"] es = Elasticsearch([es_config["hosts"]]) results = [] total_hits = 0 if query: # Only search if query is provided try: # Multi-match search with title weighted 3 times higher than content response = es.search( index="articles", body={ "query": { "multi_match": { "query": query, "fields": ["title^3", "content"], # Title has 3x weight "type": "best_fields", "fuzziness": "AUTO", } }, "size": 10, }, ) # Extract results for hit in response["hits"]["hits"]: results.append( { "id": hit["_id"], "title": hit["_source"]["title"], "category": hit["_source"]["category"], "content": hit["_source"]["content"][:150] + "...", "score": hit["_score"], } ) total_hits = response["hits"]["total"]["value"] except Exception as e: context = {"error": str(e), "query": query} return render(request, "articles/test_search.html", context) context = {"results": results, "total_hits": total_hits, "query": query} return render(request, "articles/test_search.html", context)
The search functionality doesn't differ all that much from a standard Django query or a PyMongo find operation. After extracting the query string from the GET request (we are making a get request to simplify things—in reality, you would probably want to use a POST request), the steps are the following:
The view reads the Elasticsearch server setting from the settings file (in our case, the key is default).
We initialize an empty list with the results.
Provided the query exists, we perform an Elasticsearch query on two fields: the title and the content.
The title is three times more relevant than the content—that's what the ^3 stands for. If we were to omit it, they would be treated with equal relevance.
Fuzzines set to AUTO enables Elasticsearch to perform fuzzy searches.
The size is set to 10, so we get 10 results.
Finally, we parse the results and create a list of dictionaries, each containing the _id, the article data, and the relevance score.
The views.py file contains another view, for creating articles, but we will not delve into that as it has nothing Elasticsearch-specific. The view for adding articles is useful to play around with in case you want to test how new articles fare against specific queries.
Now, let's add the template for searching! Create a template, /articles/templates/articles/test_search.html:
{% extends 'articles/base.html' %} {% block title %}Search Articles{% endblock %} {% block content %} <section> <article> <header> <h1>Search Articles</h1> </header> <!-- Search Form --> <form method="get"> <fieldset role="group"> <input type="search" name="q" placeholder="Search articles..." value="{{ query }}" required autocomplete="off"> <input type="submit" value="Search"> </fieldset> </form> </article> </section> <!-- Search Results --> {% if query %} <section> <article> <header> {% if error %} <p style="color: red;"><strong>Error:</strong> {{ error }}</p> {% else %} <h2>Search Results</h2> <p>Found <strong>{{ total_hits }}</strong> result{{ total_hits|pluralize }} for "{{ query }}"</p> {% if total_hits > 0 %} <small>Results sorted by relevance (title matches weighted 3x higher)</small> {% endif %} {% endif %} </header> {% if results %} {% for result in results %} <article> <header> <h3>{{ result.title }}</h3> <p> <small> <mark>{{ result.category }}</mark> • Relevance Score: <mark>{{ result.score|floatformat:2 }}</mark> </small> </p> </header> <p>{{ result.content }}</p> <footer> <a href="{% url 'detail' result.id %}" role="button" class="secondary">Read Full Article</a> </footer> </article> {% endfor %} {% elif not error %} <p>No articles found matching "{{ query }}". Try different keywords.</p> {% endif %} </article> </section> {% endif %} {% endblock %}
The search functionality is now complete! You can test it out, bearing in mind the source and the period of the articles, and try tweaking the search settings.
The code in the GitHub repo contains another view for creating new articles, as well as the screenshots generated from the app.
Conclusion
The combination of Elasticsearch, MongoDB, and Django creates an exceptionally powerful stack for modern web applications that need to handle complex data operations at scale. While MongoDB serves as a flexible, document-oriented, NoSQL database that excels at storing unstructured and semi-structured data without the rigid schema constraints of traditional relational databases, Elasticsearch complements this by providing lightning-fast search and analytics capabilities. In this setup, Django acts as the perfect web framework and orchestrator between these layers, offering robust ORM-like functionality for MongoDB through specialized back ends while simultaneously leveraging Elasticsearch's distributed search engine for real-time queries, full-text search, and complex aggregations.
This integration is particularly valuable because it addresses the common performance bottlenecks found in traditional Django applications—where relational databases often struggle with complex searches across large datasets—by offloading search operations to Elasticsearch while maintaining MongoDB's advantages in handling diverse data types, horizontal scaling, and rapid development cycles.
The result is an architecture that can efficiently serve both operational data storage needs and sophisticated search requirements, enabling applications to deliver fast, responsive user experiences even when dealing with millions of documents and complex query patterns that would be prohibitively slow in traditional SQL-based setups.
Top comments (0)