DEV Community

Cover image for Rapid AI-powered applications with Django MongoDB and Voyage API
MongoDB Guests for MongoDB

Posted on

6 1 1 1 1

Rapid AI-powered applications with Django MongoDB and Voyage API

This article is written by Marko Aleksendric (Data Analyst)

In the world of Python web development, Django is considered a powerhouse. It is one of the oldest, yet most actively developed web frameworks with a vibrant developer community. Technically speaking, Django is a third-party Python framework for creating web applications that run on a server.

Django has its way of doing things—through a paradigm called MVT (Model – View – Template)—and once you get used to it, it becomes very intuitive and fast. A Django web application is typically contained in a project that is further structured into what are called Django Apps—essentially, Python modules responsible for different types of common functionality. For simple projects, we can even have only one app.

Each app is responsible for its own group of URLs that are mapped to views—a conveniently named views.py file, which contains what is in other frameworks known as the controllers—a set of functions and, recently, classes. Based on the requested URL, these provide the application logic and interact with a database. These databases have traditionally been relational, but that is about to change!

Django owes its popularity and longevity to numerous factors: As long as we adhere to the Django way of doing things, it allows us to quickly build robust and secure web applications of varying degrees of complexity. Out of the box, Django provides some features that developers have loved for decades: an admin site that we get practically for free (with three simple lines of code and without any additional work), a simple yet powerful HTML templating language, and a complex yet simple to use ORM (object relational mapper) for building powerful queries.

During the past decade, with the rise in popularity of single-page applications and various frontend frameworks and libraries (such as React, Next.js, Vue.js, and Svelte), Django is being used as a backend solution by making it output JSON data instead of HTML pages, through the Django REST Framework, Django-Ninja, or by simply setting the views to output valid JSON.

It is worth mentioning that Django isn’t a web server. In fact, it requires one to serve its content. However, the “batteries included” principle implies that there is a development server that is more than enough for development. The “batteries included” moniker means also that Django itself provides everything that is needed to build a performant web application of high complexity—from the database models, to the business logic, the templating engine, the routing system, and hundreds of plugins for every imaginable purpose.

Finally, the vast Django ecosystem provides thousands of third-party integrations for virtually every problem enough developers have encountered during the past two decades.

Django has a great website with a legendary tutorial that is often updated and always matches the latest version.

OPTIONAL: When it comes to books, the author of this article has found William S. Vincent's books to be the fastest when you need to dive into Django. Michael Dinder’s book, Becoming an Enterprise Django Developer, is a great place to learn many Django-specific best practices.

A needed integration

The idea of integrating MongoDB, the most popular and powerful document-based database and the most popular Python web framework, is not new. There have been attempts in the past years, and even some pretty long-lived projects that had, however, limited usability.

The Django MongoDB Backend that we are going to use in a simple project is the result of the MongoDB team listening to the needs of the Django community. As listed in the announcement, the DMB project provides a much deeper and thorough integration approach, with the aim to provide the use of Django models (the backbone of Django projects), and to have a fully functional and customizable Django admin. It also offers native connection handling by the settings.py file, a special file in the heart of every Django project that is orchestrating the database, the applications, plugins, URL mappings, and so on. The provided project and application templates ensure that everything is configured correctly, but nevertheless, it is important to know exactly what is going on under the hood.

The Django MongoDB Backend is an official integration from MongoDB released in February 2025 as a public preview, providing models, migrations, and the admin panel and making it easy for Django developers to adopt MongoDB.

On the other hand, the DMB project allows developers to use the unleashed power of MongoDB aggregations, vector search, embedded documents, and virtually anything achievable through the official PyMongo driver. The project is not yet recommended for production use, but the roadmap includes support for MongoDB’s document model, encryption features, and native support for Atlas Search and Vector Search. Integrating MongoDB’s Vector Search with Django allows for quick and efficient development of AI-powered applications as we will see.

The project: a smart recipe application

To showcase the Django MongoDB Backend, we will create a simple project—a web application that will store recipes (the ingredients and the instructions)—and provide a couple of ways to search through them and get recommendations based on the current contents of your fridge. We will use some scraped recipes and try to showcase the speed and power of the backend through a simple application.

This is also a great opportunity to mention that MongoDb recently announced the acquisition of Voyage AI—a leader in embedding and re-ranking models.

AI-powered search and retrieval—particularly embedding generation and reranking—can extract relevant contextual data to ground AI outputs and ensure accuracy, as seen in popular paradigms such as retrieval augmented generation (RAG). And in this project, we will build a sort-of-kind-of RAG-like system.

MongoDB believes these capabilities belong in the database layer to simplify application stacks and create a more reliable foundation for AI systems. Their acquisition of Voyage AI, a leader in embedding and reranking models, aims to integrate these capabilities directly into database systems, helping businesses mitigate hallucinations, improve trustworthiness, and establish a flexible, intelligent data foundation that redefines databases for the AI era.

Voyage AI has developed state-of-the-art embedding models that significantly outperform competitors in precision and recall benchmarks related to information retrieval.

This project will help you get a taste of each of these technologies: Django for the creation of the web app, Django MongoDB Backend for the integration of MongoDB, PyMongo for direct interaction with MongoDB, Voyage AI for the embeddings necessary for the vector representation of recipe ingredients, and Anthropic’s powerful Claude LLM for the creation of the suggestions.

Prerequisites

This project will require a couple of resources:

  • An IDE (integrated development environment)—in this case, we will use Visual Studio Code
  • Python 3.10 or later—we will be using version 3.12.6
  • The Atlas CLI (command line interface)
  • Docker
  • A local Atlas deployment
  • A Voyage API key (link) for generating the embeddings
  • An Anthropic API key for the LLM

Create your local Atlas deployment

In this project, we will leverage the Atlas CLI to create a local Atlas deployment
Once you have the prerequisites installed, you can create a deployment, type in the terminal
atlas deployments setup, and select Local Database deployment.

The terminal will output something similar to the following:

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"
Enter fullscreen mode Exit fullscreen mode

Copy this connection string, as we will use it throughout our project.

Note: Keep in mind that the connection string can point to an online Atlas instance with no modifications and Docker isn't necessary for this tutorial, but was a personal choice.

Create a Python virtual environment

Every Python project begins with a new environment as it allows you to keep the packages under control and avoid collisions with other projects.

To create the environment venv, in your terminal, run:

python -m venv venv
Enter fullscreen mode Exit fullscreen mode

Then, proceed to activate it:

source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Installing the dependencies

Now, we can create a project folder (usually the one that contains the /venv virtual environment) and install the dependencies from a requirements.txt file. Create a requirements.txt file and populate it with the necessary packages if you want to be able to reproduce this environment exactly:

anthropic==0.47.2
django-mongodb-backend==5.1.0b0
python-dotenv==1.0.1
voyageai==0.3.2
Enter fullscreen mode Exit fullscreen mode

