Skip to main content

How to Implement Semantic Search in MongoDB

Learn how to implement semantic search in MongoDB with Python. Generate vector embeddings, create a Vector Search index, and run $vectorSearch queries.
Jul 30, 2026  · 9 min read

Explore with AI

Open in ChatGPTOpen in ClaudeOpen in Perplexity

These are the moments where regular text search falls short. The following is what that looks like in practice:

  • A customer support portal returns no results when a user types "won't turn on" instead of the exact phrase used in the help article.
  • An e-commerce platform finds nothing when a shopper searches "something warm for winter" because no product description contains those exact words.
  • A knowledge base misses the right document because the user phrased their question differently from how the document was written.

Semantic search solves this. It understands the meaning and intent behind a query rather than matching exact words.

This tutorial shows you how to implement semantic search in MongoDB using Python and the free nomic-embed-text-v1 embedding model.

Note: nomic-embed-text-v1 is approximately 0.27 GB when loaded into memory, so make sure your machine has at least 1 GB of available RAM before running any script in this tutorial. The first run downloads the model from Hugging Face, so allow extra time depending on your internet connection. All subsequent runs load the model from your local cache. CPU inference is significantly slower than GPU inference. On machines without a dedicated GPU, the model runs on CPU and uses RAM rather than VRAM, so embedding generation may take longer.

You'll learn the following in this tutorial:

  • What vector embeddings are and how they represent meaning
  • How to generate and store embeddings in MongoDB
  • How to create a MongoDB Vector Search index
  • How to convert a user query into a vector
  • How to run a $vectorSearch aggregation pipeline and interpret the results

You can find all the code samples for this tutorial in the GitHub repository.

Text Search vs Semantic Search

Text search matches documents that contain the exact words in your query. Semantic search matches documents that carry the same meaning as your query, even if they use entirely different words.

The following table shows what each approach returns for the search query "heart problems":

Document text Text search Semantic search Why text search returns it
"...heart problems in adults..." Yes Yes Contains the exact words "heart problems"
"...cardiac conditions and symptoms..." No Yes Doesn't contain the exact words "heart problems"
"...risk factors for heart disease..." No Yes Doesn't contain the exact words "heart problems"
"...chest pain and shortness of breath..." No Yes Doesn't contain the exact words "heart problems"

Semantic Search Concepts

Three concepts are central to semantic search in MongoDB. They'll help you understand why each step in this tutorial works the way it does.

Vector embeddings

An embedding model converts text into a fixed-length list of numbers called a vector. Each number in the vector represents a dimension of meaning.

Two texts with similar meanings produce vectors that are numerically close to each other. "cardiac conditions" and "heart problems" land near each other in vector space, even though they share no words. That closeness is what makes semantic search possible.

This tutorial uses the nomic-embed-text-v1 embedding model, which is free, open-source, and runs entirely on your local machine. The model automatically downloads from Hugging Face the first time you run the script and saves locally for all future runs. The model produces 768 numbers per input.

Vector search indexes

A vector search index tells MongoDB which field holds the embeddings, how many dimensions to expect, and which similarity function to use for comparison. You must create this index before you can run any $vectorSearch queries.

The $vectorSearch aggregation stage

$vectorSearch is the aggregation stage that runs the search. It accepts a query vector, searches the indexed field, and returns documents ranked by semantic similarity. You chain it with $project and other stages just like any other aggregation pipeline.

Now let's get started.

Prerequisites

Before you start, make sure you have the following ready:

  • Python 3.8 or later installed
  • A MongoDB Atlas account with an M0 (Free tier) cluster set up
  • pymongo 4.7 or later, sentence-transformers, and einops Python packages installed
  • Basic familiarity with Python and MongoDB collections
  • Your Atlas connection string, available in the Atlas UI under **Database > Connect > Drivers**
  • Your IP address is allowed in Atlas before you run any scripts. Go to **Security > Network Access** in the Atlas UI and add your current IP address.

Set Up Your Project

Create a folder for your project and navigate into it in your terminal:

mkdir mongodb-semantic-search
cd mongodb-semantic-search

Create and activate a virtual environment to keep your project dependencies isolated:

python -m venv venv
source venv/bin/activate

On Windows, activate the virtual environment with:

venv\Scripts\activate

