# Adding MongoDB Atlas Vector Search to a .NET Blazor C# Application

When was the last time you could remember the rough details of something but couldn’t remember the name of it? That happens to quite a few people, so being able to search semantically instead of with exact text searches is really important.

This is where MongoDB Atlas Vector Search comes in useful. It allows you to perform semantic searches against vector embeddings in your documents stored inside MongoDB Atlas. Because the embeddings are stored inside Atlas, you can create the embeddings against any type of data, both structured and unstructured.

In this tutorial, you will learn how to add vector search with MongoDB Atlas Vector Search, using the MongoDB C# driver, to a .NET Blazor application. The Blazor application uses the sample_mflix database, available in the sample dataset anyone can load into their Atlas cluster. You will add support for searching semantically against the plot field, to find any movies that might fit the plot entered into the search box.

## Prerequisites
In order to follow along with this tutorial, you will need a few things in place before you start:

 1. .NET 8 SDK installed on your computer
 2. An IDE or text editor that can support C# and Blazor for the most seamless development experience, such as Visual Studio, Visual Studio Code with the C# DevKit Extension installed, or JetBrains Rider
 3. An Atlas M0 cluster, our free forever tier, perfect for development
 4. Your cluster connection string
 5. A local copy of the Hugging Face Dataset Upload tool
 6. A fork and clone of the See Sharp Movies GitHub repo that we will be adding search to
 7. An OpenAI account and a free API key generated — you will use the OpenAI API to create a vector embedding for our search term

> Once you have forked and then cloned the repo and have it locally, you will need to add your connection string into ```appsettings.Development.json``` and ```appsettings.json``` in the placeholder section in order to connect to your cluster when running the project.

> If you don’t want to follow along, the repo has a branch called “vector-search” which has the final result implemented. However, you will need to ensure you have the embedded data in your Atlas cluster.

## Getting our embedded data into Atlas
The first thing you need is some data stored in your cluster that has vector embeddings available as a field in your documents. MongoDB has already provided a version of the movies collection from sample_mflix, called embedded_movies, which has 1500 documents, using a subset of the main movies collection which has been uploaded as a dataset to Hugging Face that will be used in this tutorial.

This is where the Hugging Face Dataset Uploader downloaded as part of the prerequisites comes in. By running this tool using ```dotnet run``` at the root of the project, and passing your connection string into the console when asked, it will go ahead and download the dataset from Hugging Face and then upload that into an ```embedded_movies``` collection inside the ```sample_mflix``` database. If you haven’t got the same dataset loaded so this database is missing, it will even just create it for you thanks to the C# driver!

You can generate vector embeddings for your own data using tools such as Hugging Face, OpenAI, LlamaIndex, and others. You can read more about generating embeddings using open-source models by reading a tutorial from Prakul Agarwal on Generative AI, Vector Search, and open-source models here on Developer Center.

## Creating the Vector Search index
Now you have a collection of movie documents with a ```plot_embedding``` field of vector embeddings for each document, it is time to create the Atlas Vector Search index. This is to enable vector search capabilities on the cluster and to let MongoDB know where to find the vector embeddings.

 1. Inside Atlas, click “Browse Collections” to open the data explorer to view your newly loaded sample_mflix database.
 2. Select the “Atlas Search” tab at the top.
 3. Click the green “Create Search Index” button to load the index creation wizard.
 4. Select JSON Editor under the Vector Search heading and then click “Next.”
 5. Select the embedded_movies collection under sample_mflix from the left.
 6. The name doesn’t matter hugely here, as long as you remember it for later but for now, leave it as the default value of ‘vector_index’.
 7. Copy and paste the following JSON in, replacing the current contents of the box in the wizard:

```json
{
  "fields": 
    {
      "type": "vector",
      "path": "plot_embedding",
      "numDimensions": 1536,
      "similarity": "dotProduct"
    }
  ]
}
``` 
This contains a few fields you might not have seen before.

 - path is the name of the field that contains the embeddings. In the case of the dataset from Hugging Face, this is plot_embedding.
 - numDimensions refers to the dimensions of the model used.
 - similarity refers to the type of function used to find similar results.