Anthropic provides us with the LLM model (the Claude family), python-dotenv is used for securely managing secret environment variables such as connection strings and API keys, and Voyage will provide the powerful embeddings from… Voyage AI.

Install the requirements:

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

And you should be ready to roll!

Creating a Django MongoDB Backend project

It is finally time to create our project leveraging the nifty project template provided by the DMB team. We will effectively use the django-create-project command and its app nomenclature, but with a couple of twists. The template ensures that we get the MongoDB-specific migrations, and the ObjectId as the primary key in the models.

Let’s name the project cookbook and use the django-admin command (the standard way of beginning Django projects) using the provided template. The use of the template is mandatory as it contains the necessary modifications that enable the backend to bridge Django and MongoDB:

django-admin startproject cookbook --template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/5.0.x.zip
Enter fullscreen mode Exit fullscreen mode

We will not go over the project structure, as this is examined in detail in the quickstart guide.

What is of paramount importance at this point is the fact that, while the default Django project provides an SQLite database for prototyping, with the DMB project, we must provide the MongoDB connection string in the cookbook/settings.py file (around line 79):

DATABASES = {
    "default": django_mongodb_backend.parse_uri(
        "mongodb://localhost:12404/cookbook?directConnection=true"
    ),
}
Enter fullscreen mode Exit fullscreen mode

Do not forget to add the database name cookbook after the host. Otherwise, Django Mongo Backend will not be able to know which database to create and where to put the collections! The documentation shows that there are more ways of specifying the database and collection.

This is the right time to spin up the development server and check that everything works. Enter into the project subdirectory (cookbook):

cd cookbook
Enter fullscreen mode Exit fullscreen mode

And run the command:

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

You should be greeted with a message saying that the server is running and that there are unapplied migrations. That is good!

The initial screen after a successful creation of a project

Creating our first (and only) Django application

To create our application, we will again use another convenient template provided by the Django-MongoDB team, making sure to name the app recipes. This template sets the newly created Django app compatible with the Backend.

python manage.py startapp recipes --template https://github.com/mongodb-labs/django-mongodb-app/archive/refs/heads/5.0.x.zip
Enter fullscreen mode Exit fullscreen mode

It is worth remembering that this application template sets the default auto field to a MongoDB ObjectID, instead of Django’s original BigAutoField for model IDs—something that you can verify by inspecting the recipes/app.py file.

After the app is created with the usual Django structure, we can proceed and populate the models.py file that will define the structure of our data.

Before doing that, let us examine a JSON representation of the document that we want to be able to store and query. The recipe structure that we are going to use in this application is the following:

{
        "title": "Irish Soda Bread",
        "ingredients": [
            "1/2 cup white sugar",
            "4 cups all-purpose flour",
            "2 teaspoons baking powder",
            "1 teaspoon baking soda",
            "3/4 teaspoon salt",
            "3 cups raisins",
            "1 tablespoon caraway seeds",
            "2 eggs, lightly beaten",
            "1 1/4 cups buttermilk",
            "1 cup sour cream"
        ],
        "instructions": "Preheat oven to 350 degrees F (175 degrees C). Grease a 9 inch round cast iron skillet or a 9 inch round baking or cake pan.\nIn a mixing bowl, combine flour (reserving 1 tablespoon), sugar, baking powder, baking soda, salt, raisins and caraway seeds. In a small bowl, blend eggs, buttermilk and sour cream. Stir the liquid mixture into flour mixture just until flour is moistened. Knead dough in bowl about 10 to 12 strokes. Dough will be sticky. Place the dough in the prepared skillet or pan and pat down. Cut a 4x3/4 inch deep slit in the top of the bread. Dust with reserved flour\nBake in a preheated 350 degrees F (175 degrees C) oven for 65 to 75 minutes. Let cool and turn bread onto a wire rack.\n",
        "embedding_ingredients": "white sugar, all-purpose flour, baking powder, baking soda, alt, raisins, caraway seeds, eggs, lightly beaten, buttermilk, our cream",
        "features": {
            "cuisine": "Irish",
            "preparation_time": "medium",
            "complexity": "low",
            "prep_time": 25
        }
    }
Enter fullscreen mode Exit fullscreen mode

This is adapted from an online recipe repository in JSON format. The provided JSON is just a small subset of the data available on the original site, and the keys embedding_ingredients and features are added for educational purposes. You will find it in the application repo as bigger_sample.json. In a real-world system, the recipes would get parsed and the features would either be provided or they would get added at insertion time. The same goes for the embeddings. Once a user inserts a recipe in the application, the embeddings would be generated automatically and the recipe would be saved. In this example, however, we are working with a pre-built dataset in JSON format, so we are importing it into MongoDB (along with some additional data generated by an LLM) and adding the Voyage AI embeddings later.

The extraction of the ingredients doesn’t have much to do with the Django MongoDB Backend itself, and the features dictionary is added somewhat arbitrarily for convenience, but also so we can showcase some MongoDB querying on embedded fields.

Creating the models of the application

Let’s try to model this data structure with Django. Open the models.py file in the recipes folder and add the following:

from django.db import models
from django_mongodb_backend.fields import ArrayField, EmbeddedModelField
from django_mongodb_backend.models import EmbeddedModel
from django_mongodb_backend.managers import MongoManager

class Features(EmbeddedModel):
    preparation_time = models.CharField(max_length=100)
    complexity = models.CharField(max_length=100)
    prep_time = models.IntegerField()
    cuisine = models.CharField(max_length=100, null=True, blank=True)

class Recipe(models.Model):
    title = models.CharField(max_length=200)
    instructions = models.TextField(blank=True)
    features = EmbeddedModelField(Features, null=True, blank=True)
    ingredients = ArrayField(models.CharField(max_length=100), null=True, blank=True)
    embedding_ingredients = models.CharField(max_length=500, null=True, blank=True)
    voyage_embedding = models.JSONField(null=True, blank=True)

    objects = MongoManager()

    class Meta:
        db_table = "recipes"
        managed = False

    def __str__(self):
        return f"Recipe {self.title}"
Enter fullscreen mode Exit fullscreen mode

There are several moving parts in the models.py file that need a bit of explanation. As documented on the Django MongoDB Backend website, the Backend supports both Django and MongoDB fields.

In our recipe model, we are using the ArrayField for representing the array of floats that will be generated later from the Voyage AI embedding model. The Features is an EmbeddedModel that will not get its own collection (since it is, well, embedded), leveraging MongoDB best practices.

