Can anyone provide an example of creating an index out of an embedding with Pinecone.Net's Neon Sunset wrapper library?

I’m trying to implement RAG using Open AI’s API to create the embeddings and Pinecone to store them as vector indexes… But I’m doing all of this in .Net and the examples are sparse… My RAG is to be able to query an Plain Old C# Object of User data. It’s a complex type with many properties and nested objects, which I iterate using reflection. Upon finding a property, I want to create it’s embedding and index, in order for my queries to be both granular (“what’s the user’s age?”) but also capturing the relations between properties and overall user object (“what age was the user then he started working at XYXY?”). For this, I’m embedding from lower levels to higher level members of my POCO.
It’s not clear to me, for instance, how can I use Pinecone.Net to create the Index for each embedding. After getting an EmbeddingsResponse object from OpenAI, I pass it’s nested Data to a List:

async Task CreateIndex(EmbeddingsResponse responseEmbedding, PineconeClient pinecone)
{
    List<IReadOnlyList<double>>? embeds = responseEmbedding.Data.Select(x => x.Embedding).ToList();
    var indexDetails = new IndexDetails()
    {
        Name = "myChunk",
        Dimension = 1536,
        Metric = Metric.Cosine,
        
    };
    await pinecone.CreateIndex(indexDetails);
}

But to create the Index, the IndexDetails class has limited parameters:

 protected IndexDetails(IndexDetails original)
 {
     Name = original.Name;
     Dimension = original.Dimension;
     Metric = original.Metric;
     Pods = original.Pods;
     PodType = original.PodType;
     Replicas = original.Replicas;
     MetadataConfig = original.MetadataConfig;
 }

How can I populate this index with my embeds ? None of the parameters seem adequate to receive such a structure… Anyone has examples of indexing an embedding from OpenAi and querying an index?

1 Like

Indexes store embeddings and cannot be created from a single particular one. Think index is database~table and an embedding vector (with, possibly, metadata) is a row.

Embedding is just a float sequence you can create a vector from

using var index = await pinecone.GetIndex("[index-name]");

var vector = new Vector
{
    Id = /* your vector id */,
    Values = /* float[] from OpenAI, if it's a list of float sequences, just pick a first one */
}

await index.Upsert([vector]);

If this doesn’t work for you, feel free to create an issue/discussion on GitHub and I’ll take a look :slight_smile:

In the meantime, you can check out the minimal C# and F# examples which show the basic usage of the library.

Otherwise, using the library is identical to how you’d use the official ones except less stability issues because .NET has much better gRPC tooling quality.

3 Likes

Thank you, fortunately I arrived at that implementation from Pinecone.Net’s github repo. However, I’m still struggling to have a clear picture of how I should group together different indexes… Upon chatting with Pinecone’s bot, which I assume is informed by all their documentation (which is awesome btw), I concluded this can be done with either clusters, namespaces, metadata or a ‘mapping file’. I chose to implement it using metadata because Pinecone’s API doesn’t have any methods that use namespaces, contrary to the index creation method on the direct original Rest implementation (and clusters and mapping files seem like hallucinations, but correct me if I’m wrong). This is how I’m traversing the object with recursion, and creating the embedding and the index for each property found with reflection:

 async Task TraversePropertiesAndCreateIndex(object obj, string indexClusterId, PineconeClient? pinecone)
 {
     if (obj == null)
         return;

     Type type = obj.GetType();
     PropertyInfo[] properties = type.GetProperties();

     foreach (PropertyInfo property in properties)
     {
         if (property.Name == "Password")     //ignore passwords
             continue;
        
         object value = property.GetValue(obj);

         var responseEmbedding = await EmbedValue(value);

         string typeGuid = obj.GetType().GUID.ToString();
         string propUniqueId = $"{indexClusterId}_{typeGuid}_{property.Name}";

         await CreateIndex(responseEmbedding, pinecone, propUniqueId, indexClusterId);


         if (value != null && property.PropertyType.Namespace.StartsWith("Domain.Models"))
         {
             TraversePropertiesAndCreateIndex(value, indexClusterId, pinecone);
         }

         Console.WriteLine($"{property.Name}: {value}");
     }
 }

 async Task<EmbeddingsResponse> EmbedValue(object? value)
 {
     using var openAI = new OpenAIClient(_externalApisOptions.OpenAIKey);
     var embeddingsRequest = new EmbeddingsRequest(value.ToString(), Model.Embedding_Ada_002);
     return await openAI.EmbeddingsEndpoint.CreateEmbeddingAsync(embeddingsRequest);
 }

 async Task CreateIndex(EmbeddingsResponse responseEmbedding, PineconeClient pinecone, string propUniqueId, string indexClusterId)
 {
     var embeddings = responseEmbedding.Data.Select(x => x.Embedding).ToList();
     var indexDetails = new IndexDetails()
     {
         Name = propUniqueId,
         Dimension = 1536,              //the supposed number of dimensions of the OpenAI embeddings
         Metric = Metric.Cosine,
         
     };
     await pinecone.CreateIndex(indexDetails);

     var wasCreated = (await pinecone.ListIndexes()).Contains(propUniqueId);
     if (wasCreated)
     {
         float[] embeddingsFloatArray = ConvertToFloatArray(embeddings);

         var index = await pinecone.GetIndex(propUniqueId);

         var vectors = new[]
         {
             new Vector
             {
                 Id = propUniqueId,
                 Values = embeddingsFloatArray,
                 Metadata = new MetadataMap
                 {
                     ["group"] = $"{indexClusterId}",
                 }
             }
         };

         await index.Upsert(vectors);

     }
 }

