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,
- Eles querem usar um modelo de dados diferente do esquema de armazenamento.
- Eles querem criar um mapeador otimizado para desempenho para seu cenário.
- 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 Id
campos , 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.