Compartir a través de


Inicio rápido: Búsqueda de vectores con .NET en Azure Cosmos DB

Aprenda a usar la búsqueda de vectores en Azure Cosmos DB para almacenar y consultar datos vectoriales de forma eficaz. En este inicio rápido se proporciona una visita guiada de técnicas de búsqueda de vectores clave mediante una aplicación de ejemplo .NET en GitHub.

La aplicación usa un conjunto de datos de hotel de ejemplo en un archivo JSON con vectores calculados del text-embedding-3-small modelo. Los datos del hotel incluyen nombres de hotel, ubicaciones, descripciones e incrustaciones vectoriales.

Prerrequisitos

Dependencias de la aplicación

La aplicación usa los siguientes paquetes NuGet:

  • Azure.Identity: biblioteca de identidades de Azure para la autenticación sin contraseña con Microsoft Entra ID
  • Azure.AI.OpenAI: Azure biblioteca cliente de OpenAI para comunicarse con modelos de IA y crear incrustaciones de vectores
  • Microsoft.Extensions.Configuration: gestión de la configuración para los ajustes de la aplicación
  • Microsoft.Azure.Cosmos: biblioteca cliente de Azure Cosmos DB para la conectividad y las operaciones de base de datos
  • Newtonsoft.Json: biblioteca de serialización y deserialización de JSON popular

Autenticación en Azure

La aplicación de ejemplo usa la autenticación sin contraseña a través de DefaultAzureCredential y Microsoft Entra ID. Inicie sesión en Azure usando una herramienta compatible como CLI de Azure o Azure PowerShell antes de ejecutar la aplicación para que pueda acceder a los recursos de Azure de manera segura.

Nota:

Asegúrese de que su identidad autenticada tiene los roles de plano de datos necesarios tanto en la cuenta de Azure Cosmos DB como en el recurso de Azure OpenAI.

az login

Aprovisionamiento y configuración de los recursos de la aplicación

Para ejecutar la aplicación de .NET, deberá aprovisionar los recursos Azure necesarios y configurar la aplicación de ejemplo para conectarse a ellos. Este proyecto está configurado para usar la CLI de Azure Developer (azd) para aprovisionar automáticamente los recursos Azure necesarios.

Aprovisionamiento de los recursos

Para aprovisionar recursos:

  1. En un terminal, clone el repositorio de ejemplo:

    git clone https://github.com/Azure-Samples/cosmos-db-vector-samples.git
    
  2. Vaya a la carpeta raíz del repositorio clonado (donde azure.yaml se encuentra), por ejemplo:

    cd cosmos-db-vector-samples
    
  3. Ejecute el siguiente comando:

    azd up
    

Siga las indicaciones para seleccionar la suscripción y el entorno de Azure.

Qué está provisionado:

  • Azure Cosmos DB: cuenta sin servidor con la base de datos y los contenedores de Hotels
  • Azure OpenAI: Recurso con implementaciones para:
    • Modelo de inserción: text-embedding-3-small
    • Modelo de chat: gpt-4.1-mini
  • Identidad administrada: identidad asignada por el usuario para el acceso seguro.
  • Asignaciones de roles de Azure RBAC que permiten el acceso sin contraseña de Microsoft Entra ID para que la identidad administrada tenga acceso a Azure Cosmos DB y Azure OpenAI.

Configuración de la aplicación

Actualice los valores del marcador de posición appsettings.json con los suyos propios.

{
  "AzureOpenAI": {
    "EmbeddingModel": "text-embedding-3-small",
    "ApiVersion": "2023-05-15",
    "Endpoint": "https://<your-openai-endpoint>.openai.azure.com"
  },
  "DataFiles": {
    "WithoutVectors": "../../../../data/HotelsData_toCosmosDB.JSON",
    "WithVectors": "../../../../data/HotelsData_toCosmosDB_Vector.json"
  },
  "Embedding": {
    "FieldToEmbed": "Description",
    "EmbeddedField": "DescriptionVector",
    "Dimensions": 1536,
    "BatchSize": 16
  },
  "CosmosDb": {
    "Endpoint": "https://<your-cosmosdb-endpoint>.documents.azure.com:443/",
    "DatabaseName": "Hotels"
  },
  "VectorSearch": {
    "Query": "quintessential lodging near running trails, eateries, retail",
    "TopK": 5
  }
}