Above, indexClusterId is going to be the object’s pointer to all embedded vectors, because to be honest I’m pretty clueless as to how to include namespaces. The Name of the Embedding and the Id of the Vector are the same, as I’ve made it a unique value and had no idea what should I use the vector Id for… I’m crossing my fingers at this point! This is how I will send over the Query:

 async Task<Vector> QueryIndex(EmbeddingsResponse responseEmbedding, PineconeClient pinecone, string indexClusterId)
 {
     var embeddings = responseEmbedding.Data.Select(x => x.Embedding).ToList();
     var indexName = $"query_{Guid.NewGuid().ToString()}";
     var indexDetails = new IndexDetails()
     {
         Name = indexName,
         Dimension = 1536,
         Metric = Metric.Cosine,
     };

     await pinecone.CreateIndex(indexDetails);

     var wasCreated = (await pinecone.ListIndexes()).Contains(indexName);
     if (wasCreated)
     {
         float[] embeddingsFloatArray = ConvertToFloatArray(embeddings);

         var index = await pinecone.GetIndex(indexName);

         var vectors = new[]
         {
             new Vector
             {
                 Id = indexName,
                 Values = embeddingsFloatArray,
                 Metadata = new MetadataMap
                 {
                     ["group"] = indexClusterId,
                 }
             }
         };

         var queryIndexId = await index.Upsert(vectors);

         var responses = await index.Query(queryIndexId.ToString(), topK: 10, includeValues: true, includeMetadata: true) ;

         //cleanup
         await index.Delete(new[] { indexName });

         var doesQueryIndexStillExist = (await pinecone.ListIndexes()).Contains(indexName);

         return responses;
     }
 }

I’m still in the process of testing this, and I will post any further updates, but I appreciate any pointers to how to correctly implement RAG to store and query complex objects that are unique to each user or tips for any improvements. Thanks!

In order to use the API successfully I strongly suggest reading through official documentation and API definition first - the latter especially is very simple.

The code in the post does not achieve the desired goal (uses the API incorrectly, ignores the comments in the example, creates and then deletes the index which is like creating a database only to delete it). Pinecone is a vector DB. This means it is designed to store and query vectors either by their similarity or by their ID while applying metadata filtering - it is not designed to be used as a standard DB.

Upserting and querying is easy and does not need complex code:

class MyVectorDbService
{
    // Do not create or, worse, delete index on each operation.
    // Cache index client instance somewhere and do not request it on each time.
    Index<GrpcTransport> Index { get; }

    public async Task Upsert(EmbeddingsResponse response /*, other args */)
    {
        var vector = new Vector
        {
            Id = /* vector id goes here */,
            Values = response
                .Data[0]
                .Embedding
                .Select(float.CreateTruncating)
                .ToArray(),
            Metadata = new()
            {   /* Example */
                ["tenantId"] = "contoso-tenant"
            }
        };

        await Index.Upsert([vector] /*, optional namespace */);
    }

    public Task<ScoredVector[]> Query(EmbeddingsResponse response)
    {
        var values = response
            .Data[0]
            .Embedding
            // OpenAI-DotNet is either auto-generated or has a mistake and
            // uses wrong floating point type here so we need to convert it.
            .Select(float.CreateTruncating)
            .ToArray();
        
        return Index.Query(values, topK: 5 /*, optional args */);
    }
}
2 Likes

So, “an index should be used like a table”… Thank you for clarifying and I will correct my code accordingly. But I’m still not sure about 2 things…

  1. Should semantic meanings within my object have several hierarchies?.. For instance, a User will have a property of type JobExperiences, which will have the StartDate, EndDate, Company, Description, etc… Then he has a Skills property… When queried for which company he worked for, the query shouldn’t spit out a skill, nor a hobby… Also, the queries themselves that are to be compared with the records will have multiple unpredictable forms, like open ended questions, yes/no questions and those will require --different levels of granularity–, for instance, a skill name or a full description of a job experience spanning over several paragraphs. I searched quite a bit in the Pinecone documentation to understand the algorithms used to store and search vectors. Until now I understand cosine similarity (or dot products or euclidean distance) will match a query to vectors stored via their index. Doesn’t this mean there should be different levels of indices, according to the semantic meanings one wishes to be captured?
  2. Can I / should I use a namespace for at least the purpose of isolating different user’s data?
    Please be pacient with me, this is a new area for me.
    Thanks again for all the help!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.