Freigeben über


Schnellstart: Vektorsuche mit .NET in Azure DocumentDB

Erfahren Sie, wie Sie die Vektorsuche in Azure DocumentDB mit dem .NET MongoDB-Treiber verwenden, um Vektordaten effizient zu speichern und abzufragen.

Diese Schnellstartanleitung bietet eine geführte Tour durch wichtige Vektorsuchtechniken mithilfe einer .NET-Beispiel-App auf GitHub.

Die App verwendet ein Beispiel-Hotel-Dataset in einer JSON-Datei mit vorab berechneten Vektoren aus dem text-embedding-ada-002 Modell, aber Sie können die Vektoren auch selbst generieren. Die Hoteldaten umfassen Hotelnamen, Standorte, Beschreibungen und Vektoreinbettungen.

Voraussetzungen

  • Ein Azure-Abonnement

    • Wenn Sie nicht über ein Azure-Abonnement verfügen, erstellen Sie ein kostenloses Konto
  • Ein vorhandener Azure DocumentDB-Cluster

    • Wenn Sie keinen Cluster haben, erstellen Sie einen neuen Cluster.

App-Abhängigkeiten

Die App verwendet die folgenden NuGet-Pakete:

  • Azure.Identity: Azure Identity-Bibliothek für kennwortlose Authentifizierung mit Microsoft Entra-ID
  • Azure.AI.OpenAI: Azure OpenAI-Clientbibliothek für die Kommunikation mit KI-Modellen und Erstellen von Vektoreinbettungen
  • Microsoft.Extensions.Configuration: Konfigurationsverwaltung für App-Einstellungen
  • MongoDB.Driver: Offizieller MongoDB .NET-Treiber für Datenbankkonnektivität und -vorgänge
  • Newtonsoft.Json: Beliebte JSON-Serialisierungs- und Deserialisierungsbibliothek

Konfigurieren und Ausführen der App

Führen Sie die folgenden Schritte aus, um die App mit Ihren eigenen Werten zu konfigurieren und Suchvorgänge für Ihren Azure DocumentDB-Cluster auszuführen.

Konfigurieren der App

Aktualisieren Sie die appsettings.json Platzhalterwerte mit Ihren eigenen:

{
  "AzureOpenAI": {
    "EmbeddingModel": "text-embedding-ada-002",
    "ApiVersion": "2023-05-15",
    "Endpoint": "https://<your-openai-service-name>.openai.azure.com/"
  },
  "DataFiles": {
    "WithoutVectors": "HotelsData_toCosmosDB.JSON",
    "WithVectors": "HotelsData_toCosmosDB_Vector.json"
  },
  "Embedding": {
    "FieldToEmbed": "Description",
    "EmbeddedField": "text_embedding_ada_002",
    "Dimensions": 1536,
    "BatchSize": 16
  },
  "MongoDB": {
    "TenantId": "<your-tenant-id>",
    "ClusterName": "<your-cluster-name>",
    "LoadBatchSize": 100
  },
  "VectorSearch": {
    "Query": "quintessential lodging near running trails, eateries, retail",
    "DatabaseName": "Hotels",
    "TopK": 5
  }
}

Für Azure authentifizieren

Die Beispiel-App verwendet die kennwortlose Authentifizierung über DefaultAzureCredential und die Microsoft Entra-ID. Melden Sie sich mit einem unterstützten Tool wie azure CLI oder Azure PowerShell bei Azure an, bevor Sie die Anwendung ausführen, damit sie sicher auf Azure-Ressourcen zugreifen kann.

Hinweis

Stellen Sie sicher, dass Ihre angemeldete Identität über die erforderlichen Datenebenenrollen sowohl im Azure DocumentDB-Konto als auch in der Azure OpenAI-Ressource verfügt.

az login

Erstellen und Ausführen des Projekts