Compila y ejecuta el proyecto

La aplicación de ejemplo rellena los datos de ejemplo vectorizados en una base de datos de Azure Cosmos DB y le permite ejecutar diferentes tipos de consultas de búsqueda. Cada consulta usa un contenedor diferente dentro de la base de datos.

  1. Use el dotnet run comando para iniciar la aplicación:

    dotnet run
    

    La aplicación imprime un menú para seleccionar las opciones de base de datos y búsqueda:

    === Cosmos DB Vector Samples Menu ===
    Please enter your choice (0-5):
    1. Create embeddings for data
    2. Show all database indexes
    3. Run Flat vector search
    4. Run Quantized Flat vector search
    5. Run DiskANN vector search
    0. Exit
    
  2. Escriba 3 y presione Entrar.

    Una vez que la aplicación rellena la base de datos y ejecuta la búsqueda, verá los cinco hoteles principales que coinciden con la consulta de búsqueda vectorial seleccionada y sus puntuaciones de similitud.

    El registro y la salida de la aplicación muestran:

    • Estado de creación y inserción de datos de contenedor
    • Confirmación de creación de índices vectoriales
    • Resultados de búsqueda con nombres de hotel, ubicaciones y puntuaciones de similitud

    Salida de ejemplo (abreviada por brevedad):

    Starting Flat vector search workflow
    Container 'hotels_flat' checked
    Vector index ready.
    Executing Flat vector search for top 5 results
    
    Search Results (5 found using Flat):
    1. Royal Cottage Resort (Similarity: 0.4991)
    2. Country Comfort Inn (Similarity: 0.4786)
    3. Nordick's Valley Motel (Similarity: 0.4635)
    4. Economy Universe Motel (Similarity: 0.4461)
    5. Roach Motel (Similarity: 0.4388)
    

Exploración del código de la aplicación

En las secciones siguientes se proporcionan detalles sobre los servicios y el código más importantes de la aplicación de ejemplo. Visit the GitHub repo para explorar el código completo de la aplicación.

Exploración del servicio de búsqueda

El VectorSearchService coordina una búsqueda de similitud vectorial de extremo a extremo utilizando técnicas de búsqueda Flat, QuantizedFlat y DiskANN con incrustaciones de Azure OpenAI.

using Azure.AI.OpenAI;
using Azure.Identity;
using CosmosDbVectorSamples.Models;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Reflection;

namespace CosmosDbVectorSamples.Services.VectorSearch;

/// <summary>
/// Service for performing vector similarity searches using Cosmos DB NoSQL.
/// </summary>
public class VectorSearchService
{
    private readonly ILogger<VectorSearchService> _logger;
    private readonly AzureOpenAIClient _openAIClient;
    private readonly CosmosDbService _cosmosService;
    private readonly AppConfiguration _config;

    public VectorSearchService(ILogger<VectorSearchService> logger, CosmosDbService cosmosService, AppConfiguration config)
    {
        _logger = logger;
        _cosmosService = cosmosService;
        _config = config;
        
        _openAIClient = new AzureOpenAIClient(new Uri(_config.AzureOpenAI.Endpoint), new DefaultAzureCredential());
    }

