Skip to content

Commit

Permalink
Further refinement
Browse files Browse the repository at this point in the history
- Document List operations
- Document parallel operation exceptions
- Fix bugs in List operations
- Fix F# example
- Fix tests build errors
- Remove UnsafeAccessor use until .NET 9
- Factor out duplicated logic into helpers
- Update readme/description
  • Loading branch information
neon-sunset committed Sep 25, 2024
1 parent d125d26 commit bd37f73
Show file tree
Hide file tree
Showing 17 changed files with 298 additions and 266 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ In the absence of an official SDK, it provides first-class support for Pinecone
- Standard operations on pod-based and serverless indexes
- gRPC and REST transports for vector operations
- Sparse-dense vectors
- Automatic batching and parallelization for upsert, fetch and delete operations
- Efficient vector serialization
- Metadata support
- Efficient vector serialization
- NativeAOT compatibility (e.g. for AWS Lambda)
- Automatic batching and parallelization for upsert, fetch and delete operations
- Preservation of data on partial failures for streaming and batched operations

## Installation

Expand Down Expand Up @@ -123,4 +124,4 @@ await pinecone.DeleteCollection("myCollection");

## Contributing

Contributions are welcome! Feel free open an issue or a PR.
Contributions are welcome! Feel free to open an issue or a PR.
12 changes: 8 additions & 4 deletions example/Example.FSharp/Program.fs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#nowarn "3391"
open Pinecone
open System
open System.Collections.Generic
open Pinecone

let createMetadata x =
MetadataMap(x |> Seq.map (fun (k, m) -> KeyValuePair(k,m) ))

let getRandomVector size =
Array.init size (fun _ -> Random.Shared.NextSingle())