Die Beispiel-App füllt vektorisierte Beispieldaten in einer MongoDB-Auflistung auf und ermöglicht es Ihnen, verschiedene Arten von Suchabfragen auszuführen.

  1. Verwenden Sie den dotnet run Befehl, um die App zu starten:

    dotnet run
    

    Die App druckt ein Menü, in dem Sie Datenbank- und Suchoptionen auswählen können:

    === Cosmos DB Vector Samples Menu ===
    Please enter your choice (0-5):
    1. Create embeddings for data
    2. Show all database indexes
    3. Run IVF vector search
    4. Run HNSW vector search
    5. Run DiskANN vector search
    0. Exit
    
  2. Geben Sie 5 ein, und drücken Sie die EINGABETASTE.

    Nachdem die App die Datenbank aufgefüllt und die Suche ausgeführt hat, werden die fünf besten Hotels angezeigt, die mit der ausgewählten Vektorsuchabfrage und ihren Ähnlichkeitsbewertungen übereinstimmen.

    Die App-Protokollierung und -Ausgabe zeigen Folgendes an:

    • Sammlungserstellungs- und Dateneinfügungsstatus
    • Bestätigung der Vektorindexerstellung
    • Suchergebnisse mit Hotelnamen, Standorten und Ähnlichkeitsbewertungen

    Beispielausgabe (verkürzt aus Platzgründen):

    MongoDB client initialized with passwordless authentication
    Starting DiskANN vector search workflow
    Collection is empty, loading data from file
    Successfully loaded 50 documents into collection
    Creating vector index 'vectorIndex_diskann'
    Vector index 'vectorIndex_diskann' is ready for DiskANN search
    Executing DiskANN vector search for top 5 results
    
    Search Results (5 found using DiskANN):
    1. Roach Motel (Similarity: 0.8399)
    2. Royal Cottage Resort (Similarity: 0.8385)
    3. Economy Universe Motel (Similarity: 0.8360)
    4. Foot Happy Suites (Similarity: 0.8354)
    5. Country Comfort Inn (Similarity: 0.8346)
    

Erkunden Sie den App-Code

Die folgenden Abschnitte enthalten Details zu den wichtigsten Diensten und Code in der Beispiel-App. Besuchen Sie das GitHub-Repository , um den vollständigen App-Code zu erkunden.

Erkunden des Suchdiensts

Der VectorSearchService orchestriert eine End-to-End-Vektorähnlichkeitssuche mittels der Suchtechniken IVF, HNSW und DiskANN mit Azure OpenAI-Einbettungen.

using Azure.AI.OpenAI;
using Azure.Identity;
using CosmosDbVectorSamples.Models;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Reflection;

namespace CosmosDbVectorSamples.Services.VectorSearch;

/// <summary>
/// Service for performing vector similarity searches using different algorithms (IVF, HNSW, DiskANN).
/// Handles data loading, vector index creation, query embedding generation, and search execution.
/// </summary>
public class VectorSearchService
{
    private readonly ILogger<VectorSearchService> _logger;
    private readonly AzureOpenAIClient _openAIClient;
    private readonly MongoDbService _mongoService;
    private readonly AppConfiguration _config;