    /// <summary>
    /// Executes a complete vector search workflow: data setup, index creation, query embedding, and search
    /// </summary>
    public async Task RunSearchAsync(VectorIndexType indexType)
    {
        try
        {
            _logger.LogInformation($"Starting {indexType} vector search workflow");

            // Setup container (simulating collection behavior)
            var collectionSuffix = indexType switch 
            { 
                VectorIndexType.Flat => "flat", 
                VectorIndexType.QuantizedFlat => "quantizedflat", 
                VectorIndexType.DiskANN => "diskann", 
                _ => throw new ArgumentException($"Unknown index type: {indexType}") 
            };
            var containerName = $"hotels_{collectionSuffix}";
            
            // Create/Update Vector Index (Ensure container exists first)
            await _cosmosService.CreateVectorIndexAsync(
                _config.CosmosDb.DatabaseName, containerName, 
                _config.Embedding.EmbeddedField, indexType, _config.Embedding.Dimensions);

            // Get Container
            var container = await _cosmosService.GetContainerAsync(_config.CosmosDb.DatabaseName, containerName);
            
            // Load data from file if collection is empty
            var assemblyLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
            var dataFilePath = Path.Combine(assemblyLocation, _config.DataFiles.WithVectors);
            await _cosmosService.LoadDataIfNeededAsync(container, dataFilePath);
            
            _logger.LogInformation($"Vector index ready. Waiting for indexing to catch up...");
            await Task.Delay(5000); 

            // Create embedding for the query
            var embeddingClient = _openAIClient.GetEmbeddingClient(_config.AzureOpenAI.EmbeddingModel);
            var queryEmbedding = (await embeddingClient.GenerateEmbeddingAsync(_config.VectorSearch.Query)).Value.ToFloats().ToArray();
            _logger.LogInformation($"Generated query embedding with {queryEmbedding.Length} dimensions");

            // Execute Cosmos NoSQL Vector Search
            var queryText = $@"
                SELECT TOP @topK 
                    c AS Document, 
                    VectorDistance(c.{_config.Embedding.EmbeddedField}, @embedding) AS Score 
                FROM c 
                ORDER BY VectorDistance(c.{_config.Embedding.EmbeddedField}, @embedding)";

            var queryDef = new QueryDefinition(queryText)
                .WithParameter("@topK", _config.VectorSearch.TopK)
                .WithParameter("@embedding", queryEmbedding);

            using var iterator = container.GetItemQueryIterator<SearchResult>(queryDef);
            var results = new List<SearchResult>();

            _logger.LogInformation($"Executing {indexType} vector search for top {_config.VectorSearch.TopK} results");
            while (iterator.HasMoreResults)
            {
                var response = await iterator.ReadNextAsync();
                results.AddRange(response);
            }

            // Print the results
            if (results.Count == 0) 
            { 
                _logger.LogInformation("❌ No search results found. Check query terms and data availability."); 
            }
            else
            {
                _logger.LogInformation($"\n✅ Search Results ({results.Count} found using {indexType}):");
                for (int i = 0; i < results.Count; i++)
                {
                    var result = results[i];
                    var hotelName = result.Document?.HotelName ?? "Unknown Hotel";
                    _logger.LogInformation($"  {i + 1}. {hotelName} (Similarity: {result.Score:F4})");
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"{indexType} vector search failed");
            throw;
        }
    }
}

En el código anterior, VectorSearchService realiza las siguientes tareas:

  • Determina los nombres de contenedor e índice en función del algoritmo solicitado.
  • Crea u obtiene el contenedor de Azure Cosmos DB y carga datos JSON si está vacío.
  • Compila las opciones de índice específicas del algoritmo (Flat / QuantizedFlat / DiskANN) y garantiza que el índice vectorial existe.
  • Genera una inserción para la consulta configurada a través de Azure OpenAI.
  • Construye y ejecuta el flujo de trabajo de búsqueda por agregación
  • Deserializa e imprime los resultados

Exploración del servicio Azure Cosmos DB

El CosmosDBService administra las interacciones con Azure Cosmos DB para controlar tareas como cargar datos, creación de índices vectoriales, enumeración de índices e inserciones masivas para la búsqueda de vectores de hotel.

using Azure.Identity;
using CosmosDbVectorSamples.Models;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net;
using System.Collections.ObjectModel;

namespace CosmosDbVectorSamples.Services;

public class CosmosDbService
{
    private readonly ILogger<CosmosDbService> _logger;
    private readonly AppConfiguration _config;
    private readonly CosmosClient _client;

