次の方法で共有


クイック スタート: Azure DocumentDB での .NET を使用したベクター検索

.NET MongoDB ドライバーと共に Azure DocumentDB でベクター検索を使用して、ベクター データを効率的に格納およびクエリする方法について説明します。

このクイック スタートでは、GitHub の .NET サンプル アプリ を使用したキー ベクター検索手法のガイド付きツアーを提供します。

このアプリでは、 text-embedding-ada-002 モデルから事前に計算されたベクターを含む JSON ファイル内のサンプル ホテル データセットを使用しますが、ベクターを自分で生成することもできます。 ホテル データには、ホテル名、場所、説明、ベクター埋め込みなどが含まれます。

[前提条件]

  • Azure サブスクリプション

    • Azure サブスクリプションをお持ちでない場合は、無料アカウントを作成してください
  • 既存の Azure DocumentDB クラスター

  • Azure Cloud Shell で Bash 環境を使用します。 詳細については、「Azure Cloud Shell の概要」を参照してください。

  • CLI 参照コマンドをローカルで実行する場合は、Azure CLI を インストール します。 Windows または macOS で実行している場合は、Docker コンテナーで Azure CLI を実行することを検討してください。 詳細については、「Docker コンテナーで Azure CLI を実行する方法」を参照してください。

    • ローカル インストールを使用する場合は、az login コマンドを使用して Azure CLI にサインインします。 認証プロセスを完了するには、ターミナルに表示される手順に従います。 その他のサインイン オプションについては、「 Azure CLI を使用した Azure への認証」を参照してください。

    • 初回使用時にインストールを求められたら、Azure CLI 拡張機能をインストールします。 拡張機能の詳細については、「Azure CLI で拡張機能を使用および管理する」を参照してください。

    • az version を実行し、インストールされているバージョンおよび依存ライブラリを検索します。 最新バージョンにアップグレードするには、az upgrade を実行します。

アプリの依存関係

このアプリでは、次の NuGet パッケージが使用されます。

  • Azure.Identity: Microsoft Entra ID を使用したパスワードレス認証用の Azure ID ライブラリ
  • Azure.AI.OpenAI: AI モデルと通信し、ベクター埋め込みを作成するための Azure OpenAI クライアント ライブラリ
  • Microsoft.Extensions.Configuration: アプリ設定の構成管理
  • MongoDB.Driver: データベース接続と操作用の公式 MongoDB .NET ドライバー
  • Newtonsoft.Json: 一般的な JSON シリアル化と逆シリアル化ライブラリ

アプリを構成して実行する

独自の値でアプリを構成し、Azure DocumentDB クラスターに対して検索を実行するには、次の手順を実行します。

アプリを設定する

appsettings.jsonプレースホルダーの値を独自の値で更新します。

{
  "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
  }
}

Azure に対して認証します

サンプル アプリでは、 DefaultAzureCredential と Microsoft Entra ID を使用してパスワードレス認証を使用します。 Azure リソースに安全にアクセスできるように、アプリケーションを実行する前に、Azure CLI や Azure PowerShell などのサポートされているツールを使用して Azure にサインインします。

サインインしている ID に、Azure DocumentDB アカウントと Azure OpenAI リソースの両方に必要なデータ プレーン ロールがあることを確認します。

az login

プロジェクトをビルドして実行する

このサンプル アプリでは、MongoDB コレクションにベクター化されたサンプル データを設定し、さまざまな種類の検索クエリを実行できます。

  1. dotnet run コマンドを使用してアプリを起動します。

    dotnet run
    

    このアプリでは、データベースと検索のオプションを選択するためのメニューが出力されます。

    === 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. 5 」と入力し、Enter キーを押します。

    アプリがデータベースにデータを入力して検索を実行すると、選択したベクター検索クエリとその類似性スコアに一致する上位 5 つのホテルが表示されます。

    アプリのログ記録と出力には、次の情報が表示されます。

    • コレクションの作成とデータの挿入の状態
    • ベクター インデックスの作成の確認
    • ホテル名、場所、類似性スコアを含む検索結果

    出力例 (簡潔にするために短縮):

    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)
    

アプリ コードを調べる

次のセクションでは、サンプル アプリで最も重要なサービスとコードについて詳しく説明します。 完全なアプリ コードを調べるには、GitHub リポジトリにアクセスしてください。

検索サービスを調べる

この VectorSearchService では、Azure OpenAI 埋め込みで IVF、HNSW、DiskANN の検索手法を使用して、エンドツーエンドのベクター類似性検索を調整します。

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
    };
}

上記のコードでは、 VectorSearchService は次のタスクを実行します。

  • 要求されたアルゴリズムに基づいてコレクション名とインデックス名を決定します
  • MongoDB コレクションを作成または取得し、空の場合は JSON データを読み込みます
  • アルゴリズム固有のインデックス オプション (IVF/HNSW/DiskANN) を構築し、ベクター インデックスが存在することを確認します
  • Azure OpenAI を使用して構成されたクエリの埋め込みを生成します
  • 集計検索パイプラインを構築して実行する
  • 結果を逆シリアル化して出力する

Azure DocumentDB サービスを探索する

MongoDbServiceは、Azure DocumentDB との対話を管理して、データの読み込み、ベクター インデックスの作成、インデックスの一覧表示、ホテル ベクター検索の一括挿入などのタスクを処理します。

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();
}

上記のコードでは、 MongoDbService は次のタスクを実行します。

  • 構成を読み取り、Azure 資格情報を使用してパスワードなしのクライアントを構築します
  • 必要に応じてデータベースまたはコレクションの参照を提供します
  • ベクター検索インデックスがまだ存在しない場合にのみ作成します
  • システム以外のすべてのデータベース、そのコレクション、および各コレクションのインデックスを一覧表示します
  • コレクションが空の場合にサンプル データを挿入し、サポート インデックスを追加します

Visual Studio Code でデータを表示および管理する

  1. Visual Studio Code に DocumentDB 拡張機能C# 拡張機能 をインストールします。

  2. DocumentDB 拡張機能を使用して Azure DocumentDB アカウントに接続します。

  3. Hotels データベースのデータとインデックスを表示します。

    DocumentDB コレクションを示す DocumentDB 拡張機能のスクリーンショット。

リソースをクリーンアップする

不要なコストを回避するためにリソース グループ、Azure DocumentDB クラスター、Azure OpenAI リソースが不要になったら、リソース グループ、Azure OpenAI リソースを削除します。