Also, just the Recipe model gets its own Manager and a db_table in the Meta of the class. We will indeed have just one collection, Recipes, in which each document will have an embedded Features document.

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 = [
    "recipes.apps.RecipesConfig",
    "cookbook.apps.MongoAdminConfig",
    "cookbook.apps.MongoAuthConfig",
    "cookbook.apps.MongoContentTypesConfig",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
Enter fullscreen mode Exit fullscreen mode

Finally, let’s hook up the admin site. Even though many developers won’t admit it, it is one of the strongest selling points of the framework. Open the admin.py file inside the cookbook/recipes folder and edit it:

from django.contrib import admin
from .models import Recipe

admin.site.register(Recipe)
Enter fullscreen mode Exit fullscreen mode

In order to be able to inspect the admin site, we need to do two more things: apply the migrations and create a superuser with unlimited power.

Stop the development server if you left it running and use the powerful manage.py file again:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

You should now be able to see the database and the collections created in your Atlas instance if you are peeking into it with MongoDB Compass or the MongoDB VSC Extension for instance.

The recipes collection as seen in MongoDB Compass

Creating basic views and templates

In Django development, the workflow typically follows a cyclical pattern centered around the Model-View-Template (MVT) paradigm. We begin by defining models in the application models.py, which represent collections and their relationships, followed by migrations to create or update the schemas.

Next, we develop views in views.py to handle user requests and implement business logic. These views determine which data to retrieve from models and which templates to render. Templates are then created, containing HTML with the Django template language (very similar to Jinja if you have worked with Flask or FastAPI) to display dynamic data passed from views.

Once these components are implemented, URLs are configured in urls.py to route requests to appropriate views. This cycle continues throughout development, with developers leveraging Django's admin interface for quick data manipulation and management. The workflow embraces Django's DRY/"don't repeat yourself" philosophy, allowing for rapid development and clean separation of concerns.

We will not delve much into Django’s templating language. It is very straightforward and intuitive. But we will make a base template from which the other templates will inherit (extend it, in Django terms) and we will use CDN—delivered Tailwind CSS for simplicity and to make the application not the ugliest thing on the web.

Create a folder called templates in the /recipes folder and inside it, create a file base.html:

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block title %}Django MongoDB Backend Recipes{% endblock %}</title>

    <script src="https://unpkg.com/@tailwindcss/browser@4"></script>
    {% block extra_css %}{% endblock %}
</head>

<body class="flex flex-col min-h-screen bg-gray-50">
    <!-- Main Content -->
    <main class="container mx-auto px-4 py-6 flex-grow">
        {% block content %}
        <h1 class="text-3xl font-bold underline">Hello world!</h1>
        {% endblock %}
    </main>

    <!-- Footer -->
    <footer class="bg-gray-800 text-white py-6">
        <div class="container mx-auto px-4">
            <div class="flex flex-col md:flex-row justify-between items-center">
                <div class="mb-4 md:mb-0">
                    <p>&copy; {% now "Y" %} Django + MongoDB <3 </p>
                </div>
            </div>
        </div>
    </footer>

</body>

</html>
Enter fullscreen mode Exit fullscreen mode

The base.html template will be displayed on every page and in that regard, it is similar to the layouts in Next.js or SveltKit, if you are coming from the JS world. The pages that we will create are the following:

  • A generic home page
  • A page that lists the top recipes alphabetically—a typical list view
  • A detail page for each recipe
  • A search page for querying the recipes collection with Atlas fuzzy search
  • A search page for performing vector search and displaying the results
  • An AI-powered suggestion page that provides recipes based on the available ingredients

Now, let’s create a simple, bare-bones index.html page that will contain nothing more than a header, in the same level where the base template resides:

{% extends 'base.html' %}

{% block content %}
    <h1 class="text-3xl font-bold text-center">{{ message }}</h1>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

The template(s) are ready, but now we need a view that will actually point the URL to it. Open the /cookbook/views.py and add the following:

from django.shortcuts import render

def index(request):
    return render(request, "index.html", {"message": "Recipes App"})
Enter fullscreen mode Exit fullscreen mode

The render function is one of the most frequent ways for rendering templates. It accepts an HTTP request, the name of the template, and the context containing data to be displayed, in the form of a dictionary. In this case, we just want to render a message containing the text “Recipes App,” but later, it will be arrays of recipes, dictionaries, and other rich structures.

We are all eager to try this out, but before that, we need to create a urls.py file in the cookbook/views.pyfile:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
]
Enter fullscreen mode Exit fullscreen mode

And this file just manages our sole view, mapping it to the “/” url (the root of the website).

Lastly, let’s make Django aware of our urls.py by hooking it up in the project-wide urls.py file: (cookbook/urls.py):

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

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("recipes.urls")),
]
Enter fullscreen mode Exit fullscreen mode

Congratulations! You have just completed the Django cycle—a view, a template, a urls.py edit!

Now, we can spin up the dev server with:

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

And you should see our minimal home page.

The bare-bones home page of the Recipes App

Importing recipe data

Although we are able to insert recipes through the admin panel, that would be no fun at this point. Instead, we will insert an array of recipes in bulk, through an external script. If you remember, we included the python-dotenv package in our requirements.txt file. We will use it to manage the API keys for the Voyage AI and Claude services, but we can also use it to keep track of our MongoDB URI and the database and collections(s) names.

In the following section, you will import the initial recipes data (without the embeddings, as we want to separate this part to emphasize it!) into MongoDB.

Create an .env file at the root of the project (same level as the /venv directory) and insert the following:

MONGO_URI=mongodb://localhost:12404/?directConnection=true
MONGO_DB=cookbook
MONGO_COLLECTION=recipes
Enter fullscreen mode Exit fullscreen mode

Now, we will use the provided recipes_sample.json file and build a simple script for importing our data. Create at the project level a script named import_json_recipes.py and insert the following:

import os
import pymongo
import json
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# Get MongoDB connection URI and database name from environment variables
# Falls back to default values if not set
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:12404/?directConnection=true")
MONGO_DB = os.getenv("MONGO_DB", "cookbook")

try:
    # Establish connection to MongoDB
    client = pymongo.MongoClient(MONGO_URI)
    db = client[MONGO_DB]
    print(db)

    # Open and load the JSON file containing recipe data
    with open("bigger_sample.json", "r") as f:
        recipes_data = json.load(f)

    # Iterate through each recipe in the loaded data
    for recipe in recipes_data:
        try:
            # Create a document with selected fields from the recipe
            recipe_doc = {
                "title": recipe["title"],
                "ingredients": recipe["ingredients"],
                "instructions": recipe["instructions"],
                "embedding_ingredients": recipe["embedding_ingredients"],
                "features": recipe["features"],
            }
            print(recipe_doc)

            # Insert the recipe document into the 'recipes' collection
            db.recipes.insert_one(recipe_doc)
        except Exception as e:
            # Handle errors for individual recipe insertion
            print(f"Error inserting recipe: {e}")

    # Print success message with count of inserted recipes
    print(f"Inserted {len(recipes_data)} recipes into MongoDB")

except pymongo.errors.ConnectionFailure as e:
    # Handle MongoDB connection failures
    print(f"Error connecting to MongoDB: {e}")