    public CosmosDbService(ILogger<CosmosDbService> logger, IConfiguration configuration)
    {
        _logger = logger;
        _config = new AppConfiguration();
        configuration.Bind(_config);
        
        var options = new CosmosClientOptions
        {
            SerializerOptions = new CosmosSerializationOptions
            {
                PropertyNamingPolicy = CosmosPropertyNamingPolicy.Default,
                IgnoreNullValues = true
            },
            // Allow bulk execution for data loading
            AllowBulkExecution = true
        };

        _client = new CosmosClient(_config.CosmosDb.Endpoint, new DefaultAzureCredential(), options);
    }

    public Task<Container> GetContainerAsync(string databaseName, string containerName)
    {
        return Task.FromResult(_client.GetContainer(databaseName, containerName));
    }

    public async Task CreateVectorIndexAsync(string databaseName, string containerName, string vectorField, VectorIndexType indexType, int dimensions)
    {
        var database = _client.GetDatabase(databaseName);
        
        _logger.LogInformation($"Ensuring container '{containerName}' exists with vector index '{indexType}'...");

        var properties = new ContainerProperties(containerName, "/HotelId");

        // Define Vector Embedding Policy
        var embeddings = new Collection<Embedding>
        {
            new Embedding
            {
                Path = "/" + vectorField,
                DataType = VectorDataType.Float32,
                DistanceFunction = DistanceFunction.Cosine,
                Dimensions = dimensions
            }
        };

        properties.VectorEmbeddingPolicy = new VectorEmbeddingPolicy(embeddings);

        // Define Indexing Policy
        properties.IndexingPolicy.VectorIndexes.Clear();
        
        var cosmosIndexType = indexType switch
        {
            VectorIndexType.Flat => Microsoft.Azure.Cosmos.VectorIndexType.Flat,
            VectorIndexType.QuantizedFlat => Microsoft.Azure.Cosmos.VectorIndexType.QuantizedFlat,
            VectorIndexType.DiskANN => Microsoft.Azure.Cosmos.VectorIndexType.DiskANN,
            _ => Microsoft.Azure.Cosmos.VectorIndexType.Flat
        };

        properties.IndexingPolicy.VectorIndexes.Add(new VectorIndexPath
        {
            Path = "/" + vectorField,
            Type = cosmosIndexType
        });
        
        // Use CreateContainerIfNotExistsAsync to behave like TS app (matches on GET if exists)
        await database.CreateContainerIfNotExistsAsync(properties);
        _logger.LogInformation($"Container '{containerName}' checked/created.");
    }

    public async Task<int> LoadDataIfNeededAsync(Container container, string dataFilePath)
    {
        try
        {
            // TypeScript app does NOT check 'SELECT COUNT(1)' first.
            // It simply tries to load data and relies on 409 Conflict for existing items.
            // Removed the pre-check to match TypeScript behavior.

            _logger.LogInformation($"Loading data from {dataFilePath}...");
            var jsonContent = await File.ReadAllTextAsync(dataFilePath);
            var items = JsonConvert.DeserializeObject<List<HotelData>>(jsonContent);

            if (items == null || items.Count == 0)
            {
                _logger.LogWarning("No data found in file.");
                return 0;
            }

            var tasks = new List<Task>();
            foreach (var item in items)
            {
                // Ensure ID is set
                if (string.IsNullOrEmpty(item.Id)) 
                {
                    item.Id = !string.IsNullOrEmpty(item.HotelId) ? item.HotelId : Guid.NewGuid().ToString();
                }
                
                // Matches TypeScript behavior: Try Create, assume failure on conflict (409) means "already exists"
                tasks.Add(container.CreateItemAsync(item, new PartitionKey(item.HotelId))
                    .ContinueWith(t => 
                    {
                        if (t.IsFaulted)
                        {
                            var cosmosEx = t.Exception?.InnerException as CosmosException;
                            if (cosmosEx?.StatusCode == HttpStatusCode.Conflict)
                            {
                                // Item already exists (409 Conflict), which is fine.
                                // We swallow this error to match TypeScript "try/catch -> continue" behavior
                            }
                            else
                            {
                                _logger.LogError($"Failed to insert item {item.HotelId}: {t.Exception?.InnerException?.Message}");
                            }
                        }
                    }));
            }

            await Task.WhenAll(tasks);
            _logger.LogInformation($"Loaded {items.Count} items (skipping existing).");
            return items.Count;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load data");
            throw;
        }
    }