Check out the [Atlas Vector Search documentation to learn more about these configuration fields.

Click “Next” and on the next page, click “Create Search Index.”

After a couple of minutes, the vector search index will be set up, you will be notified by email, and the application will be ready to have vector search added.

## Adding the backend functionality
You have the data with plot embeddings and a vector search index created against that field, so it is time to start work on the application to add search, starting with the backend functionality.
### Adding OpenAI API key to appsettings
The OpenAI API key will be used to request embeddings from the API for the search term entered since vector search understands numbers and not text. For this reason, the application needs your OpenAI API key to be stored for use later. 

 1. Add the following into the root of your ```appsettings.Development.json``` and ```appsettings.json```, after the MongoDB section, replacing the placeholder text with your own key:
```json
"OpenAPIKey": ""
```
 2. Inside ```program.cs```, after the creation of the var builder, add the following line of code to pull in the value from app config:

```csharp
var openAPIKey = builder.Configuration.GetValue("OpenAPIKey");
```
 3. Change the code that creates the MongoDBService instance to also pass in the ```openAPIKey variable```. You will change the constructor of the class later to make use of this.

```csharp
builder.Services.AddScoped(service => new MongoDBService(mongoDBSettings, openAPIKey));
```

### Adding a new method to IMongoDBService.cs
You will need to add a new method to the interface that supports search, taking in the term to be searched against and returning a list of movies that were found from the search.

Open ```IMongoDBService.cs``` and add the following code:

```csharp
public IEnumerable MovieSearch(string textToSearch);
```
### Implementing the method in MongoDBService.cs
Now to make the changes to the implementation class to support the search.

 1. Open ```MongoDBService.cs``` and add the following using statements to the top of the file:
```csharp
using System.Text; 
using System.Text.Json;
```
 2. Add the following new local variables below the existing ones at the top of the class:
```csharp
  private readonly string _openAPIKey;
  private readonly HttpClient _httpClient = new HttpClient();
```
 3. Update the constructor to take the new openAPIKey string parameter, as well as the MongoDBSettings parameter. It should look like this:
```csharp
public MongoDBService(MongoDBSettings settings, string openAPIKey)
```
 4. Inside the constructor, add a new line to assign the value of openAPIKey to _openAPIKey.
 5. Also inside the constructor, update the collection name from “movies” to “embedded_movies” where it calls ```.GetCollection```.

The following is what the completed constructor should look like:
```csharp
public MongoDBService(MongoDBSettings settings, string openAPIKey)
{
    _client = new MongoClient(settings.AtlasURI);
    _mongoDatabase = _client.GetDatabase(settings.DatabaseName);
    _movies = _mongoDatabase.GetCollection("embedded_movies");
    _openAPIKey = openAPIKey;
}
```
### Updating the Movie model
The C# driver acts as an object document mapper (ODM), taking care of mapping between a plain old C# object (POCO) that is used in C# and the documents in your collection.

However, the existing movie model fields need updating to match the documents inside your embedded_movies collection.

Replace the contents of  ```Models/Movie.cs``` with the following code:

```csharp
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace SeeSharpMovies.Models;

public class Movie
{
   BsonId]
   [BsonElement("_id")]
   public ObjectId Id { get; set; }
   
   [BsonElement("plot")]
   public string Plot { get; set; }

   [BsonElement("genres")]  
   public string[] Genres { get; set; }

   [BsonElement("runtime")]
   public int Runtime { get; set; }

   [BsonElement("cast")]
   public string[] Cast { get; set; }

   [BsonElement("num_mflix_comments")]
   public int NumMflixComments { get; set; }

   [BsonElement("poster")]
   public string Poster { get; set; }

   [BsonElement("title")]
   public string Title { get; set; }

   [BsonElement("fullplot")]
   public string FullPlot { get; set; }

   [BsonElement("languages")]
   public string[] Languages { get; set; }

   [BsonElement("directors")]
   public string[] Directors { get; set; }

   [BsonElement("writers")]
   public string[] Writers { get; set; }

   [BsonElement("awards")]
   public Awards Awards { get; set; }
   
   [BsonElement("year")]
   public string Year { get; set; }

   [BsonElement("imdb")]
   public Imdb Imdb { get; set; }

   [BsonElement("countries")]
   public string[] Countries { get; set; }

   [BsonElement("type")]
   public string Type { get; set; }

   [BsonElement("plot_embedding")]
   public float[] PlotEmbedding { get; set; }

}

public class Awards
{
   [BsonElement("wins")]
   public int Wins { get; set; }
   
   [BsonElement("nominations")]
   public int Nominations { get; set; }
   
   [BsonElement("text")]
   public string Text { get; set; }
}

public class Imdb
{
   [BsonElement("rating")]
   public float Rating { get; set; }
   
   [BsonElement("votes")]
   public int Votes { get; set; }
   
   [BsonElement("id")]
   public int Id { get; set; }
}
```

This contains properties for all the fields in the document, as well as classes and properties representing subdocuments found inside the movie document, such as “critic.” You will also note the use of the BsonElement attribute, which tells the driver how to map between the field names and the property names due to their differing naming conventions.

### Adding an EmbeddingResponse model
It is almost time to start implementing the search on the back end. When calling the OpenAI API’s embedding endpoint, you will get back a lot of data, including the embeddings. The easiest way to handle this is to create an EmbeddingResponse.cs class that models this response for use later.

Add a new class called EmbeddingResponse inside the Model folder and replace the contents of the file with the following:

```csharp
namespace SeeSharpMovies.Models
{
    public class EmbeddingResponse
    {
        public string @object { get; set; }
        public List data { get; set; }
        public string model { get; set; }
        public Usage usage { get; set; }
    }

    public class Data
    {
        public string @object { get; set; }
        public int index { get; set; }
        public List embedding { get; set; }
    }

    public class Usage
    {
        public int prompt_tokens { get; set; }
        public int total_tokens { get; set; }
    }
}
```
### Adding a method to request embeddings for the search term
It is time to make use of the API key for OpenAI and write functionality to create vector embeddings for the searched term by calling the [OpenAI API Embeddings endpoint.

Inside ```MongoDBService.cs```, add the following code:

```csharp
private async Task> GetEmbeddingsFromText(string text)
{
    Dictionary body = new Dictionary
    {
        { "model", "text-embedding-ada-002" },
        { "input", text }
    };

    _httpClient.BaseAddress = new Uri("https://api.openai.com");
    _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_openAPIKey}");

    string requestBody = JsonSerializer.Serialize(body);
    StringContent requestContent =
        new StringContent(requestBody, Encoding.UTF8, "application/json");

    var response = await _httpClient.PostAsync("/v1/embeddings", requestContent)
        .ConfigureAwait(false);

    if (response.IsSuccessStatusCode)
    {
        string responseBody = await response.Content.ReadAsStringAsync();
        EmbeddingResponse embeddingResponse = JsonSerializer.Deserialize(responseBody);
        return embeddingResponse.data0].embedding;
    }

    return new List();
}
```

The body dictionary is needed by the API to know the model used and what the input is. The text-embedding-ada-002 model is the default text embedding model.

### Implementing the SearchMovie function
The GetEmbeddingsFromText method returned the embeddings for the search term, so now it is available to be used by Atlas Vector Search and the C# driver.

Paste the following code to implement the search:

```csharp
public IEnumerable MovieSearch(string textToSearch)
{

    var vector = GetEmbeddingsFromText(textToSearch).Result.ToArray();

    var vectorOptions = new VectorSearchOptions()
    {
        IndexName = "vector_index",
        NumberOfCandidates = 150
    };

  var movies = _movies.Aggregate()
    .VectorSearch(movie => movie.PlotEmbedding, vector, 150, vectorOptions)
    .Project(Builders.Projection
    .Include(m => m.Title)
    .Include(m => m.Plot)
    .Include(m => m.Poster))            
    .ToList();

    return movies;
}
```

> If you chose a different name when creating the vector search index earlier, make sure to update this line inside vectorOptions.

Vector search is available inside the C# driver as part of the aggregation pipeline. It takes four arguments: the field name with the embeddings, the vector embeddings of the searched term, the number of results to return, and the vector options.

Further methods are then chained on to specify what fields to return from the resulting documents.

Because the movie document has changed slightly, the current code inside the ```GetMovieById``` method is no longer correct.

Replace the current line that calls ```.Find``` with the following:

```csharp
 var movie = _movies.Find(movie => movie.Id.ToString() == id).FirstOrDefault();
```

The back end is now complete and it is time to move on to the front end, adding the ability to search on the UI and sending that search back to the code we just wrote.

## Adding the frontend functionality
The frontend functionality will be split into two parts: the code in the front end for talking to the back end, and the search bar in HTML for typing into.
### Adding the code to handle search
As this is an existing application, there is already code available for pulling down the movies and even pagination. This is where you will be adding the search functionality, and it can be found inside ```Home.razor``` in the ```Components/Pages``` folder.

 1. Inside the ```@code``` block,  add a new string variable for searchTerm:
```csharp
 string searchTerm;
```
 2. Paste the following new method into the code block:
```csharp
private void SearchMovies()
{
    if (string.IsNullOrWhiteSpace(searchTerm))
    {
        movies = MongoDBService.GetAllMovies();
    }
    else
    {
        movies = MongoDBService.MovieSearch(searchTerm);
    }
}
```
This is quite straightforward. If the searchTerm string is empty, then show everything. Otherwise, search on it.

### Adding the search bar
Adding the search bar is really simple. It will be added to the header component already present on the home page.

Replace the existing header tag with the following HTML:

```html

        See Sharp Movies
    
        
        Search
    
    
```

This creates a search input with the value being bound to the searchTerm string and a button that, when clicked, calls the SearchMovies method you just called.

### Making the search bar look nicer
At this point, the functionality is implemented. But if you ran it now, the search bar would be in a strange place in the header, so let’s fix that, just for prettiness.

Inside ```wwwroot/app.css```, add the following code:

```css
.search-bar {
    padding: 5%;
}

.search-bar button {
    padding: 4px;
}
```

This just gives the search bar and the button a bit of padding to make it position more nicely within the header. Although it’s not perfect, CSS is definitely not my strong suit. C# is my favorite language!

## Testing the search
Woohoo! We have the backend and frontend functionality implemented, so now it is time to run the application and see it in action!

Run the application, enter a search term in the box, click the “Search” button, and see what movies have plots semantically close to your search term.

![Showing movie results with a plot similar to three young men and a sword

## Summary
Amazing! You now have a working Blazor application with the ability to search the plot by meaning instead of exact text. This is also a great starting point for implementing more vector search capabilities into your application.

If you want to learn more about Atlas Vector Search, you can read our documentation.
MongoDB also has a space on Hugging Face where you can see some further examples of what can be done and even play with it. Give it a go!

There is also an amazing article on using Vector Search for audio co-written by Lead Developer Advocate at MongoDB Pavel Duchovny.

If you have questions or feedback, join us in the Community Forums.