Now install the required packages:

pip install pymongo sentence-transformers einops

You'll create one Python file for each step of this tutorial. All files go in the mongodb-semantic-search folder.

Create the Embedding Utilities File

Create a file named embedding_utils.py. This file holds both embedding functions used in this tutorial:

from sentence_transformers import SentenceTransformer

# Load the free, open-source embedding model.
# The model downloads from Hugging Face on first run and saves locally.
# trust_remote_code=True is required by this model:
model = SentenceTransformer("nomic-ai/nomic-embed-text-v1", trust_remote_code=True)


def get_embedding(text, precision="float32"):
    # Use this function when embedding text you plan to store in MongoDB:
    return model.encode(text, precision=precision).tolist()


def get_query_embedding(text, precision="float32"):
    # Use this function when embedding a user's search query:
    return model.encode(text, precision=precision).tolist()

Warning: trust_remote_code=True allows the model to execute model-specific Python code downloaded from Hugging Face onto your machine. If the model's source code on Hugging Face is compromised or updated with malicious changes, that code runs automatically in your environment. Exercise caution before using this parameter in a production deployment.

Generate and Store Vector Embeddings

Create a file named generate_embeddings.py. This file imports the embedding function from embedding_utils.py, generates a vector for each document's text field, and stores the full document, including the embedding, in MongoDB:

from embedding_utils import get_embedding
from pymongo import MongoClient

# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
    "mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
    appname="devrel-tutorial-python-semantic-search"
)

collection = mongodb_client["sample_db"]["documents"]

# Sample data:
sample_documents = [
    {
        "title": "MongoDB Atlas",
        "text": "MongoDB Atlas is a fully managed cloud database."
    },
    {
        "title": "Vector Search",
        "text": "Vector search finds results based on semantic meaning."
    },
    {
        "title": "Nomic AI",
        "text": "nomic-embed-text-v1 is a free, open-source embedding model."
    },
]

docs_to_insert = []

for doc in sample_documents:
    embedding = get_embedding(doc["text"])
    docs_to_insert.append({
        "title": doc["title"],
        "text": doc["text"],
        # The vector lives alongside your original data in the same document:
        "embedding": embedding
    })

# Drop the collection before each run to avoid inserting duplicate documents:
collection.drop()

result = collection.insert_many(docs_to_insert)
print(f"Inserted {len(result.inserted_ids)} documents with embeddings.")

mongodb_client.close()

In the preceding code, collection.drop() deletes all documents and indexes on the collection before each run. Re-running generate_embeddings.py without this line inserts duplicate documents, which causes $vectorSearch to return the same document multiple times. Dropping the collection also deletes the vector search index, so you must re-run create_vector_index.py whenever you re-run generate_embeddings.py.

Run the script:

python generate_embeddings.py

You'll get the following output in your terminal:

<All keys matched successfully>
Inserted 3 documents with embeddings.

Each document in MongoDB now holds both the original text and its 768-dimensional vector in the embedding field. The vector lives alongside your data in the same document, so no separate storage or lookup is needed at query time.

Create a MongoDB Vector Search Index

Create a file named create_vector_index.py. This file defines and creates a vector search index on the embedding field so MongoDB can run $vectorSearch queries against your collection.

The index definition requires three fields:

  • path: the field that holds the embeddings (embedding in this tutorial)
  • numDimensions: must match the model's output size (768 for nomic-embed-text-v1)
  • similarity: the comparison function (cosine is correct for this model)

The numDimensions value must match your embedding model exactly. A mismatch causes the index creation to fail:

from pymongo.mongo_client import MongoClient
from pymongo.operations import SearchIndexModel
import time

# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
    "mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
    appname="devrel-tutorial-python-semantic-search"
)

# Point to the same database and collection you used in the previous step:
database = mongodb_client["sample_db"]
collection = database["documents"]

# Define the vector search index.
# The three required fields tell MongoDB what to index and how to compare vectors:
search_index_model = SearchIndexModel(
    definition={
        "fields": [
            {
                "type": "vector",
                "path": "embedding",      # The field name from generate_embeddings.py
                "numDimensions": 768,     # nomic-embed-text-v1 always outputs 768 dimensions
                "similarity": "cosine"    # Recommended similarity function for this model
            }
        ]
    },
    name="vector_index",
    type="vectorSearch"
)

