Partager via


Création d’un mappeur personnalisé pour un connecteur Vector Store (préversion)

Avertissement

La fonctionnalité de magasin de vecteurs du noyau sémantique est en préversion et des améliorations nécessitant des modifications cassants peuvent toujours se produire dans des circonstances limitées avant la mise en production.

Dans cette procédure, nous allons montrer comment remplacer le mappeur par défaut pour une collection d’enregistrements de magasin vectoriel par votre propre mappeur.

Nous allons utiliser Qdrant pour illustrer cette fonctionnalité, mais les concepts seront similaires pour d’autres connecteurs.

Background

Chaque connecteur Vector Store inclut un mappeur par défaut qui peut mapper du modèle de données fourni au schéma de stockage pris en charge par le magasin sous-jacent. Certains magasins permettent une grande liberté en ce qui concerne la façon dont les données sont stockées, tandis que d’autres magasins nécessitent une approche plus structurée, par exemple, où tous les vecteurs doivent être ajoutés à un dictionnaire de vecteurs et tous les champs non vectoriels à un dictionnaire de champs de données. Par conséquent, le mappage est une partie importante de l’abstraction des différences de chaque implémentation de magasin de données.

Dans certains cas, le développeur peut vouloir remplacer le mappeur par défaut, par exemple.

  1. ils souhaitent utiliser un modèle de données qui diffère du schéma de stockage.
  2. ils souhaitent créer un mappeur optimisé en performances pour leur scénario.
  3. le mappeur par défaut ne prend pas en charge une structure de stockage requise par le développeur.

Toutes les implémentations du connecteur Vector Store vous permettent de fournir un mappeur personnalisé.

Différences par type de magasin vectoriel

Les magasins de données sous-jacents de chaque connecteur Vector Store ont différentes façons de stocker des données. Par conséquent, ce que vous mappez sur le côté stockage peut différer pour chaque connecteur.

Par exemple, si vous utilisez le connecteur Qdrant, le type de stockage est une PointStruct classe fournie par le Kit de développement logiciel (SDK) Qdrant. Si vous utilisez le connecteur JSON Redis, le type de stockage est une string clé et un JsonNode, tandis que si vous utilisez un connecteur JSON HashSet, le type de stockage est une string clé et un HashEntry tableau.

Si vous souhaitez effectuer un mappage personnalisé et que vous souhaitez utiliser plusieurs types de connecteurs, vous devez donc implémenter un mappeur pour chaque type de connecteur.

Création du modèle de données

Notre première étape consiste à créer un modèle de données. Dans ce cas, nous n’annoterons pas le modèle de données avec des attributs, car nous fournirons une définition d’enregistrement distincte qui décrit à quoi ressemblera le schéma de base de données.

Notez également que ce modèle est complexe, avec des classes distinctes pour les vecteurs et des informations supplémentaires sur le produit.

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

Création de la définition d’enregistrement

Nous devons créer une instance de définition d’enregistrement pour définir l’apparence du schéma de base de données. Normalement, un connecteur nécessite que ces informations effectuent le mappage lors de l’utilisation du mappeur par défaut. Étant donné que nous créons un mappeur personnalisé, cela n’est pas nécessaire pour le mappage. Toutefois, le connecteur nécessite toujours ces informations pour créer des collections dans le magasin de données.

Notez que la définition ici est différente du modèle de données ci-dessus. Pour stockerProductInfo, nous avons une propriété de chaîne appelée ProductInfoJson, et les deux vecteurs sont définis au même niveau que les champs et Description les IdName champs.

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

Important

Pour ce scénario, il n’est pas possible d’utiliser des attributs au lieu d’une définition d’enregistrement, car le schéma de stockage ne ressemble pas au modèle de données.

Création du mappeur personnalisé

Tous les mappeurs implémentent l’interface Microsoft.SemanticKernel.Data.IVectorStoreRecordMapper<TRecordDataModel, TStorageModel>générique . TRecordDataModel varie en fonction du modèle de données que le développeur souhaite utiliser et TStorageModel sera déterminé par le type de Vector Store.

Pour Qdrant TStorageModel est Qdrant.Client.Grpc.PointStruct.

Nous devons donc implémenter un mappeur qui mappe entre notre Product modèle de données et 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;
    }
}

Utilisation de votre mappeur personnalisé avec une collection d’enregistrements

Pour utiliser le mappeur personnalisé que nous avons créé, nous devons le transmettre à la collection d’enregistrements au moment de la construction. Nous devons également passer la définition d’enregistrement que nous avons créée précédemment, afin que les collections soient créées dans le magasin de données à l’aide du schéma approprié. Un autre paramètre important ici est le mode vecteurs nommés de Qdrant, car nous avons plusieurs vecteurs. Sans ce mode activé, un seul vecteur est pris en charge.

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

Utilisation d’un mappeur personnalisé avec IVectorStore

Lorsque vous utilisez IVectorStore pour obtenir IVectorStoreRecordCollection des instances d’objet, il n’est pas possible de fournir un mappeur personnalisé directement à la GetCollection méthode. Cela est dû au fait que les mappeurs personnalisés diffèrent pour chaque type de magasin de vecteurs et rendent impossible l’utilisation IVectorStore pour communiquer avec n’importe quelle implémentation de magasin de vecteurs.

Toutefois, il est possible de fournir une fabrique lors de la construction d’une implémentation de Vector Store. Cela peut être utilisé pour personnaliser IVectorStoreRecordCollection des instances à mesure qu’elles sont créées.

Voici un exemple de cette fabrique, qui vérifie si CreateCollection elle a été appelée avec la définition de produit et le type de données, et si c’est le cas, injecte le mappeur personnalisé et active le mode vecteurs nommés.

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

Pour utiliser la fabrique de collection, passez-la au magasin de vecteurs lors de sa construction ou lors de son inscription auprès du conteneur d’injection de dépendances.

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

Vous pouvez maintenant utiliser le magasin vectoriel comme normal pour obtenir une collection.

var collection = vectorStore.GetCollection<ulong, Product>("skproducts", productDefinition);

Prochainement

Plus d’informations prochainement.

Prochainement

Plus d’informations prochainement.