แก้ไข

แชร์ผ่าน


Use vector stores in .NET AI apps

The 📦 Microsoft.Extensions.VectorData.Abstractions (MEVD) package provides a unified API for working with vector stores in .NET. You can use the same code to store and search embeddings across different vector database providers.

This article shows you how to use the key features of the library.

Prerequisites

Install the packages

Install a provider package for your vector database. The Microsoft.Extensions.VectorData.Abstractions package is brought in automatically as a transitive dependency. The following example uses the in-memory provider for development and testing:

dotnet package add Microsoft.SemanticKernel.Connectors.InMemory --prerelease

For production scenarios, replace Microsoft.SemanticKernel.Connectors.InMemory with the provider for your database. For available providers, see Out-of-the-box Vector Store providers. (Despite the inclusion of "SemanticKernel" in the provider package names, these providers have nothing to do with Semantic Kernel and are usable anywhere in .NET, including Agent Framework.)

Define a data model

Define a .NET class to represent the records you want to store in the vector store. Use attributes to annotate properties in the class according to whether they represent the primary key, general data, or vector data. Here's a simple example:

public class Hotel
{
    [VectorStoreKey]
    public ulong HotelId { get; set; }

    [VectorStoreData(IsIndexed = true)]
    public required string HotelName { get; set; }

    [VectorStoreData(IsFullTextIndexed = true)]
    public required string Description { get; set; }

    [VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Hnsw)]
    public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }

    [VectorStoreData(IsIndexed = true)]
    public required string[] Tags { get; set; }
}

As an alternative to using attributes, you can define your schema programmatically using a VectorStoreCollectionDefinition. This approach is useful when you want to use the same data model with different configurations, or when you can't add attributes to the data model class.

For more information, see Define your data model.

Create a vector store

Create an instance of the VectorStore implementation for your chosen database. The following example creates an in-memory vector store:

// Create an in-memory vector store (no external service required).
// For production, replace this with a connector for your preferred database.
var vectorStore = new InMemoryVectorStore();

Get a collection

Call GetCollection on the VectorStore to get a typed VectorStoreCollection<TKey,TRecord> reference. Then call EnsureCollectionExistsAsync to create the collection if it doesn't already exist:

// Get a reference to a collection named "hotels".
VectorStoreCollection<int, Hotel> collection =
    vectorStore.GetCollection<int, Hotel>("hotels");

// Ensure the collection exists in the database.
await collection.EnsureCollectionExistsAsync();

The collection name maps to the underlying storage concept for your database (for example, a table in SQL Server, an index in Azure AI Search, or a container in Cosmos DB).

Upsert records

Use UpsertAsync to insert or update records in the collection. If a record with the same key already exists, it gets updated:

// Upsert records into the collection.
// In a real app, generate embeddings using an IEmbeddingGenerator.
// The CreateFakeEmbedding helper at the bottom of this file generates
// placeholder vectors for demonstration purposes only.
var hotels = new List<Hotel>
{
    new()
    {
        HotelId = 1,
        HotelName = "Seaside Retreat",
        Description = "A peaceful hotel on the coast with stunning ocean views.",
        DescriptionEmbedding = CreateFakeEmbedding(1),
        Tags = ["beach", "ocean", "relaxation"]
    },
    new()
    {
        HotelId = 2,
        HotelName = "Mountain Lodge",
        Description = "A cozy lodge in the mountains with hiking trails nearby.",
        DescriptionEmbedding = CreateFakeEmbedding(2),
        Tags = ["mountain", "hiking", "nature"]
    },
    new()
    {
        HotelId = 3,
        HotelName = "City Centre Hotel",
        Description = "A modern hotel in the heart of the city, close to attractions.",
        DescriptionEmbedding = CreateFakeEmbedding(3),
        Tags = ["city", "business", "urban"]
    }
};

foreach (Hotel h in hotels)
{
    await collection.UpsertAsync(h);
}

Important

In a real app, it's recommended to let MEVD generate embeddings before storing the records.

Get records

Use GetAsync to retrieve a single record by its key. To retrieve multiple records, pass an IEnumerable<TKey> to GetAsync:

// Get a specific record by its key.
Hotel? hotel = await collection.GetAsync(1);
if (hotel is not null)
{
    Console.WriteLine($"Hotel: {hotel.HotelName}");
    Console.WriteLine($"Description: {hotel.Description}");
}

To retrieve multiple records at once:

// Get multiple records by their keys.
IAsyncEnumerable<Hotel> hotelBatch = collection.GetAsync([1, 2, 3]);
await foreach (Hotel h in hotelBatch)
{
    Console.WriteLine($"Batch hotel: {h.HotelName}");
}

Use SearchAsync to find records that are semantically similar to a query. Pass the embedding vector for your query and the number of results to return:

// Search for the top 2 hotels most similar to the query embedding.
IAsyncEnumerable<VectorSearchResult<Hotel>> searchResults =
    collection.SearchAsync(queryEmbedding, top: 2);

await foreach (VectorSearchResult<Hotel> result in searchResults)
{
    Console.WriteLine($"Found: {result.Record.HotelName} (score: {result.Score:F4})");
}

Each VectorSearchResult<TRecord> contains the matching record and a similarity score. Higher scores indicate a closer semantic match.

Filter search results

Use VectorSearchOptions<TRecord> to filter search results before the vector comparison. You can filter on any property marked with IsIndexed = true:

// Filter results before the vector comparison.
// Only properties marked with IsIndexed = true can be used in filters.
var searchOptions = new VectorSearchOptions<Hotel>
{
    Filter = h => h.HotelName == "Seaside Retreat"
};

IAsyncEnumerable<VectorSearchResult<Hotel>> filteredResults =
    collection.SearchAsync(queryEmbedding, top: 2, searchOptions);

await foreach (VectorSearchResult<Hotel> result in filteredResults)
{
    Console.WriteLine($"Filtered: {result.Record.HotelName} (score: {result.Score:F4})");
}

Filters are expressed as LINQ expressions. The supported operations vary by provider, but all providers support common comparisons like equality, inequality, and logical && and ||.

Control search behavior with VectorSearchOptions

Use VectorSearchOptions<TRecord> to control various aspects of vector search behavior:

// Use VectorSearchOptions to control paging and vector inclusion.
var pagedOptions = new VectorSearchOptions<Hotel>
{
    Skip = 1,           // Skip the first result (useful for paging).
    IncludeVectors = false  // Don't include vector data in results (default).
};

IAsyncEnumerable<VectorSearchResult<Hotel>> pagedResults =
    collection.SearchAsync(queryEmbedding, top: 2, pagedOptions);

await foreach (VectorSearchResult<Hotel> result in pagedResults)
{
    Console.WriteLine($"Paged: {result.Record.HotelName}");
}

The following table describes the available options:

Option Description
Filter A LINQ expression to filter records before vector comparison.
VectorProperty The vector property to search on. Required when the data model has multiple vector properties.
Skip Number of results to skip before returning. Useful for paging. Default is 0.
IncludeVectors Whether to include vector data in the returned records. Omitting vectors reduces data transfer. Default is false.

For more information, see Vector search options.

Use built-in embedding generation

Instead of generating embeddings manually before each upsert, you can configure an IEmbeddingGenerator on the vector store or collection. When you do, declare your vector property as a string type (the source text) and the store generates the embedding automatically.

For more information, see Let the vector store generate embeddings.

Some vector stores support hybrid search, which combines vector similarity with keyword matching. This approach can improve result relevance compared to vector-only search.

To use hybrid search, check whether your collection implements IKeywordHybridSearchable<TRecord>. Only providers for databases that support this feature implement this interface.

For more information, see Hybrid search using vector store providers.

Delete records

To delete a single record by key, use DeleteAsync:

// Delete a record by its key.
await collection.DeleteAsync(3);

Delete a collection

To remove an entire collection from the vector store, use EnsureCollectionDeletedAsync:

// Delete the entire collection from the vector store.
await collection.EnsureCollectionDeletedAsync();

Switch vector store providers

Because all providers implement the same VectorStore abstract class, you can switch between them by changing the concrete type at startup. For the most part, your collection and search code remains the same. However, some adjustments are usually required, for example, because different databases support different data types.