let main = task {
use pinecone = new PineconeClient("[api-key]")

Expand All @@ -23,8 +27,8 @@ let main = task {
use! index = pinecone.GetIndex(indexName)

let tags = [|"tag1" ; "tag2"|]
let first = Vector(Id = "first", Values = Array.zeroCreate 1536, Metadata = createMetadata["new", true; "price", 50; "tags", tags])
let second = Vector(Id = "second", Values = Array.zeroCreate 1536, Metadata = createMetadata["price", 50])
let first = Vector(Id = "first", Values = getRandomVector 1536, Metadata = createMetadata["new", true; "price", 50; "tags", tags])
let second = Vector(Id = "second", Values = getRandomVector 1536, Metadata = createMetadata["price", 50])

// Upsert vectors into the index
let! _ = index.Upsert [|first; second|]
Expand All @@ -36,7 +40,7 @@ let main = task {
let priceRange = createMetadata["price", createMetadata["$gte", 75; "$lte", 125]]

// Query the index by embedding and metadata filter
let! results = index.Query((Array.zeroCreate 1536), 3u, filter = priceRange, includeMetadata = true)
let! results = index.Query(getRandomVector 1536, 3u, filter = priceRange, includeMetadata = true)
let metadata =
results
|> Seq.collect _.Metadata
Expand Down
8 changes: 5 additions & 3 deletions src/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace Pinecone;

internal static class Constants
static class Constants
{
public const string RestApiKey = "Api-Key";
public const string GrpcApiKey = "api-key";

public static readonly string Version =
typeof(Constants).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
public const string ApiVersion = "2024-07";

public static readonly string UserAgent =
$"lang=C#; Pinecone.NET/{typeof(Constants).Assembly.GetName().Version?.ToString(3) ?? "0.0.0"}";

public static readonly HttpClientFactoryOptions RedactApiKeyOptions = new()
{
Expand Down
20 changes: 15 additions & 5 deletions src/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Grpc.Core;
using Microsoft.Extensions.Http.Logging;
using Microsoft.Extensions.Logging;

namespace Pinecone;

internal static class Extensions
static class Extensions
{
internal static void AddPineconeHeaders(this HttpClient http, string apiKey)
{
Expand All @@ -14,20 +16,28 @@ internal static void AddPineconeHeaders(this HttpClient http, string apiKey)
if (!headers.Contains(Constants.RestApiKey))
headers.Add(Constants.RestApiKey, apiKey);
if (!headers.Contains("X-Pinecone-Api-Version"))
headers.Add("X-Pinecone-Api-Version", "2024-07");
headers.Add("X-Pinecone-Api-Version", Constants.ApiVersion);
if (!headers.Contains("User-Agent"))
headers.TryAddWithoutValidation("User-Agent", $"lang=C#; Pinecone.NET/{Constants.Version}");
headers.TryAddWithoutValidation("User-Agent", Constants.UserAgent);
}

internal static Metadata WithPineconeProps(this Metadata metadata, string apiKey)
{
metadata.Add(Constants.GrpcApiKey, apiKey);
metadata.Add("X-Pinecone-Api-Version", "2024-07");
metadata.Add("User-Agent", $"lang=C#; Pinecone.NET/{Constants.Version}");
metadata.Add("X-Pinecone-Api-Version", Constants.ApiVersion);
metadata.Add("User-Agent", Constants.UserAgent);

return metadata;
}

internal static HttpMessageHandler CreateLoggingHandler(this ILoggerFactory factory)
{
return new LoggingHttpMessageHandler(
factory.CreateLogger<HttpClient>(),
Constants.RedactApiKeyOptions)
{ InnerHandler = new HttpClientHandler() };
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ValueTask CheckStatusCode(this HttpResponseMessage response, CancellationToken ct, [CallerMemberName] string requestName = "")
{
Expand Down
100 changes: 19 additions & 81 deletions src/Grpc/Converters.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;

namespace Pinecone.Grpc;

internal static class Converters
static class Converters
{
static readonly Value NullValue = Value.ForNull();
static readonly Value TrueValue = Value.ForBool(true);
static readonly Value FalseValue = Value.ForBool(false);

// gRPC types conversion to sane and usable ones
public static Struct ToProtoStruct(this MetadataMap source)
{
Expand All @@ -23,10 +26,10 @@ public static Struct ToProtoStruct(this MetadataMap source)
public static Value ToProtoValue(this MetadataValue source) => source.Inner switch
{
// This is terrible but such is life
null => Value.ForNull(),
null => NullValue,
double num => Value.ForNumber(num),
string str => Value.ForString(str),
bool boolean => Value.ForBool(boolean),
bool boolean => boolean ? TrueValue : FalseValue,
MetadataMap nested => Value.ForStruct(nested.ToProtoStruct()),
IEnumerable<MetadataValue> list => Value.ForList(list.Select(v => v.ToProtoValue()).ToArray()),
_ => ThrowHelpers.ArgumentException<Value>($"Unsupported metadata type: {source.Inner!.GetType()}")
Expand Down Expand Up @@ -119,75 +122,6 @@ Value.KindOneofCase.None or
};
}

#if NET8_0_OR_GREATER
// These have to be duplicated because unsafe accessor does not support generics in .NET 8.
// This approach is, however, very useful as we completely bypass referencing reflection for NAOT.
public static ReadOnlyMemory<float> AsMemory(this RepeatedField<float> source)
{
return ArrayRef(source).AsMemory(0, source.Count);
}

public static void OverwriteWith(this RepeatedField<float> target, ReadOnlyMemory<float>? source)
{
if (source is null or { IsEmpty: true }) return;

float[] array;
int count;
if (MemoryMarshal.TryGetArray(source.Value, out var segment)
&& segment.Offset is 0)
{
array = segment.Array!;
count = segment.Count;
}
else
{
array = source.Value.ToArray();
count = array.Length;
}

ArrayRef(target) = array;
CountRef(target) = count;
}

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "array")]
static extern ref float[] ArrayRef(RepeatedField<float> instance);

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "count")]
static extern ref int CountRef(RepeatedField<float> instance);

public static ReadOnlyMemory<uint> AsMemory(this RepeatedField<uint> source)
{
return ArrayRef(source).AsMemory(0, source.Count);
}

public static void OverwriteWith(this RepeatedField<uint> target, ReadOnlyMemory<uint>? source)
{
if (source is null or { IsEmpty: true }) return;

uint[] array;
int count;
if (MemoryMarshal.TryGetArray(source.Value, out var segment)
&& segment.Offset is 0)
{
array = segment.Array!;
count = segment.Count;
}
else
{
array = source.Value.ToArray();
count = array.Length;
}

ArrayRef(target) = array;
CountRef(target) = count;
}

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "array")]
static extern ref uint[] ArrayRef(RepeatedField<uint> instance);

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "count")]
static extern ref int CountRef(RepeatedField<uint> instance);
#else
public static ReadOnlyMemory<T> AsMemory<T>(this RepeatedField<T> source)
where T : unmanaged
{
Expand Down Expand Up @@ -217,8 +151,19 @@ public static void OverwriteWith<T>(this RepeatedField<T> target, ReadOnlyMemory
FieldAccessors<T>.SetCount(target, count);
}

private static class FieldAccessors<T> where T : unmanaged
// UnsafeAccessor path was removed because turns out the support for
// specified generics was added unintentionally and breaks on .NET 9.
// See https://github.com/dotnet/runtime/issues/108046
// TODO: Once .NET 9 is out, bring back UnsafeAccessor path using the
// pattern described as the solution in the issue above.
static class FieldAccessors<T> where T : unmanaged
{
static readonly FieldInfo ArrayField = typeof(RepeatedField<T>)
.GetField("array", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new NullReferenceException();

static readonly FieldInfo CountField = typeof(RepeatedField<T>)
.GetField("count", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new NullReferenceException();

public static T[] GetArray(RepeatedField<T> instance)
{
return (T[])ArrayField.GetValue(instance)!;
Expand All @@ -233,12 +178,5 @@ public static void SetCount(RepeatedField<T> instance, int value)
{
CountField.SetValue(instance, value);
}

static readonly FieldInfo ArrayField = typeof(RepeatedField<T>)
.GetField("array", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new NullReferenceException();

static readonly FieldInfo CountField = typeof(RepeatedField<T>)
.GetField("count", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new NullReferenceException();
}
#endif
}
16 changes: 8 additions & 8 deletions src/Grpc/GrpcTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,20 +145,20 @@ public async Task Update(
string? indexNamespace = null,
CancellationToken ct = default)
{
var request = new ListRequest
{
Prefix = prefix ?? "",
Limit = limit ?? 0,
PaginationToken = paginationToken ?? "",
Namespace = indexNamespace ?? ""
};
var request = new ListRequest { Namespace = indexNamespace ?? "" };
if (prefix != null)
request.Prefix = prefix;
if (limit != null)
request.Limit = limit.Value;
if (paginationToken != null)
request.PaginationToken = paginationToken;

using var call = Grpc.ListAsync(request, Metadata, cancellationToken: ct);
var response = await call.ConfigureAwait(false);

return (
response.Vectors.Select(v => v.Id).ToArray(),
response.Pagination.Next,
response.Pagination?.Next,
response.Usage.ReadUnits);
}

Expand Down
Loading

0 comments on commit bd37f73

Please sign in to comment.