# MongoDB Atlas Vector Search and AWS Bedrock modules RAG tutorial

Welcome to our in-depth tutorial on MongoDB Atlas Vector Search and AWS Bedrock modules, tailored for creating a versatile database assistant for product catalogs. This tutorial will guide you through building an application that simplifies product searches using diverse inputs such as individual products, lists, images, and even recipes. Imagine finding all the necessary ingredients for a recipe with just a simple search. Whether you're a developer or a product manager, this guide will equip you with the skills to create a powerful tool for navigating complex product databases.
Some examples of what this application can do:

### Single product search:

**Search query**: `"Organic Almonds"` 

**Result**: displays the top-rated or most popular organic almond product in the catalog

### List-based search:
**Search query**: `"Rice", "Black Beans", "Avocado"]`

**Result**: shows a list of products including rice, black beans, and avocados, along with their different brands and prices

### Image-based search:
**Search query**: [image of a whole wheat bread loaf]

**Result**: identifies and shows the top-picked brand of whole wheat bread available in the catalog

### Recipe-based search:
**Search query**: `"Chocolate Chip Cookie Recipe"`

**Result**: lists all ingredients needed for the recipe, like flour, chocolate chips, sugar, butter, etc., and suggests relevant products

![Demo Application Search Functionality

Let’s start!

## High-level architecture

1\. Frontend VUE js application implementing a chat application

2\. Trigger:

 - A trigger watching for inserted “product” documents and using a function logic sets vector embeddings on the product “title,” “img,” or both.

 3\. App services to facilitate a backend hosting the endpoints to interact with the database and AI models
 - **getSearch** — the main search engine point that receives a search string or base64 image and outputs a summarized document
 - **getChats** — an endpoint to retrieve user chats array
 - **saveChats** — an endpoint to save the chats array

 4\. MongoDB Atlas database with a vector search index to retrieve relevant documents for RAG
enter image description here

## Deploy a free cluster

Before moving forward, ensure the following prerequisites are met:

- Database cluster setup on MongoDB Atlas
- Obtained the URI to your cluster

For assistance with database cluster setup and obtaining the URI, refer to our guide for setting up a MongoDB cluster, and our guide to get your connection string.

Preferably the database location will be in the same AWS region as the Bedrock enabled modules.

MongoDB Atlas has a rich set of application services that allow a developer to host an entire application logic (authentication, permissions, functions, triggers, etc.) with a generous free tier.
We will leverage this ability to streamline development and data integration in minutes of work.

## Setup app services

1\. Start by navigating to the App Services tab.

2\. You’ll be prompted to select a starter template. Let’s go with the **Build your own App** option that’s already selected. Click the **Next** button.

3\. Next, you need to configure your application.

- Data Source: Since we have created a single cluster, Atlas already linked it to our application.
- (Optional) Application Name: Let’s give our application a meaningful name, such as bedrockDemo. (This option might be chosen for you automatically as "Application-0" for the first application.)
- (Optional) App Deployment Model: Change the deployment to Single Region and select the region closest to your physical location.

4\. Click the **Create App Service** button to create your first App Services application!

5\. Once the application is created, we need to verify data sources are linked to our cluster. Visit the **Linked Data Sources** tab:
Our Atlas cluster with a linked name of `mongodb-atlas`

## Setup secrets and trigger

We will use the app services to create a Value and a Secret for AWS access and secret keys to access our Bedrock modules.

Navigate to the **Values** tab and click **Create New Value** by following this configuration:
| **Value Type** | **Name** | **Value** |
| --- | --- | --- |
| Secret | AWS_ACCESS_KEY| ``|
| Secret | AWS_SECRET_KEY | ``|
| Value | AWS_ACCESS_KEY| Link to SECRET: AWS_ACCESS_KEY|
| Value | AWS_SECRET_KEY | Link to SECRET: AWS_SECRET_KEY|

By the end of this process you should have:

Once done, press **Review Draft & Deploy** and then **Deploy**.

### Add aws sdk dependency
The AWS SDK Bedrock client  is the easiest and most convenient way to interact with AWS bedrock models.

 1\. In your app services application, navigate to the **Functions** tab and click the **Dependencies** tab.

 2\. Click **Add Dependency** and add the following dependency:
```
@aws-sdk/client-bedrock-runtime
```
 3\. Click **Add** and wait for it to be successfully added.

 4\. Once done, press **Review Draft & Deploy** and then **Deploy**.

### Create a trigger
Navigate to **Triggers** tab and create a new trigger:

**Trigger Code**

Choose **Function type** and in the dropdown, click **New Function.** Add a name like setEmbeddings under **Function Name**.

Copy and paste the following code.
```javascript
// Header: MongoDB Atlas Function to Process Document Changes
// Inputs: MongoDB changeEvent object
// Outputs: Updates the MongoDB document with processing status and AWS model response

exports = async function(changeEvent) {
 // Connect to MongoDB service
 var serviceName = "mongodb-atlas";
 var dbName = changeEvent.ns.db;
 var collName = changeEvent.ns.coll;

 try {
   var collection = context.services.get(serviceName).db(dbName).collection(collName);

   // Set document status to 'pending'
   await collection.updateOne({'_id' : changeEvent.fullDocument._id}, {$set : {processing : 'pending'}});

   // AWS SDK setup for invoking models
   const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime");
   const client = new BedrockRuntimeClient({
     region: 'us-east-1',
     credentials: {
       accessKeyId:  context.values.get('AWS_ACCESS_KEY'),
       secretAccessKey: context.values.get('AWS_SECRET_KEY')
     },
     model: "amazon.titan-embed-text-v1",
   });

   // Prepare embedding input from the change event
   let embedInput = {}
   if (changeEvent.fullDocument.title) {
     embedInput'inputText'] = changeEvent.fullDocument.title
   }
   if (changeEvent.fullDocument.imgUrl) {
     const imageResponse = await context.http.get({ url: changeEvent.fullDocument.imgUrl });
     const imageBase64 = imageResponse.body.toBase64();
     embedInput['inputImage'] = imageBase64
   }

   // AWS SDK call to process the embedding
   const input = {
     "modelId": "amazon.titan-embed-image-v1",
     "contentType": "application/json",
     "accept": "*/*",
     "body": JSON.stringify(embedInput)
   };

   console.log(`before model invoke ${JSON.stringify(input)}`);
   const command = new InvokeModelCommand(input);
   const response = await client.send(command);
    
   // Parse and update the document with the response
   const doc = JSON.parse(Buffer.from(response.body));
   doc.processing = 'completed';
   await collection.updateOne({'_id' : changeEvent.fullDocument._id}, {$set : doc});

 } catch(err) {
   // Handle any errors in the process
   console.error(err)
 }
};
```
Click **Save** and **Review Draft & Deploy**.

Now, we need to set the function setEmbeddings as a SYSTEM function. Click on the Functions tab and then click on the **setEmbeddings** function, **Settings** tab. Change the Authentication to **System** and click **Save**.

![System setting on a function

A trigger running successfully will produce a collection in our Atlas cluster. You can navigate to **Data Services > Database**. Click the **Browse Collections** button on the cluster view. The database name is Bedrock and the collection is `products`.

> Please note that the trigger run will only happen when we insert data into the `bedrock.products` collection and might take a while the first time. Therefore, you can watch the Logs section on the App Services side.

## Create an Atlas Vector Search index

Let’s move back to the **Data Services** and **Database** tabs.

**Atlas search index creation **
1. First, navigate to your cluster’s "Atlas Search" section and press the Create Index button.

1. Click **Create Search Index**.
2. Choose the Atlas Vector Search index and click **Next**.
3. Select the "bedrock" database and "products" collection.
4. Paste the following index definition:
```
{
 "fields": 
   {
     "type": "vector",
     "path": "embedding",
     "numDimensions": 1024,
     "similarity": "dotProduct"
   }
 ]
}
```
1. Click **Create** and wait for the index to be created.
2. The index is going to go through a build phase and will appear "Active" eventually.
Now, you are ready to write $search aggregations for Atlas Search.

The HTTP endpoint getSearch implemented in Chapter 3 already includes a search query.

```
const items = await collection.aggregate([
     {
       "$vectorSearch": {
         "queryVector": doc.embedding,
         "index": "vector_index",
         "path": "embedding",
         "numCandidates": 15,
         "limit": 1
       }
     },
     {"$project": {"embedding": 0}}
   ]).toArray();
```
With this code, we are performing a vector search with whatever is placed in the "doc.embedding" variable on fields "embedding." We look for just one document’s results and limit the set for the first one.

## Set up the backend logic

Our main functionality will rely on a user HTTP endpoint which will orchestrate the logic of the catalog search. The input from the user will be turned into a multimodal embedding via AWS Titan and will be passed to Atlas Vector Search to find the relevant document. The document will be returned to the user along with a prompt that will engineer a response from a Cohere LLM.

> Cohere LLM `cohere.command-light-text-v14` is part of the AWS Bedrock base model suite.

### Create application search HTTPS endpoint
1. On the App Services application, navigate to the **HTTPS Endpoints** section.
2. Create a new POST endpoint by clicking **Add An Endpoint** with a path of **/getSearch**.
3. Important! Toggle the **Response With Result** to On.
4. The logic of this endpoint will get a "term" from the query string and search for that term. If no term is provided, it will return the first 15 results.

![getSearch endpoint
5. Add under Function and New Function (name: getProducts) the following function logic:

```javascript
// Function Name : getProducts

exports = async function({ body }, response) {

 // Import required SDKs and initialize AWS BedrockRuntimeClient
 const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime");
 const client = new BedrockRuntimeClient({
   region: 'us-east-1',
   credentials: {
     accessKeyId: context.values.get('AWS_ACCESS_KEY'),
     secretAccessKey: context.values.get('AWS_SECRET_KEY')
   }
 });

 // MongoDB and AWS SDK service setup
 const serviceName = "mongodb-atlas";
 const dbName = "bedrock";
 const collName = "products";
 const collection = context.services.get(serviceName).db(dbName).collection(collName);

 // Function to run AWS model command
 async function runModel(command, body) {
   command.body = JSON.stringify(body);
    console.log(`before running ${command.modelId} and prompt ${body.prompt}`)
   const listCmd = new InvokeModelCommand(command);
   console.log(`after running ${command.modelId} and prompt ${body.prompt}`)
   const listResponse = await client.send(listCmd);
   console.log('model body ret', JSON.stringify(JSON.parse(Buffer.from(listResponse.body))))
   console.log('before return from runModel')
   return JSON.parse(Buffer.from(listResponse.body));
 }

 // Function to generate list query for text input
function generateListQuery(text) {
 const listDescPrompt = `Please build a json only output start with: {productList : {"product" : "" , "quantity" : }]} stop output after json fully generated.
     The list for ${text}. Complete {productList : `;
 return {
   "prompt": listDescPrompt,
   "temperature": 0
 };
}

// Function to process list items
async function processListItems(productList, embedCmd) {
 let retDocuments = [];
 for (const product of productList) {
   console.log('product', JSON.stringify(product))
   const embedBody = { 'inputText': product.product };
   const resEmbedding = await runModel(embedCmd, embedBody);
   const items = await collection.aggregate([
     vectorSearchQuery(resEmbedding.embedding), {"$project" : {"embedding" : 0}}
   ]).toArray();
   retDocuments.push(items[0]);
 }
 return retDocuments;
}

// Function to process a single item
async function processSingleItem(doc) {
 const items = await collection.aggregate([
   vectorSearchQuery(doc.embedding), {"$project" : {"embedding" : 0}}]).toArray();
 return items;
}

// Function to create vector search query
function vectorSearchQuery(embedding) {
 return {
   "$vectorSearch": {
     "queryVector": embedding,
     "index": "vector_index",
     "path": "embedding",
     "numCandidates": 15,
     "limit": 1
   }
 };
}

 // Parsing input data
 const { image, text } = JSON.parse(body.text());

 try {
   let embedCmd = {
     "modelId": "amazon.titan-embed-image-v1",
     "contentType": "application/json",
     "accept": "*/*"
   };
  
   // Process text input
   if (text) {
     const genList = generateListQuery(text);
     const listResult = await runModel({ "modelId": "cohere.command-light-text-v14", "contentType": "application/json",
     "accept": "*/*" }, genList);
     const list = JSON.parse(listResult.generations[0].text);
     console.log('list', JSON.stringify(list));

     let retDocuments = await processListItems(list.productList, embedCmd);
     console.log('retDocuments', JSON.stringify(retDocuments));
      let prompt, success = true;
      prompt = `In one simple sentence explain how the retrieved docs: ${JSON.stringify(retDocuments)}
      and mention the searched ingridiants from list: ${JSON.stringify(list.productList)} `;

     // Generate text based on the prompt
        genQuery = {
         "prompt": prompt,
         "temperature": 0
       };
  
        textGenInput = {
         "modelId": "cohere.command-light-text-v14",
         "contentType": "application/json",
         "accept": "*/*"
       };
    
       const assistantResponse = await runModel(textGenInput, genQuery);
       console.log('assistant', JSON.stringify(assistantResponse));
        retDocuments[0].assistant = assistantResponse.generations[0].text;
    
     return retDocuments;
   }

   // Process image or other inputs
   if (image) {
     const doc = await runModel(embedCmd, { inputImage: image });
     return await processSingleItem(doc);
   }

 } catch (err) {
   console.error("Error: ", err);
   throw err;
 }
};

```
Click **Save Draft** and follow the **Review Draft & Deploy** process.
Make sure to keep the http callback URL as we will use it in our final chapter when consuming the data from the frontend application.
> TIP:
>
> The URL will usually look something like: `https://us-east-1.aws.data.mongodb-api.com/app//endpoint/getSearch`

Make sure that the function created (e.g., getProducts) is on "SYSTEM" privilege for this demo.

This page can be accessed by going to the **Functions** tab and looking at the **Settings** tab of the relevant function.

### Import data into Atlas
Now, we will import the data into Atlas from our [github repo.
1. On the **Data Services** main tab, click your cluster name.
Click the **Collections** tab.
2. We will start by going into the "bedrock" database and importing the "products" collection.
3. Click **Insert Document** or **Add My Own Data** (if present) and switch to the document view. Paste the content of the "products.json" file from the "data" folder in the repository.
4. Click Insert and wait for the data to be imported. 

### Create an endpoint to save and retrieve chats
1\. `/getChats` - will save a chat to the database
Endpoint
- Name: getChats
- Path: /getChats
- Method: GET
- Response with Result: Yes
``` javascript
// This function is the endpoint's request handler.
exports = async function({ query, headers, body}, response) {
   // Data can be extracted from the request as follows:

   const {player } = query;

   // Querying a mongodb service:
    const doc = await context.services.get("mongodb-atlas").db("bedrock").collection("players").findOne({"player" : player}, {messages : 1})

   return doc;

};
```
2\. `/saveChats` — will save a chat to the database
Endpoint

- Name: saveChats
- Path: /saveChats
- Method: POST
- Response with Result: Yes
```javascript
// This function is the endpoint's request handler.
exports = async function({ query, headers, body}, response) {
   // Data can be extracted from the request as follows:

   // Headers, e.g. {"Content-Type": "application/json"]}
   const contentTypes = headers["Content-Type"];

   const {player , messages } = JSON.parse(body.text());

   // Querying a mongodb service:
   const doc = await context.services.get("mongodb-atlas").db("bedrock").collection("players").findOneAndUpdate({player : player}, {$set : {messages : messages}}, {returnNewDocument : true});
  

  return doc;
};
```
Make sure that all the functions created (e.g., registerUser) are on "SYSTEM" privilege for this demo.

![System setting on a function
This page can be accessed by going to the Functions tab and looking at the Settings tab of the relevant function.
Finally, click Save **Draft** and follow the **Review Draft & Deploy** process.

## GitHub Codespaces frontend setup

It’s time to test our back end and data services. We will use the created search HTTPS endpoint to show a simple search page on our data.

You will need to get the HTTPS Endpoint URL we created as part of the App Services setup.

### Play with the front end

We will use the github repo to launch codespaces from: 
1. Open the repo in GitHub.
2. Click the green **Code** button.
3. Click the **Codespaces** tab and **+** to create a new codespace.

### Configure the front end

1. Create a file called .env in the root of the project.
2. Add the following to the file:
```
VUE_APP_BASE_APP_SERVICE_URL=''
VUE_APP_SEARCH_ENDPOINT='getSearch'
VUE_APP_SAVE_CHATS_ENDPOINT='saveChats'
VUE_APP_GET_CHATS_ENDPOINT='getChats'
## Small chart to present possible products
VUE_APP_SIDE_IFRAME='https://charts.mongodb.com/charts-fsidemo-ubsdv/embed/charts?id=65a67383-010f-4c3d-81b7-7cf19ca7000b&maxDataAge=3600&theme=light&autoRefresh=true'
```
### Install the front end
```
npm install
```
Install serve.
```
npm install -g serve
```
### Build the front end
```
npm run build
```
### Run the front end
```
serve -s dist/
```
### Test the front end
Open the browser to the URL provided by serve in a popup.

## Summary 

In summary, this tutorial has equipped you with the technical know-how to leverage MongoDB Atlas Vector Search and AWS Bedrock for building a cutting-edge database assistant for product catalogs. We've delved deep into creating a robust application capable of handling a variety of search inputs, from simple text queries to more complex image and recipe-based searches. As developers and product managers, the skills and techniques explored here are crucial for innovating and improving database search functionalities. 

The combination of MongoDB Atlas and AWS Bedrock offers a powerful toolkit for efficiently navigating and managing complex product data. By integrating these technologies into your projects, you’re set to significantly enhance the user experience and streamline the data retrieval process, making every search query more intelligent and results more relevant. Embrace this technology fusion to push the boundaries of what’s possible in database search and management.

If you want to explore more about MongoDB  and AI please refer to our main landing page. 

Additionally, if you wish to communicate with our community please visit https://community.mongodb.com .