except Exception as e:
    # Handle any other exceptions during the import process
    print(f"Error inserting recipes: {e}")
Enter fullscreen mode Exit fullscreen mode

Place the recipes_sample.json in the same directory and start the script:

python import_json_recipes.py
Enter fullscreen mode Exit fullscreen mode

After a few seconds, your collection should be populated with roughly 200 recipes—not too much, but enough for tinkering and getting a feel of the data structure and searching and aggregation capabilities. But before starting to play with the data, let’s add the final part of the puzzle: the Voyage AI embeddings!

The recipes collection without embeddings

Generating Voyage AI embeddings

Vector databases are specialized systems built to handle vectorized data—those scary-looking, high-dimensional arrays of numbers that AI models generate in order to be able to talk to each other in the only language they truly understand: numbers and vectors.

You can think of these as special databases that are really good at working with vector embeddings (the numerical representations that capture the semantic and real meaning of text, images, etc.). What's great about MongoDB is that it lets us store these embeddings right alongside our actual data, which makes retrieving documents much easier, whether we're grabbing them directly or through simple relationships when documents are chunked up. In RAG (retrieval-augmented generation) systems, chunking involves splitting documents into smaller, more manageable segments of text before creating embeddings for them, which enables more precise retrieval of relevant context. These chunks strike a balance between being small enough to provide focused information yet large enough to preserve contextual meaning, thus improving the relevance of retrieved content when responding to user queries.

But here's the thing. Embedding models are very important in this whole setup! They are what transform our messy, complex data into those neat vector embeddings that databases can work with. These models are key to capturing how different pieces of data relate to each other semantically, which powers advanced search and analysis. When you use the same embedding model for both storing your data and processing queries, your vector database can effectively find similar content based on meaning, not just exact matches. This is what makes things like personalized recommendations and semantic search possible. Bringing these embedding models together with vector databases just makes building and scaling AI applications much more straightforward, and MongoDB allows us to do that.

Before starting to write views and templates to display our recipes, we will create another script for generating embeddings with the Voyage AI API. Visit the Voyage AI website and create an API key if you haven’t already, and store it into the .env file that you created.

Take some time to familiarize yourself with the Voyage AI Python API docs and you will find it is simple and user-friendly.

Since we will be using the free version (no credit card!), the script will contain a call to the Python’s time.sleep() function to introduce some delay, so that we stay within the API rate limits. Create a script named generate_embeddings.py in the root of the project (same level as the venv folder):

import os
import time

import pymongo
import voyageai
from dotenv import load_dotenv
from tqdm import tqdm

load_dotenv()

MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:12404/?directConnection=true")
MONGO_DB = os.getenv("MONGO_DB", "cookbook")


# MongoDB connection
try:
    # Connect to MongoDB
    client = pymongo.MongoClient(MONGO_URI)
    db = client[MONGO_DB]
    collection = db["recipes"]

    # Initialize Voyage AI client
    # This automatically uses the VOYAGE_API_KEY environment variable
    vo = voyageai.Client()

    # Find documents without voyage_embedding
    query = {"voyage_embedding": {"$exists": False}}
    documents_without_embeddings = list(collection.find(query))

    print(
        f"Found {len(documents_without_embeddings)} documents without Voyage embeddings"
    )

    # Add embeddings to documents that don't have them
    for doc in tqdm(documents_without_embeddings, desc="Adding embeddings"):
        # Prepare content for embedding - just the title and the ingredients
        content_to_embed = (
            f"{doc['title']}. Ingredients: {doc['embedding_ingredients']}"
        )

        # Get embedding from Voyage AI
        try:
            embedding_result = vo.embed(
                [content_to_embed],
                model="voyage-lite-01-instruct",
                input_type="document",
            )
            embedding = embedding_result.embeddings[0]

            # Update document with embedding
            collection.update_one(
                {"_id": doc["_id"]}, {"$set": {"voyage_embedding": embedding}}
            )

            # Add a small delay to avoid hitting rate limits
            time.sleep(25)

        except Exception as e:
            print(f"Error generating embedding for recipe '{doc['title']}': {e}")

    print(
        f"Successfully added embeddings to {len(documents_without_embeddings)} documents"
    )

except pymongo.errors.ConnectionFailure as e:
    print(f"Error connecting to MongoDB: {e}")
except Exception as e:
    print(f"Error processing documents: {e}")
Enter fullscreen mode Exit fullscreen mode

Finally, run the script in the terminal:

python generate_embeddings.py
Enter fullscreen mode Exit fullscreen mode

Give this script a couple of minutes, or even better, sign up for a Voyage AI API key with a credit card (still free as in free beer) and ditch the sleep function call altogether. After the script has finished, we will have roughly 200 recipes with embeddings, ready to be queried, aggregated and, who knows, maybe even cooked? Let’s start building some simple views.

Embeddings in the Recipes collection

A couple of traditional Django views

The real fun with Django web development begins with writing the views—functions that apply the business logic, interact with the database, and output Python data structures of varying complexity to be displayed in the templates.

This tutorial will cover only a couple of “traditional” Django views, just enough to get the hang of it. Let’s create a trivial view that displays the first 20 recipes alphabetically. Django makes this task very simple. Edit the views.py/ file and add the following view:

def recipe_list(request):
    recipes = Recipe.objects.all().order_by("title")[:20]

    return render(request, "recipe_list.html", {"recipes": recipes})
Enter fullscreen mode Exit fullscreen mode

The page with the alphabetical list of recipes

Yes, it is as simple as it seems. Let’s finish the cycle, and add a template (in the folder /recipes/templates). Name it top_recipes.html and add the following:

{% extends 'base.html' %}

{% block content %}
<h1 class="text-3xl font-bold mb-6">Alphabetical list of recipes</h1>

{% if recipes %}
<ul class="list-disc pl-5">
    {% for recipe in recipes %}
    <li class="mb-1">
        {% comment  %}
        <a href="{% url 'recipe_detail' recipe.id %}">{{ recipe.title }}</a>
        {% endcomment %}
        {{ recipe.title }}
    </li>
    {% endfor %}
</ul>
{% else %}
<p>No recipes found matching "{{ query }}".</p>
{% endif %}

{% endblock %}
Enter fullscreen mode Exit fullscreen mode

The Django templating language allows looping over lists of objects, provides if/else constructs, custom filters, and much, much more, but that is beyond the scope of this tutorial. The final piece of the cycle is plugging it into the application-level urls.py which will now look like this:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("top/", views.top_recipes, name="top_recipes"),
]
Enter fullscreen mode Exit fullscreen mode

Before starting the development server, let’s add the most essential view—for viewing an individual recipe. Remember that we are using MongoDB's ObjectId as the primary key, so we need to take that into account.

Add another view to the views.py file:

