Optimizing Vector Search in .NET Applications with Azure AI Search

Summary: This post explores how to implement and optimize vector search in .NET applications using Azure AI Search. Learn about vector embeddings, similarity search algorithms, and best practices for building high-performance semantic search experiences.

Introduction

Vector search has revolutionized how we implement search functionality in modern applications. By representing content as numerical vectors (embeddings) and searching based on semantic similarity rather than keyword matching, vector search enables more intuitive and powerful search experiences.

In this post, we’ll explore how to implement and optimize vector search in .NET applications using Azure AI Search. We’ll cover everything from generating vector embeddings to configuring search indexes, implementing efficient search queries, and optimizing performance. By the end of this article, you’ll have the knowledge to build high-performance semantic search experiences in your .NET applications.

Understanding Vector Search

Before diving into implementation details, let’s understand the key concepts behind vector search.

What is Vector Search?

Vector search is a technique that enables searching for content based on semantic similarity rather than exact keyword matching. It works by:

  1. Converting content (text, images, etc.) into numerical vectors (embeddings) using AI models
  2. Storing these vectors in a specialized index
  3. Finding similar content by calculating the distance between vectors

Unlike traditional keyword search, vector search can understand the meaning and context of queries, leading to more relevant results.

Key Concepts in Vector Search

To effectively implement vector search, it’s important to understand these key concepts:

  • Embeddings: Numerical representations of content that capture semantic meaning
  • Vector Dimensions: The number of values in each vector (typically 768-1536 for text)
  • Similarity Metrics: Methods to measure distance between vectors (cosine similarity, Euclidean distance, etc.)
  • Approximate Nearest Neighbor (ANN): Algorithms that efficiently find similar vectors
  • Hybrid Search: Combining vector search with traditional keyword search

Benefits of Vector Search

Vector search offers several advantages over traditional search methods:

  1. Semantic Understanding: Captures meaning beyond keywords
  2. Multilingual Support: Works across languages with minimal configuration
  3. Handling Synonyms: Automatically understands related terms
  4. Query Flexibility: Handles natural language queries effectively
  5. Multimodal Capabilities: Can search across text, images, and other content types

Setting Up Azure AI Search for Vector Search

Let’s start by setting up Azure AI Search for vector search capabilities.

Prerequisites

To follow along with this tutorial, you’ll need:

  • An Azure subscription
  • Visual Studio 2022 or Visual Studio Code
  • .NET 8 SDK
  • An Azure OpenAI resource (for generating embeddings)
  • An Azure AI Search resource

Creating an Azure AI Search Resource

First, let’s create an Azure AI Search resource:

  1. Go to the Azure portal (https://portal.azure.com )
  2. Click “Create a resource” and search for “Azure AI Search”
  3. Click “Create”
  4. Fill in the required details:
    • Subscription: Your Azure subscription
    • Resource group: Create new or select existing
    • Service name: A unique name for your search service
    • Location: Choose a region close to your users
    • Pricing tier: Standard or higher (vector search requires Standard S1 or above)
  5. Click “Review + create” and then “Create”

Creating a .NET Project

Let’s create a new .NET project to work with Azure AI Search:

bash

dotnet new webapi -n VectorSearchDemo
cd VectorSearchDemo

Installing Required Packages

Add the necessary packages to your project:

bash

dotnet add package Azure.Search.Documents
dotnet add package Azure.AI.OpenAI
dotnet add package Microsoft.Extensions.Azure
dotnet add package Microsoft.Extensions.Configuration.Json

Configuring Azure Services

Let’s set up the configuration for our Azure services:

json

// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AzureAISearch": {
    "Endpoint": "https://your-search-service.search.windows.net",
    "ApiKey": "your-search-api-key",
    "IndexName": "vector-search-index"
  },
  "AzureOpenAI": {
    "Endpoint": "https://your-openai-service.openai.azure.com/",
    "ApiKey": "your-openai-api-key",
    "EmbeddingDeployment": "text-embedding-ada-002"
  }
}

Registering Services

Register the Azure services in your Program.cs file:

csharp

// Program.cs
using Azure;
using Azure.AI.OpenAI;
using Azure.Search.Documents;
using Microsoft.Extensions.Azure;

