Dela via


Så här skapar du en anpassad mappare för en Vector Store-anslutning (förhandsversion)

Varning

Funktionen Semantic Kernel Vector Store är i förhandsversion, och förbättringar som kräver icke-bakåtkompatibla ändringar kan fortfarande ske under begränsade omständigheter före lanseringen.

I det här gör vi så här för att visa hur du kan ersätta standardmapparen för en vektorlagringspostsamling med din egen mappare.

Vi kommer att använda Qdrant för att demonstrera den här funktionen, men begreppen kommer att likna andra anslutningsappar.

Bakgrund

Varje Vector Store-anslutningsprogram innehåller en standardmappare som kan mappa från den angivna datamodellen till det lagringsschema som stöds av det underliggande arkivet. Vissa lager ger mycket frihet när det gäller hur data lagras medan andra lager kräver en mer strukturerad metod, t.ex. där alla vektorer måste läggas till i en ordlista med vektorer och alla icke-vektorfält i en ordlista med datafält. Därför är mappning en viktig del av abstraktionen av skillnaderna i varje implementering av datalager.

I vissa fall kanske utvecklaren vill ersätta standardmapparen om t.ex.

  1. de vill använda en datamodell som skiljer sig från lagringsschemat.
  2. de vill skapa en prestandaoptimerad mappare för sitt scenario.
  3. standardmapparen stöder inte en lagringsstruktur som krävs av utvecklaren.

Med alla implementeringar av Vector Store-anslutningsappen kan du ange en anpassad mappare.

Skillnader efter typ av vektorlager

De underliggande datalagren för varje Vector Store-anslutningsprogram har olika sätt att lagra data. Därför kan det du mappar till på lagringssidan skilja sig åt för varje anslutningsapp.

Om du till exempel använder Qdrant-anslutningsappen är lagringstypen en PointStruct klass som tillhandahålls av Qdrant SDK. Om du använder Redis JSON-anslutningsappen är lagringstypen en string nyckel och en JsonNode. Om du använder en JSON HashSet-anslutning är lagringstypen en string nyckel och en HashEntry matris.

Om du vill göra anpassad mappning och vill använda flera anslutningstyper måste du därför implementera en mappare för varje anslutningstyp.

Skapa datamodellen

Vårt första steg är att skapa en datamodell. I det här fallet kommenterar vi inte datamodellen med attribut, eftersom vi tillhandahåller en separat postdefinition som beskriver hur databasschemat ska se ut.

Observera också att den här modellen är komplex, med separata klasser för vektorer och ytterligare produktinformation.

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

Skapa postdefinitionen

Vi måste skapa en postdefinitionsinstans för att definiera hur databasschemat ska se ut. Normalt kräver en anslutningsapp den här informationen för att mappa när du använder standardmapparen. Eftersom vi skapar en anpassad mappning krävs inte detta för mappning, men anslutningsappen kräver fortfarande den här informationen för att skapa samlingar i datalagret.

Observera att definitionen här skiljer sig från datamodellen ovan. För att lagra ProductInfo har vi en strängegenskap som heter ProductInfoJson, och de två vektorerna definieras på samma nivå som fälten Idoch 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 }
    }
};

Viktigt!

I det här scenariot skulle det inte vara möjligt att använda attribut i stället för en postdefinition eftersom lagringsschemat inte liknar datamodellen.

Skapa den anpassade mappningen

Alla mappare implementerar det generiska gränssnittet Microsoft.SemanticKernel.Data.IVectorStoreRecordMapper<TRecordDataModel, TStorageModel>. TRecordDataModel varierar beroende på vilken datamodell utvecklaren vill använda och TStorageModel bestäms av typen av Vector Store.

För Qdrant TStorageModel är Qdrant.Client.Grpc.PointStruct.

Därför måste vi implementera en mappare som mappar mellan vår Product datamodell och en 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;
    }
}

Använda din anpassade mappning med en postsamling

Om du vill använda den anpassade mappning som vi har skapat måste vi skicka den till postsamlingen vid byggtiden. Vi måste också skicka den postdefinition som vi skapade tidigare, så att samlingar skapas i datalagret med rätt schema. En annan inställning som är viktig här är Qdrants namngivna vektorläge, eftersom vi har mer än en vektor. Utan att det här läget är aktiverat stöds endast en vektor.

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

Använda en anpassad mappare med IVectorStore

När du använder IVectorStore för att hämta IVectorStoreRecordCollection objektinstanser är det inte möjligt att tillhandahålla en anpassad mappare direkt till GetCollection metoden. Det beror på att anpassade mappare skiljer sig åt för varje typ av vektorlager och gör det omöjligt att använda IVectorStore för att kommunicera med någon implementering av vektorlager.

Det är dock möjligt att tillhandahålla en fabrik när du skapar en Vector Store-implementering. Detta kan användas för att anpassa IVectorStoreRecordCollection instanser när de skapas.

Här är ett exempel på en sådan fabrik, som kontrollerar om CreateCollection anropades med produktdefinitionen och datatypen, och i så fall matar in den anpassade mapparen och växlar i namngivet vektorläge.

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

Om du vill använda samlingsfabriken skickar du den till Vector Store när du skapar den eller när du registrerar den med containern för beroendeinmatning.

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

Nu kan du använda vektorarkivet som vanligt för att hämta en samling.

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

Kommer snart

Mer information kommer snart.

Kommer snart

Mer information kommer snart.