    public async Task ShowAllIndexesAsync()
    {
        // Simple implementation to list databases and containers
        // Not implemented fully for brevity but good to have signature match
        try {
            var iterator = _client.GetDatabaseQueryIterator<DatabaseProperties>();
            while (iterator.HasMoreResults)
            {
                foreach (var db in await iterator.ReadNextAsync())
                {
                    _logger.LogInformation($"Database: {db.Id}");
                    var containerIterator = _client.GetDatabase(db.Id).GetContainerQueryIterator<ContainerProperties>();
                    while (containerIterator.HasMoreResults)
                    {
                        foreach (var container in await containerIterator.ReadNextAsync())
                        {
                            _logger.LogInformation($"\tContainer: {container.Id}");
                            if (container.VectorEmbeddingPolicy != null)
                            {
                                foreach(var embed in container.VectorEmbeddingPolicy.Embeddings)
                                {
                                    _logger.LogInformation($"\t\tVector Embedding: {embed.Path} ({embed.Dimensions}, {embed.DistanceFunction})");
                                }
                            }
                            if (container.IndexingPolicy.VectorIndexes != null)
                            {
                                foreach(var idx in container.IndexingPolicy.VectorIndexes)
                                {
                                    _logger.LogInformation($"\t\tVector Index: {idx.Path} ({idx.Type})");
                                }
                            }
                        }
                    }
                }
            }
        }
        catch(Exception ex)
        {
             _logger.LogError(ex, "Error listing indexes");
        }
    }
}

En el código anterior, CosmosDBService realiza las siguientes tareas:

  • Lee la configuración y compila un cliente sin contraseña con credenciales de Azure
  • Proporciona referencias de base de datos o contenedor a petición
  • Crea un índice de búsqueda vectorial solo si aún no existe.
  • Enumera todas las bases de datos que no son del sistema, sus contenedores y los índices de cada contenedor
  • Inserta datos de ejemplo si el contenedor está vacío y agrega índices auxiliares.

Visualización y administración de datos en Visual Studio Code

  1. Instale la extensión Azure Cosmos DB y C# en Visual Studio Code.
  2. Conéctese a la cuenta de Azure Cosmos DB mediante la extensión Azure Cosmos DB.
  3. Vea los datos e índices en la base de datos Hotels.

Limpieza de recursos

Cuando ya no necesite la API de la cuenta NoSQL, puede eliminar el grupo de recursos correspondiente.

  1. Vaya al grupo de recursos que creó anteriormente en el portal de Azure.

    Sugerencia

    En esta guía de inicio rápido, se recomienda el nombre msdocs-cosmos-quickstart-rg.

  2. Seleccione Eliminar grupo de recursos.

    Captura de pantalla de la opción Eliminar grupo de recursos en la barra de navegación de un grupo de recursos.

  3. En el cuadro de diálogo ¿Está seguro de que desea eliminar ?, escriba el nombre del grupo de recursos y, a continuación, seleccione Eliminar.

    Captura de pantalla de la página de confirmación de eliminación de un grupo de recursos.