Compartir a través de


Creación de un asignador personalizado para un conector de almacén de vectores (versión preliminar)

Advertencia

La funcionalidad Almacén de vectores de kernel semántico está en versión preliminar y las mejoras que requieren cambios importantes pueden producirse en circunstancias limitadas antes de la versión.

En este procedimiento, mostraremos cómo puede reemplazar el asignador predeterminado para una colección de registros del almacén de vectores por su propio asignador.

Usaremos Qdrant para demostrar esta funcionalidad, pero los conceptos serán similares para otros conectores.

Fondo

Cada conector de almacén de vectores incluye un asignador predeterminado que puede asignar desde el modelo de datos proporcionado al esquema de almacenamiento admitido por el almacén subyacente. Algunos almacenes permiten una gran libertad con respecto a cómo se almacenan los datos, mientras que otros almacenes requieren un enfoque más estructurado, por ejemplo, donde todos los vectores deben agregarse a un diccionario de vectores y todos los campos no vectoriales a un diccionario de campos de datos. Por lo tanto, la asignación es una parte importante de la abstracción de las diferencias de cada implementación del almacén de datos.

En algunos casos, es posible que el desarrollador quiera reemplazar el asignador predeterminado si, por ejemplo,

  1. quieren usar un modelo de datos que difiere del esquema de almacenamiento.
  2. quieren crear un asignador optimizado para el rendimiento para su escenario.
  3. El asignador predeterminado no admite una estructura de almacenamiento que requiera el desarrollador.

Todas las implementaciones del conector del almacén de vectores permiten proporcionar un asignador personalizado.

Diferencias por tipo de almacén de vectores

Los almacenes de datos subyacentes de cada conector de almacén de vectores tienen diferentes formas de almacenar datos. Por lo tanto, lo que se asigna a en el lado de almacenamiento puede diferir para cada conector.

Por ejemplo, si usa el conector Qdrant, el tipo de almacenamiento es una PointStruct clase proporcionada por el SDK de Qdrant. Si usa el conector JSON de Redis, el tipo de almacenamiento es una string clave y , JsonNodemientras que si usa un conector JSON HashSet, el tipo de almacenamiento es una string clave y una HashEntry matriz.

Si quiere realizar una asignación personalizada y quiere usar varios tipos de conector, por lo tanto, deberá implementar un asignador para cada tipo de conector.

Creación del modelo de datos

Nuestro primer paso es crear un modelo de datos. En este caso no anotaremos el modelo de datos con atributos, ya que proporcionaremos una definición de registro independiente que describa el aspecto del esquema de la base de datos.

Tenga en cuenta también que este modelo es complejo, con clases independientes para vectores e información adicional del producto.

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

Creación de la definición de registro

Es necesario crear una instancia de definición de registro para definir el aspecto del esquema de la base de datos. Normalmente, un conector requerirá esta información para realizar la asignación al usar el asignador predeterminado. Dado que estamos creando un asignador personalizado, esto no es necesario para la asignación; sin embargo, el conector seguirá necesitando esta información para crear colecciones en el almacén de datos.

Tenga en cuenta que la definición aquí es diferente al modelo de datos anterior. Para almacenar ProductInfo una propiedad de cadena denominada ProductInfoJson, y los dos vectores se definen en el mismo nivel que los Idcampos , Name y 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

En este escenario, no sería posible usar atributos en lugar de una definición de registro, ya que el esquema de almacenamiento no se parece al modelo de datos.

Creación del asignador personalizado

Todos los asignadores implementan la interfaz Microsoft.SemanticKernel.Data.IVectorStoreRecordMapper<TRecordDataModel, TStorageModel>genérica . TRecordDataModel variará en función del modelo de datos que el desarrollador quiera usar y TStorageModel lo determinará el tipo de almacén de vectores.

Para Qdrant TStorageModel es Qdrant.Client.Grpc.PointStruct.

Por lo tanto, tenemos que implementar un asignador que se asignará entre nuestro Product modelo de datos y 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 asignador personalizado con una colección de registros

Para usar el asignador personalizado que hemos creado, es necesario pasarlo a la colección de registros en tiempo de construcción. También es necesario pasar la definición de registro que creamos anteriormente, de modo que las colecciones se creen en el almacén de datos mediante el esquema correcto. Una configuración más importante aquí es el modo de vectores con nombre de Qdrant, ya que tenemos más de un vector. Sin este modo activado, solo se admite un vector.

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 de un asignador personalizado con IVectorStore

Al usar IVectorStore para obtener IVectorStoreRecordCollection instancias de objeto, no es posible proporcionar un asignador personalizado directamente al GetCollection método . Esto se debe a que los asignadores personalizados difieren para cada tipo de almacén de vectores y harían imposible usar IVectorStore la comunicación con cualquier implementación del almacén de vectores.

Sin embargo, es posible proporcionar una fábrica al construir una implementación del almacén de vectores. Esto se puede usar para personalizar IVectorStoreRecordCollection las instancias a medida que se crean.

Este es un ejemplo de este generador, que comprueba si CreateCollection se llamó con la definición del producto y el tipo de datos, y si es así, inserta el asignador personalizado y cambia en el modo de vectores con nombre.

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

Para usar el generador de colecciones, páselo al almacén de vectores al construirlo o al registrarlo con el contenedor de inserción de dependencias.

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

Ahora puede usar el almacén de vectores como normal para obtener una colección.

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

Próximamente

Más información próximamente.

Próximamente

Más información próximamente.