result = collection.create_search_index(model=search_index_model)
print("New search index named " + result + " is building.")

# Poll every five seconds until the index is ready to accept queries:
print("Polling to check if the index is ready. This may take up to a minute.")

predicate = lambda index: index.get("queryable") is True

while True:
    indices = list(collection.list_search_indexes(result))
    if len(indices) and predicate(indices[0]):
        break
    time.sleep(5)

print(result + " is ready for querying.")

mongodb_client.close()

Run the script:

python create_vector_index.py

You'll get the following output in your terminal:

New search index named vector_index is building.
Polling to check if the index is ready. This may take up to a minute.
vector_index is ready for querying.

The index takes up to a minute to build. The polling loop checks every 5 seconds and 5 exits only when MongoDB confirms the index is queryable. Don't move to the next step until you see "vector_index is ready for querying."

Convert Your Search Query Into a Vector

Create a file named generate_query_vector.py. This file imports the get_query_embedding() function from the embedding_utils.py file. Run the generate_query_vector.py file to verify that the embedding model loads correctly and produces a 768-dimensional vector:

  • It defines the get_query_embedding() function that the next step imports.
  • You can run it on its own to verify that the model is working correctly.

You must use the same model here as you did in the first step. A different model produces vectors that occupy a different numerical space, making comparison meaningless:

from embedding_utils import get_query_embedding

user_query = "I need an automated, scalable system for serious information storage"
query_vector = get_query_embedding(user_query)
print(f"Query: '{user_query}'")
print(f"Vector dimensions: {len(query_vector)}")
print(f"First 5 values: {query_vector[:5]}")

Run the script:

python generate_query_vector.py

You'll get an output similar to this:

<All keys matched successfully>
Query: 'I need an automated, scalable system for serious information storage'
Vector dimensions: 768
First 5 values: [0.0025914530269801617, 0.09862980246543884, -0.023092379793524742, -0.0171672236174345, -0.05548065900802612]

In the preceding output, Vector dimensions: 768 confirms that the model output matches your stored embeddings.

Run a $vectorSearch Query

Create a file named run_vector_search.py. This file imports the get_query_embedding() function from the embedding_utils.py file and converts a user's search term into a vector. run_vector_search.py runs a $vectorSearch aggregation pipeline, and prints the results ranked by semantic similarity:

from embedding_utils import get_query_embedding
from pymongo import MongoClient

# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
    "mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
    appname="devrel-tutorial-python-semantic-search"
)

collection = mongodb_client["sample_db"]["documents"]

# Generate a query vector from the user's search input:
user_query = "I need an automated, scalable system for serious information storage"
query_vector = get_query_embedding(user_query)

# Define the $vectorSearch aggregation pipeline:
pipeline = [
    {
        "$vectorSearch": {
            "index": "vector_index",      # The index created in create_vector_index.py
            "path": "embedding",          # The field that holds your stored vectors
            "queryVector": query_vector,  # The vector generated from the user's query
            "numCandidates": 150,         # How many neighbors MongoDB considers
            "limit": 3                    # How many results to return
        }
    },
    {
        "$project": {
            "_id": 0,
            "title": 1,
            "text": 1,
            "score": {
                "$meta": "vectorSearchScore"  # Relevance score for each result
            }
        }
    }
]

results = collection.aggregate(pipeline)

print(f"\nTop results for query: '{user_query}'\n")

for doc in results:
    print(f"Title: {doc['title']}")
    print(f"Text:  {doc['text']}")
    print(f"Score: {doc['score']:.4f}")
    print()

mongodb_client.close()

In the preceding code, two parameters control the search behavior:

  • numCandidates sets the size of the initial search pool that MongoDB examines before narrowing to the final results. A larger value improves recall but takes slightly longer.
  • limit sets how many results you get back. In this tutorial, limit is set to 3, and numCandidates is set to 150. A common starting point is to set numCandidates to 10-15 times your limit.

Now run the script:

python run_vector_search.py

You'll get an output similar to this:

<All keys matched successfully>

Top results for query: 'I need an automated, scalable system for serious information storage.'

Title: MongoDB Atlas
Text:  MongoDB Atlas is a fully managed cloud database.
Score: 0.7211

