Come creare un mapper personalizzato per un connettore vector store (anteprima)
Avviso
La funzionalità di archiviazione vettoriale del kernel semantico è in anteprima e i miglioramenti che richiedono modifiche di rilievo possono ancora verificarsi in circostanze limitate prima del rilascio.
In questa procedura verrà illustrato come sostituire il mapper predefinito per una raccolta di record dell'archivio vettoriale con il mapper personalizzato.
Verrà usato Qdrant per illustrare questa funzionalità, ma i concetti saranno simili per altri connettori.
Background
Ogni connettore Vector Store include un mapper predefinito in grado di eseguire il mapping dal modello di dati fornito allo schema di archiviazione supportato dall'archivio sottostante. Alcuni archivi consentono una grande libertà per quanto riguarda la modalità di archiviazione dei dati, mentre altri archivi richiedono un approccio più strutturato, ad esempio quando tutti i vettori devono essere aggiunti a un dizionario di vettori e tutti i campi non vettoriali a un dizionario di campi dati. Di conseguenza, il mapping è una parte importante dell'astrazione delle differenze di ogni implementazione dell'archivio dati.
In alcuni casi, lo sviluppatore potrebbe voler sostituire il mapper predefinito, ad esempio
- vogliono usare un modello di dati diverso dallo schema di archiviazione.
- vogliono creare un mapper ottimizzato per le prestazioni per il proprio scenario.
- il mapper predefinito non supporta una struttura di archiviazione richiesta dallo sviluppatore.
Tutte le implementazioni del connettore Vector Store consentono di fornire un mapper personalizzato.
Differenze in base al tipo di archivio vettoriale
Gli archivi dati sottostanti di ogni connettore Vector Store hanno modi diversi per archiviare i dati. Di conseguenza, il mapping a sul lato di archiviazione può essere diverso per ogni connettore.
Ad esempio, se si usa il connettore Qdrant, il tipo di archiviazione è una PointStruct
classe fornita dall'SDK Qdrant. Se si usa il connettore JSON Redis, il tipo di archiviazione è una string
chiave e , JsonNode
mentre se si usa un connettore HashSet JSON, il tipo di archiviazione è una string
chiave e una HashEntry
matrice.
Se si vuole eseguire il mapping personalizzato e si vogliono usare più tipi di connettore, sarà quindi necessario implementare un mapper per ogni tipo di connettore.
Creazione del modello di dati
Il primo passaggio consiste nel creare un modello di dati. In questo caso non verrà annotato il modello di dati con attributi, poiché verrà specificata una definizione di record separata che descrive l'aspetto dello schema del database.
Si noti anche che questo modello è complesso, con classi separate per vettori e informazioni aggiuntive sul prodotto.
public class Product
{
public ulong Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ProductVectors Vectors { get; set; }
public ProductInfo ProductInfo { get; set; }
}
public class ProductInfo
{
public double Price { get; set; }
public string SupplierId { get; set; }
}
public class ProductVectors
{
public ReadOnlyMemory<float> NameEmbedding { get; set; }
public ReadOnlyMemory<float> DescriptionEmbedding { get; set; }
}
Creazione della definizione del record
È necessario creare un'istanza di definizione di record per definire l'aspetto dello schema del database. In genere, un connettore richiederà questa informazione per eseguire il mapping quando si usa il mapper predefinito. Poiché si sta creando un mapper personalizzato, questo non è necessario per il mapping, ma il connettore richiederà comunque queste informazioni per la creazione di raccolte nell'archivio dati.
Si noti che la definizione qui è diversa dal modello di dati precedente. Per archiviare ProductInfo
è disponibile una proprietà stringa denominata ProductInfoJson
e i due vettori sono definiti allo stesso livello dei Id
campi e Name
Description
.
using Microsoft.Extensions.VectorData;
var productDefinition = new VectorStoreRecordDefinition
{
Properties = new List<VectorStoreRecordProperty>
{
new VectorStoreRecordKeyProperty("Id", typeof(ulong)),
new VectorStoreRecordDataProperty("Name", typeof(string)) { IsFilterable = true },
new VectorStoreRecordDataProperty("Description", typeof(string)),
new VectorStoreRecordDataProperty("ProductInfoJson", typeof(string)),
new VectorStoreRecordVectorProperty("NameEmbedding", typeof(ReadOnlyMemory<float>)) { Dimensions = 1536 },
new VectorStoreRecordVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory<float>)) { Dimensions = 1536 }
}
};
Importante
Per questo scenario, non sarebbe possibile usare attributi anziché una definizione di record poiché lo schema di archiviazione non è simile al modello di dati.
Creazione del mapper personalizzato
Tutti i mapper implementano l'interfaccia Microsoft.SemanticKernel.Data.IVectorStoreRecordMapper<TRecordDataModel, TStorageModel>
generica .
TRecordDataModel
sarà diverso a seconda del modello di dati che lo sviluppatore vuole usare e TStorageModel
sarà determinato dal tipo di archivio vettoriale.
Per Qdrant TStorageModel
è Qdrant.Client.Grpc.PointStruct
.
È quindi necessario implementare un mapper che eseguirà il mapping tra il Product
modello di dati e un Qdrant PointStruct
.
using Microsoft.Extensions.VectorData;
using Qdrant.Client.Grpc;
public class ProductMapper : IVectorStoreRecordMapper<Product, PointStruct>
{
public PointStruct MapFromDataToStorageModel(Product dataModel)
{
// Create a Qdrant PointStruct to map our data to.
var pointStruct = new PointStruct
{
Id = new PointId { Num = dataModel.Id },
Vectors = new Vectors(),
Payload = { },
};
// Add the data fields to the payload dictionary and serialize the product info into a json string.
pointStruct.Payload.Add("Name", dataModel.Name);
pointStruct.Payload.Add("Description", dataModel.Description);
pointStruct.Payload.Add("ProductInfoJson", JsonSerializer.Serialize(dataModel.ProductInfo));
// Add the vector fields to the vector dictionary.
var namedVectors = new NamedVectors();
namedVectors.Vectors.Add("NameEmbedding", dataModel.Vectors.NameEmbedding.ToArray());
namedVectors.Vectors.Add("DescriptionEmbedding", dataModel.Vectors.DescriptionEmbedding.ToArray());
pointStruct.Vectors.Vectors_ = namedVectors;
return pointStruct;
}
public Product MapFromStorageToDataModel(PointStruct storageModel, StorageToDataModelMapperOptions options)
{
var product = new Product
{
Id = storageModel.Id.Num,
// Retrieve the data fields from the payload dictionary and deserialize the product info from the json string that it was stored as.
Name = storageModel.Payload["Name"].StringValue,
Description = storageModel.Payload["Description"].StringValue,
ProductInfo = JsonSerializer.Deserialize<ProductInfo>(storageModel.Payload["ProductInfoJson"].StringValue)!,
// Retrieve the vector fields from the vector dictionary.
Vectors = new ProductVectors
{
NameEmbedding = new ReadOnlyMemory<float>(storageModel.Vectors.Vectors_.Vectors["NameEmbedding"].Data.ToArray()),
DescriptionEmbedding = new ReadOnlyMemory<float>(storageModel.Vectors.Vectors_.Vectors["DescriptionEmbedding"].Data.ToArray())
}
};
return product;
}
}
Uso del mapper personalizzato con una raccolta di record
Per usare il mapper personalizzato creato, è necessario passarlo alla raccolta di record in fase di costruzione. È anche necessario passare la definizione di record creata in precedenza, in modo che le raccolte vengano create nell'archivio dati usando lo schema corretto. Un'altra impostazione importante in questo caso è la modalità vettori denominati di Qdrant, perché abbiamo più di un vettore. Senza questa modalità attivata, è supportato un solo vettore.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Qdrant;
using Qdrant.Client;
var productMapper = new ProductMapper();
var collection = new QdrantVectorStoreRecordCollection<Product>(
new QdrantClient("localhost"),
"skproducts",
new()
{
HasNamedVectors = true,
PointStructCustomMapper = productMapper,
VectorStoreRecordDefinition = productDefinition
});
Uso di un mapper personalizzato con IVectorStore
Quando si usano IVectorStore
per ottenere IVectorStoreRecordCollection
le istanze dell'oggetto, non è possibile fornire un mapper personalizzato direttamente al GetCollection
metodo . Ciò è dovuto al fatto che i mapper personalizzati differiscono per ogni tipo di archivio vettoriale e renderebbero impossibile usare IVectorStore
per comunicare con qualsiasi implementazione dell'archivio vettoriale.
Tuttavia, è possibile fornire una factory quando si costruisce un'implementazione dell'archivio vettoriale. Può essere usato per personalizzare IVectorStoreRecordCollection
le istanze durante la creazione.
Di seguito è riportato un esempio di factory di questo tipo, che controlla se CreateCollection
è stato chiamato con la definizione del prodotto e il tipo di dati e, in tal caso, inserisce il mapper personalizzato e attiva la modalità vettori denominati.
public class QdrantCollectionFactory(VectorStoreRecordDefinition productDefinition) : IQdrantVectorStoreRecordCollectionFactory
{
public IVectorStoreRecordCollection<TKey, TRecord> CreateVectorStoreRecordCollection<TKey, TRecord>(QdrantClient qdrantClient, string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition)
where TKey : notnull
where TRecord : class
{
// If the record definition is the product definition and the record type is the product data
// model, inject the custom mapper into the collection options.
if (vectorStoreRecordDefinition == productDefinition && typeof(TRecord) == typeof(Product))
{
var customCollection = new QdrantVectorStoreRecordCollection<Product>(
qdrantClient,
name,
new()
{
HasNamedVectors = true,
PointStructCustomMapper = new ProductMapper(),
VectorStoreRecordDefinition = vectorStoreRecordDefinition
}) as IVectorStoreRecordCollection<TKey, TRecord>;
return customCollection!;
}
// Otherwise, just create a standard collection with the default mapper.
var collection = new QdrantVectorStoreRecordCollection<TRecord>(
qdrantClient,
name,
new()
{
VectorStoreRecordDefinition = vectorStoreRecordDefinition
}) as IVectorStoreRecordCollection<TKey, TRecord>;
return collection!;
}
}
Per usare la factory di raccolta, passarla all'archivio vettoriale durante la costruzione o durante la registrazione con il contenitore di inserimento delle dipendenze.
// When registering with the dependency injection container on the kernel builder.
kernelBuilder.AddQdrantVectorStore(
"localhost",
options: new()
{
VectorStoreCollectionFactory = new QdrantCollectionFactory(productDefinition)
});
// When constructing the Vector Store instance directly.
var vectorStore = new QdrantVectorStore(
new QdrantClient("localhost"),
new()
{
VectorStoreCollectionFactory = new QdrantCollectionFactory(productDefinition)
});
A questo punto è possibile usare l'archivio vettoriale come di consueto per ottenere una raccolta.
var collection = vectorStore.GetCollection<ulong, Product>("skproducts", productDefinition);
Presto disponibili
Altre informazioni saranno presto disponibili.
Presto disponibili
Altre informazioni saranno presto disponibili.