Condividi tramite


Usare archivi vettoriali nelle app .NET per intelligenza artificiale

Il 📦 pacchetto Microsoft.Extensions.VectorData.Abstractions (MEVD) fornisce un'API unificata per l'uso di archivi vettoriali in .NET. È possibile usare lo stesso codice per archiviare e cercare incorporamenti in provider di database vettoriali diversi.

Questo articolo illustra come usare le funzionalità principali della libreria.

Prerequisiti

Installare i pacchetti

Installare un pacchetto fornitore per il database vettoriale. Il Microsoft.Extensions.VectorData.Abstractions pacchetto viene importato automaticamente in qualità di dipendenza transitiva. Nell'esempio seguente viene usato il provider in memoria per lo sviluppo e il test:

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

Per gli scenari di produzione, sostituire Microsoft.SemanticKernel.Connectors.InMemory con il provider per il database. Per i provider disponibili, vedere Provider di archiviazione vettoriale predefiniti. Nonostante l'inclusione di "SemanticKernel" nei nomi dei pacchetti del provider, questi provider non hanno nulla a che fare con il kernel semantico e sono utilizzabili ovunque in .NET, incluso Agent Framework.

Definire un modello di dati

Definire una classe .NET per rappresentare i record da archiviare nell'archivio vettoriale. Usare gli attributi per annotare le proprietà nella classe in base al fatto che rappresentino la chiave primaria, i dati generali o i dati vettoriali. Ecco un semplice esempio:

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

In alternativa all'uso degli attributi, è possibile definire lo schema a livello di codice usando un oggetto VectorStoreCollectionDefinition. Questo approccio è utile quando si vuole usare lo stesso modello di dati con configurazioni diverse o quando non è possibile aggiungere attributi alla classe del modello di dati.

Per altre informazioni, vedere Definire il modello di dati.

Crea un archivio vettoriale

Creare un'istanza dell'implementazione per il VectorStore database scelto. L'esempio seguente crea un archivio vettoriale in memoria:

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

Ottieni una raccolta

Chiamare GetCollection su VectorStore per ottenere un riferimento VectorStoreCollection<TKey,TRecord> tipizzato. Quindi, chiamare EnsureCollectionExistsAsync per creare la raccolta, se non esiste già:

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

Il nome della raccolta è mappato al concetto di archiviazione sottostante per il database, ad esempio una tabella in SQL Server, un indice in Ricerca di intelligenza artificiale di Azure o un contenitore in Cosmos DB.

Inserimento o aggiornamento di record

Utilizzare UpsertAsync per inserire o aggiornare i record nella raccolta. Se esiste già un record con la stessa chiave, viene aggiornato:

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

Importante

In un'app reale è consigliabile consentire a MEVD di generare incorporamenti prima di archiviare i record.

Ottieni record

Utilizzare GetAsync per recuperare un singolo record in base alla relativa chiave. Per recuperare più record, passare un IEnumerable<TKey> a 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}");
}

Per recuperare più record contemporaneamente:

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

Usare SearchAsync per trovare record semanticamente simili a una query. Invia il vettore di incorporamento per la tua query e il numero di risultati da restituire.

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

Ognuno VectorSearchResult<TRecord> contiene il record corrispondente e un punteggio di somiglianza. I punteggi più alti indicano una corrispondenza semantica più vicina.

Filtrare i risultati della ricerca

Usare VectorSearchOptions<TRecord> per filtrare i risultati della ricerca prima del confronto tra vettori. È possibile filtrare in base a qualsiasi proprietà contrassegnata con 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})");
}

I filtri vengono espressi come espressioni LINQ. Le operazioni supportate variano in base al provider, ma tutti i provider supportano confronti comuni come uguaglianza, disuguaglianza e logica && e ||.

Controllare il comportamento di ricerca con VectorSearchOptions

Usare VectorSearchOptions<TRecord> per controllare vari aspetti del comportamento di ricerca vettoriale:

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

Nella tabella seguente vengono descritte le opzioni disponibili:

Opzione Descrizione
Filter Espressione LINQ per filtrare i record prima del confronto tra vettori.
VectorProperty Proprietà del vettore su cui eseguire la ricerca. Obbligatorio quando il modello di dati ha più proprietà vettoriali.
Skip Numero di risultati da ignorare prima del ritorno. Utile per il paging. Il valore predefinito è 0.
IncludeVectors Indica se includere i dati vettoriali nei record restituiti. L'omissione di vettori riduce il trasferimento dei dati. Il valore predefinito è false.

Per altre informazioni, vedere Opzioni di ricerca vettoriale.

Usare il generatore di embedding incorporato

Anziché generare manualmente incorporamenti prima di ogni upsert, è possibile configurare un oggetto IEmbeddingGenerator nell'archivio vettoriale o nella raccolta. Quando esegui questa operazione, dichiara la proprietà del vettore come un tipo string (il testo di origine) e il negozio genera automaticamente l'embedding.

Per altre informazioni, vedere Consentire all'archivio vettoriale di generare incorporamenti.

Alcuni archivi vettoriali supportano la ricerca ibrida, che combina la somiglianza dei vettori con la corrispondenza delle parole chiave. Questo approccio può migliorare la pertinenza dei risultati rispetto alla ricerca solo vettoriale.

Per usare la ricerca ibrida, controllare se la raccolta implementa IKeywordHybridSearchable<TRecord>. Solo i provider per i database che supportano questa funzionalità implementano questa interfaccia.

Per altre informazioni, vedere Ricerca ibrida con provider di archivi vettoriali.

Eliminazione di record

Per eliminare un singolo record per chiave, usare DeleteAsync:

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

Eliminare una raccolta

Per rimuovere un'intera raccolta dall'archivio vettoriale, usare EnsureCollectionDeletedAsync:

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

Cambiare i provider di archivi vettoriali

Poiché tutti i provider implementano la stessa VectorStore classe astratta, è possibile spostarsi tra di essi modificando il tipo concreto all'avvio. Nella maggior parte dei casi, la raccolta e il codice di ricerca rimangono invariati. Tuttavia, alcune modifiche sono in genere necessarie, ad esempio, perché database diversi supportano tipi di dati diversi.