def recipe_detail(request, recipe_id):
    """
    Display a recipe by its MongoDB ObjectId

    Args:
        request: Django request object
        recipe_id: String representation of the MongoDB ObjectId
    """
    # Convert string ID to MongoDB ObjectId
    try:
        object_id = ObjectId(recipe_id)
    except InvalidId:
        raise Http404(f"Invalid recipe ID format: {recipe_id}")

    # Get the recipe or return 404
    recipe = get_object_or_404(Recipe, id=object_id)

    # Create context with all needed data
    context = {"recipe": recipe}

    return render(request, "recipe_detail.html", context)
Enter fullscreen mode Exit fullscreen mode

At the top of the views.py file, add the necessary imports:

from bson import ObjectId
from bson.errors import InvalidId
from django.http import Http404
from django.shortcuts import get_object_or_404, render
Enter fullscreen mode Exit fullscreen mode

The bson package provides us the ObjectId itself, as well as the InvalidId error, while Django provides all the possible http-related helpers: In this case, we are using the standard get_object_or_404 shortcut that, as the name implies, fetches an object by ID or returns a 404 error response in case the object isn't found.

The other Http404 response is needed because we must first validate that the provided ID is actually convertible to a valid ObjectId. If you were working with plain Django, this step would not be needed.

Let’s provide a simple template for displaying the individual recipe. Create the file recipe_details.html in the templates folder:

{% extends 'base.html' %}

{% block title %}{{ recipe.title }}{% endblock %}

{% block content %}
<div class="max-w-2xl mx-auto py-4">
    <h1 class="text-xl font-semibold mb-3">{{ recipe.title }}</h1>

    {% if recipe.prep_time %}
    <p class="text-sm text-gray-600 mb-3">Prep time: {{ recipe.prep_time }} minutes</p>
    {% endif %}

    <div class="mb-4">
        <h2 class="text-md font-medium mb-1">Ingredients</h2>
        <div class="pl-4">
            <ul>
                {% for ingredient in recipe.ingredients %}
                <li>{{ ingredient }}</li>
                {% endfor %}
            </ul>
        </div>
    </div>

    <div>
        <h2 class="text-md font-medium mb-1">Instructions</h2>
        <div class="pl-4">
            {{ recipe.instructions|linebreaks }}
        </div>
    </div>
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

The templates contain simple Tailwind classes for purely aesthetic purposes, and you are of course free to go with bare-bones HTML or use virtually any CSS framework.

The urls.py entry for this view is a bit different, as it contains a parameter—the recipe_id that will be passed to the view function:

path("recipe/<str:recipe_id>/", views.recipe_detail, name="recipe_detail")
Enter fullscreen mode Exit fullscreen mode

The natural conversion of a MongoDB document structure into the Django templating language is a big plus: Development becomes fast, and errors are rare and easily identifiable.

Start the server, try out the URLss /search and /list, and verify that the links are working.
If inclined, you could create the remaining CRUD views yourself, for creating new and updating recipes.

Bear in mind that in that case, you would need to take in account the Voyage embeddings and regenerate them after every save. Django models provide a ton of features and methods, and overriding the save() method is probably the best place to implement such functionality.

Now, we will leverage some MongoDB specificities that make this crossover such a powerful tool in modern, AI-powered web development!

MongoDB aggregations and indexes

We will create a simple MongoDB aggregation to display the number of recipes belonging to each type of cuisine: French, Italian, Asian, and so on.

As per documentation:

Raw queries allow you to query the database by using MongoDB's aggregation pipeline syntax rather than Django methods. You can also run queries directly on your MongoClient object for expanded access to your MongoDB data. Let’s use the raw_aggregate and create a view that builds the aggregation pipeline.

Edit the views.py file:

def recipe_statistics(request):

    # Define the aggregation pipeline
    pipeline = [
        # Stage 1: Extract cuisine from the features subdocument
        {"$project": {"_id": 1, "cuisine": "$features.cuisine"}},
        # Stage 2: Group by cuisine and count occurrences
        {"$group": {"_id": "$cuisine", "count": {"$sum": 1}}},
        # Stage 3: Sort by count in descending order
        {"$sort": {"count": -1}},
        # Stage 4: Reshape the output for better readability
        {
            "$project": {
                "_id": 1,
                "cuisine": {"$ifNull": ["$_id", "Unspecified"]},
                "count": 1,
            }
        },
    ]

    stats = Recipe.objects.raw_aggregate(pipeline)
    result = list(stats)

    return render(
        request,
        "statistics.html",
        {"cuisine_stats": result},
    )
Enter fullscreen mode Exit fullscreen mode

Just being aware of the possibility of providing a MongoDB aggregation to the raw_aggregate method simplifies development. In this view, we created a very simple and plain MongoDB aggregation for counting recipes belonging to each cuisine. The rest is all Django. We leverage the objects manager (MongoManager, in the models.py), convert the queryset into a list, and provide it to the render function, along with the template and the request.

The template (statistics.html, together with the other templates) is simple. We just need to display the cuisines and the corresponding numbers:

{% extends 'base.html' %}

{% block title %}Recipe Statistics{% endblock %}

{% block content %}
<div class="py-6">
    <h1 class="text-2xl font-bold mb-6">Recipe Statistics</h1>

    <div class="grid md:grid-cols-2 gap-8">
        <!-- Cuisine Statistics -->
        <div class="border rounded p-4">
            <h2 class="text-xl font-semibold mb-4">Recipes by Cuisine</h2>

            {% if cuisine_stats %}
            <ul>
                {% for item in cuisine_stats %}
                <li class="mb-2">
                    <span class="font-medium">{{ item.cuisine|default:"Unspecified" }}:</span>
                    {{ item.count }} recipe{% if item.count != 1 %}s{% endif %}
                </li>
                {% endfor %}
            </ul>
            {% else %}
            <p>No cuisine data available.</p>
            {% endif %}
        </div>


    </div>
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Do not forget to hook the view up into the urls.py (the “local one”, not the topmost):

path("stats/", views.recipe_statistics, name="recipe_stats"),
Enter fullscreen mode Exit fullscreen mode

To recap, the cookbook/recipes/urls.py should look like this, at this point:

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("recipe/<str:recipe_id>/", views.recipe_detail, name="recipe_detail"),
    path("search/", views.recipe_search, name="recipe_search"),
    path("list/", views.recipe_list, name="recipe_list"),
    path("stats/", views.recipe_statistics, name="recipe_stats"),
Enter fullscreen mode Exit fullscreen mode

Creating indexes

We haven’t created any search indexes for our recipes—it is time to do so. We will create two indexes: a vector search index and an Atlas index:

Creation of the Atlas search index

Creation of the Atlas search index

The recipe_vector_search is the following:

