Have you ever imagined taking a photo of yourself, comparing it against a database full of celebrities, and instantly finding out who you look most alike?
Or maybe uploading any image, an object, a place, a product, and getting back the items in your database that look the most similar?
This is the idea behind image similarity search, and it’s becoming increasingly common in modern applications. The best part: You don’t need a complex architecture to build it. MongoDB Atlas already gives you everything you need.
In this article, we’ll walk through this solution in two simple stages:
-
Automatic embeddings generation
- You insert a document containing an image URL.
- An Atlas Trigger automatically detects the insert event.
- An Atlas Function calls Voyage AI’s multimodal model to generate the image embedding and store it back into the document.
-
Searching for similar images using vector search: Once your documents have embeddings, the similarity search workflow happens in three simple steps:
- Create the vector search index.
- Generate another embedding for the query image.
- Run the vector search query.
By the end, you’ll have a database that can understand images—and return the ones that look the most similar to any photo you send.
Prerequisites
To follow this tutorial, you only need:
- A MongoDB Atlas account.
- A Voyage AI API token.
Document model
To keep the example simple and realistic, we’ll work with a collection of articles. Each article contains basic metadata, a title, tags, authors, and a URL, along with a cover image.
{
"title": "Getting Started With Hibernate ORM and MongoDB",
"tags": ["Java", "ORM"],
"publishedAt": "11-04-2025",
"authors": ["Ricardo Mello"],
"url": "https://foojay.io/today/getting-started-with-hibernate-orm-and-mongodb/",
"views": 1065,
"cover": "https://www.freelancinggig.com/blog/wp-content/uploads/2018/03/Hibernate-ORM.jpg"
}
This cover image is what we’ll use later to perform image similarity search, allowing us to find articles whose images look similar to a query image.
Once this document is inserted, we’ll set up an Atlas Trigger so that every new article automatically gets an image embedding.
Atlas Trigger
Atlas Triggers let you automatically run server-side logic whenever a change occurs in your MongoDB Atlas data. They listen to real-time insert, update, replace, and delete events through MongoDB change streams and react instantly by executing custom logic that you define.
Because Triggers run on a fully managed, serverless compute layer, they’re ideal for event-driven workflows such as enriching documents, synchronizing data, or calling external APIs.
In our case, we use a Trigger that fires on every insert and invokes an Atlas Function to generate the image embedding. In MongoDB Atlas, open your project and go to:
Project → Streaming Data → Triggers
Configure it as follows:
Trigger Type: Database
Watch Against: Collection
Cluster / Database / Collection: select your target collection
Operation Type: Insert
Full Document: Enable
Click Save to create the Trigger.
Atlas Functions
Atlas Functions let you run custom JavaScript in a fully managed, serverless environment. You don’t need to deploy or maintain any servers, you just write the code, and MongoDB Atlas takes care of executing it whenever it’s called.
They’re ideal for lightweight tasks like transforming documents, validating data, moving information between collections, or calling external APIs. For example, you might send an email notification, update related records, or, in our case, call Voyage AI to generate an image embedding.
Adding the function to the Trigger
After creating the Trigger, the next step is to attach the Function that will run whenever the event occurs.
Inside the Trigger configuration screen:
Scroll down to the Event Type section.
Select Function as the action to execute.
-
Paste the complete Function code provided below into the editor.
- Remember to change the serviceName if necessary.
Save the Trigger to apply the Function.
From this point on, every time a new document is inserted into the target collection, MongoDB Atlas will automatically call your Function and run the code you defined.
Function code:
exports = async function(changeEvent) {
const fullDoc = changeEvent.fullDocument;
const serviceName = "Cluster0";
const db = context.services.get(serviceName).db(changeEvent.ns.db);
const collection = db.collection(changeEvent.ns.coll);
async function setError(message) {
console.error("[Embedding ERROR]", message);
const id = fullDoc && fullDoc._id ? fullDoc._id : changeEvent.documentKey._id;
try {
await collection.updateOne(
{ _id: id },
{ $set: { embeddingError: message } }
);
} catch (e) {
console.error("Failed to persist embeddingError:", e.message);
}
}
if (!fullDoc || !fullDoc.cover) {
await setError("No image URL found in 'cover' field");
return;
}
const imageUrl = fullDoc.cover;
const requestBody = {
model: "voyage-multimodal-3",
inputs: [
{
content: [
{
type: "image_url",
image_url: imageUrl
}
]
}
]
};
try {
const key = context.values.get("Value-0");
const response = await context.http.post({
url: "https://api.voyageai.com/v1/multimodalembeddings",
headers: {
"Content-Type": ["application/json"],
"Authorization": [`Bearer ${key}`]
},
body: JSON.stringify(requestBody)
});
const status = response.statusCode;
const rawBody = response.body.text();
console.log("Voyage STATUS:", status);
if (status !== 200) {
await setError(`Voyage returned status ${status}: ${rawBody}`);
return;
}
let result;
try {
result = JSON.parse(rawBody);
} catch (e) {
await setError(`JSON.parse failed: ${e.message}`);
return;
}
console.log("Voyage PARSED:", JSON.stringify(result));
if (!result.data || !Array.isArray(result.data)) {
await setError("Voyage response has no data[] field");
return;
}
if (!result.data[0] || !result.data[0].embedding) {
await setError("Voyage response has no data[0].embedding");
return;
}
const embedding = result.data[0].embedding;
try {
await collection.updateOne(
{ _id: fullDoc._id },
{
$set: { embedding: embedding },
$unset: { embeddingError: "" }
}
);
console.log("Embedding saved successfully!");
} catch (e) {
await setError(`Failed to update document with embedding: ${e.message}`);
}
} catch (err) {
await setError(`Unexpected error calling Voyage: ${err.message}`);
}
};
What this Function does
Runs automatically whenever a new document is inserted
Reads the document’s cover field to get the image URL
Sends the image to Voyage AI’s multimodal model, which generates an embedding from the image
Saves the resulting embedding back into the same document
If something goes wrong, writes the error into an embeddingError field for easy debugging
Uses a Voyage AI API key (we’ll configure this key next using an Atlas Value)
Configuring the Voyage AI API Key
Before the Function can call Voyage AI to generate embeddings, we need to store the API key securely inside MongoDB Atlas. We do this using Secrets and Values in App Services.
Follow these steps:
-
Open App Services
- In MongoDB Atlas, go to Project → Triggers → Linked App Services.
-
Go to Values & Secrets
- In the App Services sidebar, open Values.
-
Create a Secret
- Click Create Secret
- Name it: VOYAGE_API_KEY
- Paste your VoyageAI API key
- Save
-
Create a Value
- Click Create Value
- Name it: Value-0
- Choose Value Type: Secret
- Click Link to Secret
- Select VOYAGE_API_KEY
Once this is done, your Function can safely access the key using:
const key = context.values.get("Value-0");
This keeps your API key protected and out of the application code.
Creating the vector search index
Now that each document contains an embedding, we need to create a vector search index so MongoDB can efficiently compare those vectors.
To create the index:
In the Project Overview page of MongoDB Atlas, open Search & Vector Search.
Click Create Search Index.
Choose Vector Search as the index type.
Select your cluster, then choose the database and the articles collection.
Replace the index definition with the following configuration:
{
"fields": [
{
"numDimensions": 1024,
"path": "embedding",
"similarity": "dotProduct",
"type": "vector"
}
]
}
Click Next, then Create Index.
Once this index is created, your collection is ready to perform fast and accurate image similarity queries using $vectorSearch.
Testing everything end-to-end
With the Trigger, Function, and vector search index in place, everything is ready. Let’s confirm that the pipeline works as expected by inserting a new document into the articles collection.
1. Insert a new article
Run the following insert in your Atlas Cluster:
db.articles.insertOne({
"title": "Clean and Modular Java: A Hexagonal Architecture Approach",
"tags": [
"Java", "Architecture"
],
"publishedAt": "10-23-2025",
"authors": [
"Ricardo Mello"
],
"url": "https://foojay.io/today/clean-and-modular-java-a-hexagonal-architecture-approach/",
"views": 17864,
"cover": "https://m.media-amazon.com/images/I/71stxGw9JgL._SL1500_.jpg"
})
The moment this document is inserted, your database Trigger fires and executes the Function you configured.
- The Function reads the cover URL.
- It sends it to Voyage AI’s multimodal model.
- It writes the resulting embedding back into the document.
- If something goes wrong, you’ll instead see an embeddingError field showing exactly what failed. Useful for debugging.
After a few seconds, check the document again and you should see:
"embedding": [0.018554688,… ]
Now, we’re ready to run similarity search.
2. Generate an embedding for the query image
To search for similar images, you first need an embedding for the query image, the one the user wants to compare against your dataset. In a real application, this embedding is usually generated by the application itself. For this example, we’ll generate it manually using a simple curl command:
curl --location 'https://api.voyageai.com/v1/multimodalembeddings' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <YOUR_VOYAGE_API_KEY>' \
--data '{
"inputs": [
{
"content": [
{
"type": "image_url",
"image_url": "https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg"
}
]
}
],
"model": "voyage-multimodal-3"
}'
This returns an embedding vector. Copy that array and use it in queryVector to find the most similar articles based on the query embedding:
[
{
$vectorSearch:
{
path: "embedding",
numCandidates: 150,
index: "vector_index",
limit: 10,
queryVector: [
-0.003646851
...
],
}
}
]
MongoDB will compute similarity using the vector index and return the closest matches in the articles collection, effectively giving you image similarity search inside your database.
Conclusion
At the end of the day, the most important takeaway is the core idea behind this entire workflow: image similarity search. Whether you build it with Java, Node.js, Python, or any other language, the concept remains the same: Generate embeddings, store them, and compare vectors to find what looks alike.
In this walkthrough, we intentionally used native Atlas features like Triggers, Functions, and Vector Search to show that you don’t always need a full backend to experiment with AI-driven
For more information about MongoDB and everything it can do, check out the official documentation.


Top comments (0)