Title: Nomic AI
Text:  nomic-embed-text-v1 is a free, open-source embedding model.
Score: 0.6818

Title: Vector Search
Text:  Vector search finds results based on semantic meaning.
Score: 0.6642

"MongoDB Atlas" scores highest even though the query "I need an automated, scalable system for serious information storage" shares no words with "MongoDB Atlas is a fully managed cloud database." The search returns it because their vectors are close in meaning: "...an automated, scalable system for information storage" maps semantically to "...a fully managed cloud database". A text search for the same query would return zero results, because none of the exact query words appear in any document. That is semantic search working as intended.

Key Takeaways

  • MongoDB Vector Search runs on an M0 Free tier cluster. No paid plan is required.
  • You must use the same embedding model for both generating stored embeddings and generating query vectors. Mixing models produces meaningless results.
  • The numDimensions value in your index definition must match the output size of your embedding model exactly. nomic-embed-text-v1 always produces 768 dimensions.
  • input_type="document" optimizes embeddings for storage. input_type="query" optimizes them for retrieval. Use the correct type at each stage.
  • numCandidates controls the size of the search net MongoDB casts before narrowing to the final limit results. A larger value improves recall at the cost of query time.
  • vectorSearchScore ranks results by semantic similarity. Results don't need to contain any of the exact words from the query. Score ranges vary by model and dataset, but the following boundaries are a useful starting point for nomic-embed-text-v1 with cosine similarity:
    • 0.9 and above: Near-identical meaning. The document and query are semantically almost the same.
    • 0.7 to 0.9: Strong relevance. The document clearly relates to the query intent.
    • 0.5 to 0.7: Moderate relevance. The document is topically related but uses different framing or context.
    • Below 0.5: Weak relevance. The connection is loose, and the result may not be useful.

You can find all the code samples for this tutorial in the GitHub repository.

Further Reading

FAQs

Do I need a paid Atlas plan to implement semantic search?

No. All four steps in this tutorial run on an M0 Free tier cluster, which is free.

What happens if I use a different embedding model for my query than for my stored documents?

Your results will be meaningless. The vectors won't be comparable because different models map text to different numerical spaces. Always use the same model for both indexing and querying.

What's the difference between `numCandidates` and `limit`?

numCandidates is how many vectors MongoDB examines during the search. limit is how many of the top results it returns to you. A higher numCandidates value improves the quality of results at the cost of slightly slower queries. A common starting point is to set numCandidates to 10 to 15 times your limit.

Can I use semantic search and text search together?

Yes. MongoDB supports hybrid search, which combines $vectorSearch and $search in a single pipeline.

Does semantic search work for languages other than English?

It depends on your embedding model. nomic-embed-text-v1 is trained primarily on English text. For multilingual use cases, choose a multilingual embedding model trained on the languages your data contains.


Damilola Oladele's photo
Author
Damilola Oladele
Topics
Related

Tutorial

How to Build a Vector Search Application with MongoDB Atlas and Python

Learn how to run your first MongoDB vector search. This tutorial walks you through finding similar items with embeddings and step-by-step examples.
Nilesh Soni's photo

Nilesh Soni

Tutorial

How to Store and Query Embeddings in MongoDB

Learn how to store, index, and query embeddings in MongoDB using Atlas Vector Search.
Nilesh Soni's photo

Nilesh Soni

Tutorial

How to Search Images and Text Using MongoDB Vector Search With FastAPI

Build an end-to-end vector search app with FastAPI and MongoDB Atlas. Learn to generate, store, and query text and image embeddings in this hands-on tutorial.
Moses Anumadu's photo

Moses Anumadu

Tutorial

Mastering Vector Search in MongoDB: A Guide With Examples

Learn how to set up and use MongoDB's Vector Search for building smart apps. This practical guide covers vector indexing, query best practices, and real-world examples.
Karen Zhang's photo

Karen Zhang

Tutorial

Hybrid Search: Combining Vector and Keyword Queries in MongoDB

Learn about hybrid search and how to utilize it in MongoDB.
Anaiya Raisinghani's photo

Anaiya Raisinghani

Tutorial

Introduction to MongoDB and Python

In this tutorial, you'll learn how to integrate MongoDB with your Python applications.
Derrick Mwiti's photo

Derrick Mwiti

See MoreSee More