var builder = WebApplication.CreateBuilder(args );

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Add Azure clients
builder.Services.AddAzureClients(clientBuilder =>
{
    // Add Azure AI Search client
    clientBuilder.AddSearchClient(
        new Uri(builder.Configuration["AzureAISearch:Endpoint"]),
        builder.Configuration["AzureAISearch:IndexName"],
        new AzureKeyCredential(builder.Configuration["AzureAISearch:ApiKey"]));
    
    // Add Azure OpenAI client
    clientBuilder.AddOpenAIClient(
        new Uri(builder.Configuration["AzureOpenAI:Endpoint"]),
        new AzureKeyCredential(builder.Configuration["AzureOpenAI:ApiKey"]));
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Creating a Search Index with Vector Support

Now, let’s create a search index that supports vector search.

Defining the Document Model

First, let’s define a model for our documents:

csharp

// Models/Document.cs
using System.Text.Json.Serialization;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;

namespace VectorSearchDemo.Models
{
    public class Document
    {
        [SimpleField(IsKey = true)]
        public string Id { get; set; }
        
        [SearchableField]
        public string Title { get; set; }
        
        [SearchableField]
        public string Content { get; set; }
        
        [SearchableField]
        public string[] Categories { get; set; }
        
        [SimpleField]
        public DateTimeOffset CreatedAt { get; set; }
        
        [VectorSearchField(
            VectorSearchDimensions = 1536,
            VectorSearchProfileName = "myHnswProfile")]
        public float[] ContentVector { get; set; }
        
        [VectorSearchField(
            VectorSearchDimensions = 1536,
            VectorSearchProfileName = "myHnswProfile")]
        public float[] TitleVector { get; set; }
    }
}

Creating an Index Management Service

Let’s create a service to manage our search index:

csharp

// Services/SearchIndexService.cs
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using VectorSearchDemo.Models;

namespace VectorSearchDemo.Services
{
    public class SearchIndexService
    {
        private readonly SearchIndexClient _searchIndexClient;
        private readonly string _indexName;
        
        public SearchIndexService(SearchIndexClient searchIndexClient, IConfiguration configuration)
        {
            _searchIndexClient = searchIndexClient;
            _indexName = configuration["AzureAISearch:IndexName"];
        }
        
        public async Task CreateIndexAsync()
        {
            // Define vector search algorithm
            var hnswAlgorithmConfig = new HnswAlgorithmConfiguration("myHnswAlgorithm")
            {
                Parameters = new HnswParameters
                {
                    Metric = VectorSearchAlgorithmMetric.Cosine,
                    M = 4,
                    EfConstruction = 400,
                    EfSearch = 500
                }
            };
            
            // Define vector search profile
            var vectorSearchProfile = new VectorSearchProfile("myHnswProfile", "myHnswAlgorithm");
            
            // Create the index definition
            var indexDefinition = new SearchIndex(_indexName)
            {
                Fields = new FieldBuilder().Build(typeof(Document)),
                VectorSearch = new VectorSearch
                {
                    AlgorithmConfigurations = { hnswAlgorithmConfig },
                    Profiles = { vectorSearchProfile }
                },
                SemanticSearch = new SemanticSearch
                {
                    Configurations =
                    {
                        new SemanticConfiguration("default", new SemanticPrioritizedFields
                        {
                            TitleField = new SemanticField { FieldName = "Title" },
                            ContentFields =
                            {
                                new SemanticField { FieldName = "Content" }
                            },
                            KeywordsFields =
                            {
                                new SemanticField { FieldName = "Categories" }
                            }
                        })
                    }
                }
            };
            
            // Create the index
            await _searchIndexClient.CreateOrUpdateIndexAsync(indexDefinition);
        }
        
        public async Task DeleteIndexAsync()
        {
            await _searchIndexClient.DeleteIndexAsync(_indexName);
        }
    }
}

Creating a Controller to Manage the Index

Let’s create a controller to expose index management operations:

csharp

// Controllers/IndexController.cs
using Microsoft.AspNetCore.Mvc;
using VectorSearchDemo.Services;

namespace VectorSearchDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class IndexController : ControllerBase
    {
        private readonly SearchIndexService _indexService;
        
        public IndexController(SearchIndexService indexService)
        {
            _indexService = indexService;
        }
        
        [HttpPost]
        public async Task<IActionResult> CreateIndex()
        {
            await _indexService.CreateIndexAsync();
            return Ok("Index created successfully");
        }
        
        [HttpDelete]
        public async Task<IActionResult> DeleteIndex()
        {
            await _indexService.DeleteIndexAsync();
            return Ok("Index deleted successfully");
        }
    }
}

Generating Vector Embeddings

To perform vector search, we need to generate embeddings for our content. Let’s create a service to handle this.

Creating an Embedding Service

csharp

// Services/EmbeddingService.cs
using Azure;
using Azure.AI.OpenAI;

namespace VectorSearchDemo.Services
{
    public class EmbeddingService
    {
        private readonly OpenAIClient _openAIClient;
        private readonly string _embeddingDeployment;
        
        public EmbeddingService(OpenAIClient openAIClient, IConfiguration configuration)
        {
            _openAIClient = openAIClient;
            _embeddingDeployment = configuration["AzureOpenAI:EmbeddingDeployment"];
        }
        
        public async Task<float[]> GenerateEmbeddingsAsync(string text)
        {
            // Prepare the embedding request
            var embeddingOptions = new EmbeddingsOptions(_embeddingDeployment, new List<string> { text });
            
            // Generate embeddings
            var response = await _openAIClient.GetEmbeddingsAsync(embeddingOptions);
            
            // Return the embedding vector
            return response.Value.Data[0].Embedding.ToArray();
        }
        
        public async Task<IList<float[]>> GenerateEmbeddingsBatchAsync(IList<string> texts)
        {
            // Prepare the embedding request
            var embeddingOptions = new EmbeddingsOptions(_embeddingDeployment, texts);
            
            // Generate embeddings
            var response = await _openAIClient.GetEmbeddingsAsync(embeddingOptions);
            
            // Return the embedding vectors
            return response.Value.Data.Select(d => d.Embedding.ToArray()).ToList();
        }
    }
}

Indexing Documents with Vector Embeddings

Now, let’s create a service to index documents with vector embeddings.

Creating a Document Service

csharp

// Services/DocumentService.cs
using Azure;
using Azure.Search.Documents;
using VectorSearchDemo.Models;

namespace VectorSearchDemo.Services
{
    public class DocumentService
    {
        private readonly SearchClient _searchClient;
        private readonly EmbeddingService _embeddingService;
        
        public DocumentService(SearchClient searchClient, EmbeddingService embeddingService)
        {
            _searchClient = searchClient;
            _embeddingService = embeddingService;
        }
        
        public async Task<Document> IndexDocumentAsync(Document document)
        {
            // Generate embeddings for the document
            document.ContentVector = await _embeddingService.GenerateEmbeddingsAsync(document.Content);
            document.TitleVector = await _embeddingService.GenerateEmbeddingsAsync(document.Title);
            
            // Index the document
            var batch = IndexDocumentsBatch.Create(
                IndexDocumentsAction.Upload(document));
                
            await _searchClient.IndexDocumentsAsync(batch);
            
            return document;
        }
        
        public async Task<IList<Document>> IndexDocumentsAsync(IList<Document> documents)
        {
            // Generate embeddings for all documents in batches
            var titles = documents.Select(d => d.Title).ToList();
            var contents = documents.Select(d => d.Content).ToList();
            
            var titleVectors = await _embeddingService.GenerateEmbeddingsBatchAsync(titles);
            var contentVectors = await _embeddingService.GenerateEmbeddingsBatchAsync(contents);
            
            // Assign embeddings to documents
            for (int i = 0; i < documents.Count; i++)
            {
                documents[i].TitleVector = titleVectors[i];
                documents[i].ContentVector = contentVectors[i];
            }
            
            // Index the documents
            var batch = IndexDocumentsBatch.Create(
                documents.Select(d => IndexDocumentsAction.Upload(d)).ToArray());
                
            await _searchClient.IndexDocumentsAsync(batch);
            
            return documents;
        }
        
        public async Task DeleteDocumentAsync(string id)
        {
            var batch = IndexDocumentsBatch.Create(
                IndexDocumentsAction.Delete<Document>("id", id));
                
            await _searchClient.IndexDocumentsAsync(batch);
        }
    }
}