Compartilhar via


Como criar um mapeador personalizado para um conector do Repositório de Vetores (versão prévia)

Aviso

A funcionalidade do Repositório de Vetores do Kernel Semântico está em versão prévia e as melhorias que exigem alterações significativas ainda podem ocorrer em circunstâncias limitadas antes do lançamento.

Neste tutorial, mostraremos como você pode substituir o mapeador padrão de uma coleção de registros de armazenamento de vetores por seu próprio mapeador.

Usaremos o Qdrant para demonstrar essa funcionalidade, mas os conceitos serão semelhantes para outros conectores.

Tela de fundo

Cada conector do Repositório de Vetores inclui um mapeador padrão que pode ser mapeado do modelo de dados fornecido para o esquema de armazenamento compatível com o repositório subjacente. Alguns armazenamentos permitem muita liberdade em relação à forma como os dados são armazenados, enquanto outros armazenamentos exigem uma abordagem mais estruturada, por exemplo, onde todos os vetores devem ser adicionados a um dicionário de vetores e todos os campos não vetoriais a um dicionário de campos de dados. Portanto, o mapeamento é uma parte importante da abstração das diferenças de cada implementação de armazenamento de dados.

Em alguns casos, o desenvolvedor pode querer substituir o mapeador padrão se, por exemplo,

  1. Eles querem usar um modelo de dados diferente do esquema de armazenamento.
  2. Eles querem criar um mapeador otimizado para desempenho para seu cenário.
  3. O mapeador padrão não dá suporte a uma estrutura de armazenamento exigida pelo desenvolvedor.

Todas as implementações do conector do Repositório de Vetores permitem que você forneça um mapeador personalizado.

Diferenças por tipo de loja de vetores

Os armazenamentos de dados subjacentes de cada conector do Repositório de Vetores têm maneiras diferentes de armazenar dados. Portanto, o que você está mapeando no lado do armazenamento pode ser diferente para cada conector.

Por exemplo, se estiver usando o conector Qdrant, o tipo de armazenamento é uma PointStruct classe fornecida pelo Qdrant SDK. Se estiver usando o conector JSON Redis, o tipo de armazenamento será uma string chave e um JsonNode, enquanto se estiver usando um conector JSON HashSet, o tipo de armazenamento será uma string chave e uma HashEntry matriz.

Se você quiser fazer um mapeamento personalizado e usar vários tipos de conector, precisará implementar um mapeador para cada tipo de conector.

Criando o modelo de dados

Nosso primeiro passo é criar um modelo de dados. Nesse caso, não anotaremos o modelo de dados com atributos, pois forneceremos uma definição de registro separada que descreve a aparência do esquema de banco de dados.

Observe também que esse modelo é complexo, com classes separadas para vetores e informações adicionais do produto.

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

Criando a definição de registro

Precisamos criar uma instância de definição de registro para definir a aparência do esquema de banco de dados. Normalmente, um conector exigirá essas informações para fazer o mapeamento ao usar o mapeador padrão. Como estamos criando um mapeador personalizado, isso não é necessário para o mapeamento, no entanto, o conector ainda exigirá essas informações para criar coleções no armazenamento de dados.

Observe que a definição aqui é diferente do modelo de dados acima. Para armazenar ProductInfo , temos uma propriedade de string chamada ProductInfoJson, e os dois vetores são definidos no mesmo nível que os Idcampos , Name e 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

Para esse cenário, não seria possível usar atributos em vez de uma definição de registro, pois o esquema de armazenamento não se assemelha ao modelo de dados.

Criando o mapeador personalizado

Todos os mapeadores implementam a interface Microsoft.SemanticKernel.Data.IVectorStoreRecordMapper<TRecordDataModel, TStorageModel>genérica. TRecordDataModel será diferente dependendo do modelo de dados que o desenvolvedor deseja usar e TStorageModel será determinado pelo tipo de Repositório de Vetores.

Pois Qdrant TStorageModel é Qdrant.Client.Grpc.PointStruct.

Portanto, temos que implementar um mapeador que mapeará entre nosso Product modelo de dados e um 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;
    }
}

Usando seu mapeador personalizado com uma coleção de registros

Para usar o mapeador personalizado que criamos, precisamos passá-lo para a coleção de registros no momento da construção. Também precisamos passar a definição de registro que criamos anteriormente, para que as coleções sejam criadas no armazenamento de dados usando o esquema correto. Mais uma configuração importante aqui é o modo de vetores nomeados do Qdrant, já que temos mais de um vetor. Sem este modo ativado, apenas um vetor é suportado.

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

Usando um mapeador personalizado com IVectorStore

Ao usar IVectorStore para obter IVectorStoreRecordCollection instâncias de objeto, não é possível fornecer um mapeador personalizado diretamente para o GetCollection método. Isso ocorre porque os mapeadores personalizados diferem para cada tipo de Repositório de Vetores e impossibilitariam o uso IVectorStore para se comunicar com qualquer implementação de repositório de vetores.

No entanto, é possível fornecer uma fábrica ao construir uma implementação de Armazenamento de Vetores. Isso pode ser usado para personalizar IVectorStoreRecordCollection instâncias à medida que são criadas.

Aqui está um exemplo de tal fábrica, que verifica se CreateCollection foi chamado com a definição do produto e o tipo de dados e, em caso afirmativo, injeta o mapeador personalizado e ativa o modo de vetores nomeados.

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 a fábrica de coleções, passe-a para o Repositório de Vetores ao construí-la ou ao registrá-la no contêiner de injeção de dependência.

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

Agora você pode usar o armazenamento de vetores normalmente para obter uma coleção.

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

Em breve

Mais informações em breve.

Em breve

Mais informações em breve.