    public VectorSearchService(ILogger<VectorSearchService> logger, MongoDbService mongoService, AppConfiguration config)
    {
        _logger = logger;
        _mongoService = mongoService;
        _config = config;
        
        // Initialize Azure OpenAI client with passwordless authentication
        _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>
    /// <param name="indexType">The vector search algorithm to use (IVF, HNSW, or DiskANN)</param>
    public async Task RunSearchAsync(VectorIndexType indexType)
    {
        try
        {
            _logger.LogInformation($"Starting {indexType} vector search workflow");
            
            // Setup collection
            var collectionSuffix = indexType switch 
            { 
                VectorIndexType.IVF => "ivf", 
                VectorIndexType.HNSW => "hnsw", 
                VectorIndexType.DiskANN => "diskann", 
                _ => throw new ArgumentException($"Unknown index type: {indexType}") 
            };
            var collectionName = $"hotels_{collectionSuffix}_fixed";
            var indexName = $"vectorIndex_{collectionSuffix}";
            
            var collection = _mongoService.GetCollection<HotelData>(_config.VectorSearch.DatabaseName, collectionName);
            
            // 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 _mongoService.LoadDataIfNeededAsync(collection, dataFilePath);

            // Create the vector index with algorithm-specific search options
            var searchOptions = indexType switch
            {
                VectorIndexType.IVF => CreateIVFSearchOptions(_config.Embedding.Dimensions),
                VectorIndexType.HNSW => CreateHNSWSearchOptions(_config.Embedding.Dimensions),
                VectorIndexType.DiskANN => CreateDiskANNSearchOptions(_config.Embedding.Dimensions),
                _ => throw new ArgumentException($"Unknown index type: {indexType}")
            };
            
            await _mongoService.CreateVectorIndexAsync(
                _config.VectorSearch.DatabaseName, collectionName, indexName,
                _config.Embedding.EmbeddedField, searchOptions);
            
            _logger.LogInformation($"Vector index '{indexName}' is ready for {indexType} search");
            await Task.Delay(5000); // Allow index to be fully initialized

            // 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");

            // Build MongoDB aggregation pipeline for vector search
            var searchPipeline = new BsonDocument[]
            {
                // Vector similarity search using cosmosSearch
                new BsonDocument("$search", new BsonDocument
                {
                    ["cosmosSearch"] = new BsonDocument
                    {
                        ["vector"] = new BsonArray(queryEmbedding.Select(f => new BsonDouble(f))),
                        ["path"] = _config.Embedding.EmbeddedField,  // Field containing embeddings
                        ["k"] = _config.VectorSearch.TopK           // Number of results to return
                    }
                }),
                // Project results with similarity scores
                new BsonDocument("$project", new BsonDocument
                {
                    ["score"] = new BsonDocument("$meta", "searchScore"),
                    ["document"] = "$$ROOT"
                })
            };

            // Execute and process the search
            _logger.LogInformation($"Executing {indexType} vector search for top {_config.VectorSearch.TopK} results");
            var searchResults = (await collection.AggregateAsync<BsonDocument>(searchPipeline)).ToList()
                .Select(result => new SearchResult 
                { 
                    Document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<HotelData>(result["document"].AsBsonDocument), 
                    Score = result["score"].AsDouble 
                }).ToList();

            // Print the results
            if (searchResults?.Count == 0) 
            { 
                _logger.LogInformation("❌ No search results found. Check query terms and data availability."); 
            }
            else
            {
                _logger.LogInformation($"\n✅ Search Results ({searchResults!.Count} found using {indexType}):");
                for (int i = 0; i < searchResults.Count; i++)
                {
                    var result = searchResults[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;
        }
    }

    /// <summary>
    /// Creates IVF (Inverted File) search options - good for large datasets with fast approximate search
    /// </summary>
    private BsonDocument CreateIVFSearchOptions(int dimensions) => new BsonDocument
    {
        ["kind"] = "vector-ivf",
        ["similarity"] = "COS",
        ["dimensions"] = dimensions,
        ["numLists"] = 1
    };

    /// <summary>
    /// Creates HNSW (Hierarchical Navigable Small World) search options - best accuracy/speed balance
    /// </summary>
    private BsonDocument CreateHNSWSearchOptions(int dimensions) => new BsonDocument
    {
        ["kind"] = "vector-hnsw",
        ["similarity"] = "COS",
        ["dimensions"] = dimensions,
        ["m"] = 16,
        ["efConstruction"] = 64
    };

    /// <summary>
    /// Creates DiskANN search options - optimized for very large datasets stored on disk
    /// </summary>
    private BsonDocument CreateDiskANNSearchOptions(int dimensions) => new BsonDocument
    {
        ["kind"] = "vector-diskann",
        ["similarity"] = "COS",
        ["dimensions"] = dimensions
    };
}

Im vorherigen Code führt VectorSearchService die folgenden Aufgaben aus:

  • Bestimmt die Auflistungs- und Indexnamen basierend auf dem angeforderten Algorithmus.
  • Erstellt oder ruft die MongoDB -Auflistung ab und lädt JSON-Daten, wenn sie leer ist
  • Erstellt die algorithmusspezifischen Indexoptionen (IVF / HNSW / DiskANN) und stellt sicher, dass der Vektorindex vorhanden ist.
  • Generiert eine Einbettung für die konfigurierte Abfrage über Azure OpenAI
  • Erstellt und führt die Aggregationssuchpipeline aus.
  • Deserialisiert und druckt die Ergebnisse.

Erkunden des Azure DocumentDB-Diensts

Die MongoDbService verwaltet die Interaktionen mit Azure DocumentDB, um Aufgaben wie das Laden von Daten, die Erstellung von Vektorindizes, die Indexauflistung und Masseneinfügungen für die Hotelvektorsuche zu handhaben.

using Azure.Identity;
using CosmosDbVectorSamples.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json;

namespace CosmosDbVectorSamples.Services;

/// <summary>
/// Service for MongoDB operations including data insertion, index management, and vector index creation.
/// Supports Azure Cosmos DB for MongoDB with passwordless authentication.
/// </summary>
public class MongoDbService
{
    private readonly ILogger<MongoDbService> _logger;
    private readonly AppConfiguration _config;
    private readonly MongoClient _client;

    public MongoDbService(ILogger<MongoDbService> logger, IConfiguration configuration)
    {
        _logger = logger;
        _config = new AppConfiguration();
        configuration.Bind(_config);
        
        // Validate configuration
        if (string.IsNullOrEmpty(_config.MongoDB.ClusterName))
            throw new InvalidOperationException("MongoDB connection not configured. Please provide ConnectionString or ClusterName.");
            
        // Configure MongoDB connection for Azure Cosmos DB with OIDC authentication
        var connectionString = $"mongodb+srv://{_config.MongoDB.ClusterName}.global.mongocluster.cosmos.azure.com/?tls=true&authMechanism=MONGODB-OIDC&retrywrites=false&maxIdleTimeMS=120000";
        var settings = MongoClientSettings.FromUrl(MongoUrl.Create(connectionString));
        settings.UseTls = true;
        settings.RetryWrites = false;
        settings.MaxConnectionIdleTime = TimeSpan.FromMinutes(2);
        settings.Credential = MongoCredential.CreateOidcCredential(new AzureIdentityTokenHandler(new DefaultAzureCredential(), _config.MongoDB.TenantId));
        settings.Freeze();
        
        _client = new MongoClient(settings);
        _logger.LogInformation("MongoDB client initialized with passwordless authentication");
    }

    /// <summary>Gets a database instance by name</summary>
    public IMongoDatabase GetDatabase(string databaseName) => _client.GetDatabase(databaseName);
    
    /// <summary>Gets a collection instance from the specified database</summary>
    public IMongoCollection<T> GetCollection<T>(string databaseName, string collectionName) => 
        _client.GetDatabase(databaseName).GetCollection<T>(collectionName);

    /// <summary>
    /// Creates a vector search index for Cosmos DB MongoDB, with support for IVF, HNSW, and DiskANN algorithms
    /// </summary>
    public async Task<BsonDocument> CreateVectorIndexAsync(string databaseName, string collectionName, string indexName, string embeddedField, BsonDocument cosmosSearchOptions)
    {
        var database = _client.GetDatabase(databaseName);
        var collection = database.GetCollection<BsonDocument>(collectionName);
        
        // Check if index already exists to avoid duplication
        var indexList = await (await collection.Indexes.ListAsync()).ToListAsync();
        if (indexList.Any(index => index.TryGetValue("name", out var nameValue) && nameValue.AsString == indexName))
        {
            _logger.LogInformation($"Vector index '{indexName}' already exists, skipping creation");
            return new BsonDocument { ["ok"] = 1 };
        }
        
        // Create the specified vector index type
        _logger.LogInformation($"Creating vector index '{indexName}' on field '{embeddedField}'");
        return await database.RunCommandAsync<BsonDocument>(new BsonDocument
        {
            ["createIndexes"] = collectionName,
            ["indexes"] = new BsonArray 
            { 
                new BsonDocument 
                { 
                    ["name"] = indexName, 
                    ["key"] = new BsonDocument { [embeddedField] = "cosmosSearch" }, 
                    ["cosmosSearchOptions"] = cosmosSearchOptions 
                } 
            }
        });
    }

    /// <summary>
    /// Displays all indexes across all user databases, excluding system databases
    /// </summary>
    public async Task ShowAllIndexesAsync()
    {
        try
        {
            // Get user databases (exclude system databases)
            var databases = (await (await _client.ListDatabaseNamesAsync()).ToListAsync())
                .Where(name => !new[] { "admin", "config", "local" }.Contains(name)).ToList();
                
            if (!databases.Any()) 
            { 
                _logger.LogInformation("No user databases found or access denied"); 
                return; 
            }

            foreach (var dbName in databases)
            {
                var database = _client.GetDatabase(dbName);
                var collections = await (await database.ListCollectionNamesAsync()).ToListAsync();
                
                if (!collections.Any()) 
                { 
                    _logger.LogInformation($"Database '{dbName}': No collections found"); 
                    continue; 
                }
                
                _logger.LogInformation($"\n📂 DATABASE: {dbName} ({collections.Count} collections)");
                
                // Display indexes for each collection
                foreach (var collName in collections)
                {
                    try
                    {
                        var indexList = await (await database.GetCollection<BsonDocument>(collName).Indexes.ListAsync()).ToListAsync();
                        _logger.LogInformation($"\n  🗃️ COLLECTION: {collName} ({indexList.Count} indexes)");
                        indexList.ForEach(index => _logger.LogInformation($"    Index: {index.ToJson()}"));
                    }
                    catch (Exception ex) 
                    { 
                        _logger.LogError(ex, $"Failed to list indexes for collection '{collName}'"); 
                    }
                }
            }
        }
        catch (Exception ex) 
        { 
            _logger.LogError(ex, "Failed to retrieve database indexes"); 
            throw; 
        }
    }

    /// <summary>
    /// Loads data from file into collection if the collection is empty
    /// </summary>
    /// <param name="collection">Target collection to load data into</param>
    /// <param name="dataFilePath">Path to the JSON data file containing vector embeddings</param>
    /// <returns>Number of documents loaded, or 0 if collection already had data</returns>
    public async Task<int> LoadDataIfNeededAsync<T>(IMongoCollection<T> collection, string dataFilePath) where T : class
    {
        var existingDocCount = await collection.CountDocumentsAsync(Builders<T>.Filter.Empty);

        // Skip loading if collection already has data
        if (existingDocCount > 0)
        {
            _logger.LogInformation("Collection already contains data, skipping load operation");
            return 0;
        }

        // Load and validate data file
        _logger.LogInformation("Collection is empty, loading data from file");
        if (!File.Exists(dataFilePath))
            throw new FileNotFoundException($"Vector data file not found: {dataFilePath}");

        var jsonContent = await File.ReadAllTextAsync(dataFilePath);
        var data = JsonConvert.DeserializeObject<List<T>>(jsonContent) ?? new List<T>();
        
        if (data.Count == 0)
            throw new InvalidOperationException("No data found in the vector data file");

        // Insert data using existing method
        var summary = await InsertDataAsync(collection, data);
        _logger.LogInformation($"Successfully loaded {summary.Inserted} documents into collection");
        
        return summary.Inserted;
    }

    /// <summary>
    /// Inserts data into MongoDB collection, converts JSON embeddings to float arrays, and creates standard indexes
    /// </summary>
    public async Task<InsertSummary> InsertDataAsync<T>(IMongoCollection<T> collection, IEnumerable<T> data)
    {
        var dataList = data.ToList();
        _logger.LogInformation($"Processing {dataList.Count} items for insertion");

        // Convert JSON array embeddings to float arrays for vector search compatibility
        foreach (var hotel in dataList.OfType<HotelData>().Where(h => h.ExtraElements != null))
            foreach (var kvp in hotel.ExtraElements.ToList().Where(k => k.Value is Newtonsoft.Json.Linq.JArray))
                hotel.ExtraElements[kvp.Key] = ((Newtonsoft.Json.Linq.JArray)kvp.Value).Select(token => (float)token).ToArray();

        int inserted = 0, failed = 0;
        try
        {
            // Use unordered insert for better performance
            await collection.InsertManyAsync(dataList, new InsertManyOptions { IsOrdered = false });
            inserted = dataList.Count;
            _logger.LogInformation($"Successfully inserted {inserted} items");
        }
        catch (Exception ex)
        {
            failed = dataList.Count;
            _logger.LogError(ex, $"Batch insert failed for {dataList.Count} items");
        }

        // Create standard indexes for common query fields
        var indexFields = new[] { "HotelId", "Category", "Description", "Description_fr" };
        foreach (var field in indexFields)
            await collection.Indexes.CreateOneAsync(new CreateIndexModel<T>(Builders<T>.IndexKeys.Ascending(field)));

        return new InsertSummary { Total = dataList.Count, Inserted = inserted, Failed = failed };
    }

    /// <summary>Disposes the MongoDB client and its resources</summary>
    public void Dispose() => _client?.Cluster?.Dispose();
}

Im vorherigen Code führt MongoDbService die folgenden Aufgaben aus:

  • Liest die Konfiguration und erstellt einen kennwortlosen Client mit Azure-Anmeldeinformationen.
  • Stellt Datenbank- oder Sammlungsverweise bei Bedarf bereit.
  • Erstellt einen Vektorsuchindex nur, wenn er noch nicht vorhanden ist
  • Listet alle Nicht-Systemdatenbanken, deren Auflistungen und die Indizes der einzelnen Auflistungen auf.
  • Fügt Beispieldaten ein, wenn die Auflistung leer ist und unterstützende Indizes hinzufügt.

Anzeigen und Verwalten von Daten in Visual Studio Code

  1. Installieren Sie die DocumentDB-Erweiterung und die C#-Erweiterung in Visual Studio Code.

  2. Stellen Sie mithilfe der DocumentDB-Erweiterung eine Verbindung mit Ihrem Azure DocumentDB-Konto her.

  3. Zeigen Sie die Daten und Indizes der Datenbank "Hotels" an.

    Screenshot der DocumentDB-Erweiterung mit der DocumentDB-Auflistung.

Bereinigen von Ressourcen

Löschen Sie die Ressourcengruppe, den Azure DocumentDB-Cluster und die Azure OpenAI-Ressource, wenn Sie sie nicht mehr benötigen, um unnötige Kosten zu vermeiden.