{
  "fields": [
    {
      "type": "vector",
      "path": "voyage_embedding",
      "numDimensions": 1024,
      "similarity": "cosine"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The path is voyage_embedding since this is what we named our embedding field. The number of dimensions corresponds to the selected embedding and we chose the cosine distance as the similarity metric.

The other index that we will create is named default:

{
  "mappings": {
    "dynamic": false,
    "fields": {
      "instructions": {
        "type": "string",
        "indexOptions": "offsets",
        "store": true,
        "norms": "include"
      },
      "ingredients": {
        "type": "string",
        "indexOptions": "offsets",
        "store": true,
        "norms": "include",
        "multi": {
          "ingredients": {
            "type": "string",
            "indexOptions": "offsets",
            "store": true,
            "norms": "include"
          }
        }
      },
      "title": {
        "type": "string",
        "indexOptions": "offsets",
        "store": true,
        "norms": "include"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This index covers the recipe title, instructions, and ingredients. Notice the indexOptions key: When we create a text index with the "offsets" option, MongoDB stores additional information about where each indexed term appears in the indexed text. This additional positional data enables more advanced text search capabilities, particularly for phrase matching, proximity searches, and finding terms that appear close to each other in the text. This option does increase the index size but provides more powerful text search capabilities.

Let us now begin with a function that will perform the vector search. In a more serious project, this function would not live inside the views.py, but in this case, we will leave it there for simplicity:

def perform_vector_search(query_text, limit=10, num_candidates=None):
    if num_candidates is None:
    num_candidates = limit * 3

    try:
        # Generate embedding for the search query
        vo = voyageai.Client()  # Uses VOYAGE_API_KEY from environment
        query_embedding = vo.embed(
            [query_text], model="voyage-lite-01-instruct", input_type="query"
        ).embeddings[0]

        # Use Django's raw_aggregate to perform vector search
        results = Recipe.objects.raw_aggregate([
            {
                "$vectorSearch": {
                    "index": "recipe_vector_index",
                    "path": "voyage_embedding",
                    "queryVector": query_embedding,
                    "numCandidates": num_candidates,
                    "limit": limit,
                }
            },
            {
                "$project": {
                    "_id": 1,
                    "title": 1,
                    "ingredients": 1,
                    "instructions": 1,
                    "features": 1,
                    "score": {"$meta": "vectorSearchScore"},
                }
            },
        ])

        # Format the results - accessing attributes directly
        recipes = []
        for recipe in results:
            try:
                # Try direct attribute access first
                recipe_dict = {
                    "id": str(recipe.id),
                    "title": recipe.title,
                    "ingredients": recipe.ingredients,
                    "instructions": getattr(recipe, "instructions", ""),
                    "features": getattr(recipe, "features", {}),
                    "similarity_score": getattr(recipe, "score", 0),
                }
                recipes.append(recipe_dict)
            except Exception as e:
                print(f"Error formatting recipe: {str(e)}")
        return recipes

    except Exception as e:
        print(f"Error in vector search: {str(e)}")
        return []
Enter fullscreen mode Exit fullscreen mode

In order for this function to work, we need to add some imports at the top of the views.py file and enable the Voyage AI embeddings. At the top of the views.py file, add the following:

import voyageai
from bson import ObjectId
from bson.errors import InvalidId
from django.http import Http404
from django.shortcuts import get_object_or_404, render
from dotenv import load_dotenv

from .models import Recipe

load_dotenv()
Enter fullscreen mode Exit fullscreen mode

Remember, the query needs to be converted into numbers/embeddings using the same embeddings model used to create the embeddings for the recipes. Otherwise, it will not work. At the top of the views.py file, we imported the dotenv library that allows us to access the Voyage AI API key.

The rest is really just an ordinary MongoDB vector search. We specify the name of the vector index, the path (voyage_embedding), and some options. The vector search has to be the first entry in the aggregation pipeline. After that, we just add a project stage in which we add the score—how close the result is to the search query.

Finally, the results are just formatted and returned as a list of dictionaries. This function is not a view. It is a helper that we will use in the actual view.

Let's see the view function:

def ingredient_vector_search(request):
    """
    View for searching recipes by ingredients using vector search
    """
    query = request.GET.get("query", "")
    results = []

    if query:
        ingredient_query = f"Ingredients: {query}"
        results = perform_vector_search(ingredient_query, limit=10)

    context = {"query": query, "results": results}
    return render(request, "vector_search.html", context)
Enter fullscreen mode Exit fullscreen mode

The bulk of the work is done in the vector search function. this view just leverages it and passes the results to a template. Let’s create the template vector_search.html, alongside the others:

{% extends 'base.html' %}

{% block title %}Ingredient Vector Search{% endblock %}

{% block content %}
<div class="py-4">
    <h1 class="text-2xl font-bold mb-4">Search Recipes by Ingredients</h1>

    <form method="GET" action="{% url 'ingredient_search' %}" class="mb-6">
        <div class="flex">
            <input type="text" name="query" placeholder="Enter ingredients (e.g., chicken, garlic, lemon)"
                value="{{ query }}" class="px-4 py-2 border rounded-l flex-grow">
            <button type="submit" class="bg-blue-600 text-white px-6 py-2 rounded-r hover:bg-blue-700 transition">
                Search
            </button>
        </div>
        <p class="text-gray-600 text-sm mt-2">Uses AI-powered vector search to find recipes with similar ingredients</p>
    </form>

    {% if query %}
    <h2 class="text-lg font-semibold mb-4">Results for "{{ query }}"</h2>

    {% if results %}
    <div class="space-y-6">
        {% for recipe in results %}
        <div class="border rounded-lg p-4 shadow hover:shadow-md transition">
            <h3 class="text-xl font-semibold mb-2">{{ recipe.title }}</h3>
            <div class="mb-3">
                <p class="text-sm text-gray-600">Similarity: {{ recipe.similarity_score|floatformat:2 }}</p>
            </div>
            <div class="mb-3">
                <h4 class="font-medium mb-1">Ingredients:</h4>
                <ul class="list-disc pl-5">
                    {% for ingredient in recipe.ingredients %}
                    <li>{{ ingredient }}</li>
                    {% endfor %}
                </ul>
            </div>
            <a href="{% url 'recipe_detail' recipe.id %}" class="text-blue-600 hover:underline">View full recipe</a>
        </div>
        {% endfor %}
    </div>
    {% else %}
    <p class="text-gray-600">No recipes found matching your ingredients.</p>
    {% endif %}
    {% endif %}
</div>
{% endblock %}

And let’s add it to the urls.py:
    path(
        "ingredient-search/", views.ingredient_vector_search, name="ingredient_search"
    ),
Enter fullscreen mode Exit fullscreen mode

The vector search page

The final code (in the repository) contains a very similar search function and view for an Atlas search that performs fuzzy matching. If you type “corrot” instead of “carrot”, you will get the correct results. Fuzzy search is a powerful technique in information retrieval that allows users to find relevant results even when their query contains typos, misspellings, or slight variations from the target content.

The function is extracted from the view in the same way it was done for the vector search. This would allow us to reuse much of the templates—for instance, one template for all pages that display some type of results list and even a single view function (or class-based view!) which takes the type of search as a parameter, but this is left to the reader as an exercise in Django-ing.

Integrating an LLM for a personalized experience

For the final part of this recipe application, we will create a simple view for suggesting meals based on the ingredients a user has at hand. By implementing a couple of simple prompts for Claude Haiku, we will leverage the vector search results and ask the LLM to provide suggestions based on the recipes. Any LLM model could be used in this case, and for local prototyping, even a locally run LLM would be more than enough. We are not adding some great value really, just showcasing how to stitch the parts together.

A more thorough approach would include a hybrid search—combining results from an Atlas search and a vector search and integrating all the results together, maybe running them through a re-ranker (also provided by Voyage AI) and crafting sophisticated prompts through prompt engineering techniques.

The possibilities are virtually unlimited. Django provides the rock-solid web framework foundation, while the MongoDB integration allows for rich and complex queries and AI-powered solutions.

Let’s see a simple function that uses Claude Haiku to provide suggestions. At the top of the views.py file, add the imports needed for the Anthropic client:

from anthropic import Anthropic
and read the API key:
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
Enter fullscreen mode Exit fullscreen mode

While Voyage AI reads the API key automatically, Anthropic’s client does not, so the previous explicit statement is mandatory. The function, which should reside in a separate file, just like our search functions, will stay inside the views.py file for simplicity:

def get_claude_suggestions(user_ingredients, similar_recipes, max_suggestions=4):
    """
    Get meal suggestions from Claude based on available ingredients and similar recipes

    Args:
        user_ingredients (list): List of ingredients provided by the user
        similar_recipes (list): List of similar recipes found by vector search
        max_suggestions (int): Maximum number of suggestions to return

    Returns:
        list: List of meal suggestions from Claude
    """
    client = Anthropic(api_key=ANTHROPIC_API_KEY)

    # Prepare the prompt for Claude
    prompt = f"""I have these ingredients: {", ".join(user_ingredients)}


Based on these ingredients, I need `{max_suggestions}` meal suggestions. 
Here are some similar recipes from my database that might help you:

    {json.dumps(similar_recipes, indent=2)}

For each suggestion, please:
1. Provide a recipe name
2. List the ingredients I have that can be used
3. Suggest substitutions for any missing ingredients
4. Give a brief description of how to prepare it
5. Mention difficulty level (easy, medium, hard)

Be friendly, practical, and focus on using what I have available with minimal extra ingredients. 
Keep your answer concise and focused on the meal suggestions.
"""

    # Call Claude API
    response = client.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=1500,
        temperature=0.7,
        system="You are a helpful cooking assistant that provides meal suggestions based on available ingredients.",
        messages=[{"role": "user", "content": prompt}],
    )

    # Extract and parse suggestions
    suggestions_text = response.content[0].text

    # Split the suggestions - we'll assume each suggestion starts with a recipe name and number
    raw_suggestions = []
    current_suggestion = ""

    for line in suggestions_text.split("\n"):
        # Check if this line starts a new suggestion
        if line.strip() and (
            line.strip()[0].isdigit() and line.strip()[1:3] in [". ", ") "]
        ):
            if current_suggestion:
                raw_suggestions.append(current_suggestion.strip())
            current_suggestion = line
        else:
            current_suggestion += "\n" + line

    # Add the last suggestion
    if current_suggestion:
        raw_suggestions.append(current_suggestion.strip())

    # Limit to max_suggestions
    return raw_suggestions[:max_suggestions]
Enter fullscreen mode Exit fullscreen mode

The function is a bit long, but it essentially works on a list of recipes. Those will be provided by the vector search and a list of user-provided ingredients. After getting the response from the Anthropic API, the function wraps it into a list (raw_suggestions) and returns the list.

The view function that will use the get_claude_suggestions() function and operate on the HTTP request is the following:

def ai_meal_suggestions(request):
    """
    View that combines vector search with Claude AI to suggest meals
    based on user-provided ingredients
    """
    query = request.GET.get("ingredients", "")
    suggestions = []
    error_message = None

    if query:
        try:
            # Clean up the input - split by commas and strip whitespace
            ingredients_list = [ing.strip() for ing in query.split(",") if ing.strip()]
            ingredients_text = ", ".join(ingredients_list)

            # Perform vector search to find similar recipes
            search_query = f"Ingredients: {ingredients_text}"
            similar_recipes = perform_vector_search(search_query, limit=10)

            if similar_recipes:
                # Format recipe data for Claude
                recipes_data = []
                for recipe in similar_recipes:
                    recipes_data.append({
                        "title": recipe.get("title", ""),
                        "ingredients": recipe.get("ingredients", []),
                        "score": recipe.get("similarity_score", 0),
                        "id": recipe.get("id", ""),
                    })

                # Call Claude API for meal suggestions
                suggestions = get_claude_suggestions(ingredients_list, recipes_data)
            else:
                error_message = "No similar recipes found for the provided ingredients."

        except Exception as e:
            error_message = f"An error occurred: {str(e)}"

    context = {
        "ingredients": query,
        "suggestions": suggestions,
        "error_message": error_message,
    }

    return render(request, "ai_suggestions.html", context)
Enter fullscreen mode Exit fullscreen mode

The previous function is really just a wrapper for the Claude suggestions that needs improvement, but it gets the job done. The template for this last view is named ai_suggestions.html:

{% extends 'base.html' %}

{% block title %}AI Meal Suggestions{% endblock %}

{% block content %}
<div class="py-4">
    <h1 class="text-2xl font-bold mb-4">AI Meal Suggestions</h1>
    <p class="mb-4">Enter the ingredients you have available, separated by commas. Our AI will suggest meals you can
        make!</p>

    <form method="GET" action="{% url 'ai_meal_suggestions' %}" class="mb-8">
        <div class="mb-4">
            <label for="ingredients" class="block text-sm font-medium text-gray-700 mb-1">Your Ingredients:</label>
            <textarea id="ingredients" name="ingredients" rows="3"
                placeholder="e.g., chicken, garlic, onion, rice, bell pepper"
                class="w-full px-4 py-2 border rounded focus:ring-blue-500 focus:border-blue-500">{{ ingredients }}</textarea>
        </div>
        <button type="submit" 
            class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition">
            Get Meal Suggestions
        </button>
    </form>

    {% if error_message %}
    <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-6">
        {{ error_message }}
    </div>
    {% endif %}

    {% if suggestions %}
    <h2 class="text-xl font-semibold mb-4">Suggested Meals with Your Ingredients</h2>

    <div class="space-y-6">
        {% for suggestion in suggestions %}
        <div class="bg-white border rounded-lg p-5 shadow">
            {{ suggestion|linebreaks }}

        </div>
        {% endfor %}
    </div>

    <div class="mt-8 p-4 bg-blue-50 rounded">
        <p class="text-sm text-gray-600">
            <strong>Note:</strong> These suggestions are generated by AI based on your provided ingredients and similar
            recipes in our database.
            You may need to adjust quantities or cooking techniques based on your preferences.
        </p>
    </div>
    {% endif %}
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

With all the pieces now in place, we are ready to integrate everything in a final /cookbook/recipes/urls.py file:

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("top/", views.top_recipes, name="top_recipes"),
    path("recipe/<str:recipe_id>/", views.recipe_detail, name="recipe_detail"),
    path("stats/", views.recipe_statistics, name="recipe_stats"),
    path(
        "ingredient-search/", views.ingredient_vector_search, name="ingredient_search"
    ),
    path("fuzzy-search/", views.fuzzy_search, name="fuzzy_search"),
    path("ai-suggestions/", views.ai_meal_suggestions, name="ai_meal_suggestions"),
]
Enter fullscreen mode Exit fullscreen mode

And edit the base.html file to include a nice sidebar with all the links to the views that we have created:

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block title %}Django MongoDB Backend Recipes{% endblock %}</title>
    <script src="https://unpkg.com/@tailwindcss/browser@4"></script>
    {% block extra_css %}{% endblock %}
</head>

<body class="flex flex-col min-h-screen bg-gray-50">
    <!-- Mobile Header - Only shown on small screens -->
    <header class="bg-blue-600 text-white py-3 px-4 md:hidden">
        <div class="flex justify-between items-center">
            <a href="{% url 'index' %}" class="text-xl font-bold">Django MongoDB Recipes</a>
            <button id="mobile-menu-button" class="text-white focus:outline-none">
                <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
                    xmlns="http://www.w3.org/2000/svg">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16">
                    </path>
                </svg>
            </button>
        </div>
        <!-- Mobile Menu -->
        <div id="mobile-menu" class="hidden mt-3">
            <a href="{% url 'index' %}" class="block py-2 hover:bg-blue-700 rounded px-2">Home</a>
            <a href="{% url 'recipe_search' %}" class="block py-2 hover:bg-blue-700 rounded px-2">Search</a>
            <a href="{% url 'recipe_list' %}" class="block py-2 hover:bg-blue-700 rounded px-2">All Recipes</a>
            <a href="{% url 'recipe_stats' %}" class="block py-2 hover:bg-blue-700 rounded px-2">Statistics</a>
        </div>
    </header>

    <div class="flex flex-grow">
        <!-- Left Sidebar -->
        <aside class="w-64 bg-gray-800 text-white hidden md:block">
            <div class="p-4 sticky top-0">
                <!-- Logo/Title Area -->
                <div class="mb-6 pb-4 border-b border-gray-700">
                    <a href="{% url 'index' %}" class="text-xl font-bold block">
                        MongoDB Recipes
                    </a>
                </div>

                <h3 class="text-lg font-semibold mb-4 border-b border-gray-700 pb-2">Navigation</h3>
                <ul class="space-y-2">
                    <li>
                        <a href="{% url 'index' %}" class="block py-2 px-4 hover:bg-gray-700 rounded transition">
                            Home
                        </a>
                    </li>
                    <li>
                        <a href="{% url 'recipe_search' %}"
                            class="block py-2 px-4 hover:bg-gray-700 rounded transition">
                            Search Recipes
                        </a>
                    </li>
                    <li>
                        <a href="{% url 'recipe_list' %}" class="block py-2 px-4 hover:bg-gray-700 rounded transition">
                            All Recipes
                        </a>
                    </li>
                    <li>
                        <a href="{% url 'recipe_stats' %}" class="block py-2 px-4 hover:bg-gray-700 rounded transition">
                            Recipe Statistics
                        </a>
                    </li>
                    <li>
                        <a href="{% url 'ingredient_search' %}"
                            class="block py-2 px-4 hover:bg-gray-700 rounded transition">
                            Ingredient Search
                        </a>
                    </li>
                    <li>
                        <a href="{% url 'atlas_search' %}" class="block py-2 px-4 hover:bg-gray-700 rounded transition">
                            Atlas Search
                        </a>
                    </li>
                    <li>
                        <a href="{% url 'ai_meal_suggestions' %}"
                            class="block py-2 px-4 hover:bg-gray-700 rounded transition">
                            AI Meal Suggestions
                        </a>
                    </li>
                </ul>


            </div>
        </aside>

        <!-- Main Content -->
        <main class="flex-grow px-4 py-6">
            <div class="container mx-auto">
                {% block content %}
                <h1 class="text-3xl font-bold underline">Hello world!</h1>
                {% endblock %}
            </div>
        </main>
    </div>

    <!-- Footer -->
    <footer class="bg-gray-800 text-white py-6">
        <div class="container mx-auto px-4">
            <div class="flex flex-col md:flex-row justify-between items-center">
                <div class="mb-4 md:mb-0">
                    <p>&copy; {% now "Y" %} Django + MongoDB = Backend <3 </p>
                </div>
            </div>
        </div>
    </footer>

    <script>
        // Toggle mobile menu
        document.getElementById('mobile-menu-button').addEventListener('click', function () {
            const menu = document.getElementById('mobile-menu');
            menu.classList.toggle('hidden');
        });
    </script>
    {% block extra_js %}{% endblock %}
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

The final application should look more or less like this:

The finished web application

Conclusion

In this simple yet practical project, we have learned how to combine Django, a premier web framework, and MongoDB, the most popular and feature-rich document database, with the use of the Django MongoDB Backend. We have seen how to quickly map our business logic through Django views, how to model data with Django models, and the MongoDB-specific features provided by the backend. We have seen how to quickly draw modular templates and integrate third-party services, such as the Claude API.

View the GitHub repo with the code.

There is much more functionality that can be added to this project. Django provides excellent user management, so the processes of registering and logging users, for instance, are streamlined and battle-tested. It is worth noting that the most popular Django authentication package / Django AllAuth is on top of the priorities list when it comes to integrations with the Backend.

The Django MongoDB Backend is an official and deep integration. Rather than using workarounds, the team took a deep approach to understand Django's framework internals, hooking directly into Django's lookup operators and making MongoDB's aggregation framework match Django's query structure. The team plans to integrate and ensure compatibility with some of the most sought Django packages—Django Filter and Django REST Framework, to name a couple.

An excellent basis for rapid AI-driven web applications, the Django MongoDB Backend is a sign of exciting times to come in the everchanging and fast-paced world at the intersection of AI and web development!

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

Quickstart image

Django MongoDB Backend Quickstart! A Step-by-Step Tutorial

Get up and running with the new Django MongoDB Backend Python library! This tutorial covers creating a Django application, connecting it to MongoDB Atlas, performing CRUD operations, and configuring the Django admin for MongoDB.

Watch full video →

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay