Skip to content

Instantly share code, notes, and snippets.

@roji
Created July 1, 2024 14:29
Show Gist options
  • Select an option

  • Save roji/f792e6e1c84abbeea7e0f1c63275cb97 to your computer and use it in GitHub Desktop.

Select an option

Save roji/f792e6e1c84abbeea7e0f1c63275cb97 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
// Note: QdrantVectorStoreClient is a thing I shoved in which basically wraps a typical vector DB client.
// In this sketch, it implements IVectorCollectionStore (which can be seen as a view over the SDK client's capabilities)
{
// Super basic, non-DI usage scenario, no database-specific options
var client = new QdrantVectorStoreClient(/* connection info */);
var schema = new VectorStoreSchema(/* schema info */);
var customerRecordStore = client.GetRecordStore<Guid, CustomerMetadata>("Customers", schema);
await customerRecordStore.CreateIfNotExists(); // Optional, depends if we know the store has already been created
// Use customerRecordStore
}
{
// Non-DI usage scenario, with database-specific options
var client = new QdrantVectorStoreClient(/* connection info */);
var schema = new VectorStoreSchema(/* schema info */);
var qdrantStoreConfig = new QdrantVectorStoreConfig { UseNamedVectors = true };
var customerRecordStore = client.GetRecordStore<Guid, CustomerMetadata>("Customers", schema, qdrantStoreConfig);
// Note that from this point, we can create and use the collection via the abstraction - IVectorRecordStore has
// closed over the Qdrant-specific details, such as UseNamedVectors
await customerRecordStore.CreateIfNotExists(); // Optional, depends if we know the store has already been created
// Use customerRecordStore
}
{
// DI usage, register single store (no key necessary), no database-specific options
var schema = new VectorStoreSchema(/* schema info */);
builder.AddQdrantVectorStore(/* login info */, "Customers", schema);
// In DI in general, there's the question of where/when the collection is to get created - that's a tricky question
// in general (with relational databases too), I'll not deal with that since it seems mostly orthogonal to the
// abstraction API shape.
}
{
// DI usage, register multiple stores, no database-specific options
var schema = new VectorStoreSchema(/* schema info */);
builder.AddKeyedQdrantVectorStore(/* login info */, "Customers", schema);
// An interesting idea would be to just use the store name as the key by default ("Customers"), rather than forcing
// the user to manually specify it. But at least an optional key: parameter is obviously needed.
}
{
// DI usage, register single store, with database-specific options
var schema = new VectorStoreSchema(/* schema info */);
var qdrantStoreConfig = new QdrantVectorStoreConfig { UseNamedVectors = true };
builder.AddQdrantVectorStore(/* login info */, "Customers", schema, qdrantStoreConfig);
}
{
// DI usage, register a client or collection store. Used in applications where collections need to be managed
// by the application (presumably not the common case)
builder.AddQdrantCollectionStore(/* login info */);
// Optionally also: builder.AddQdrantVectorStoreClient(/* login info */);
// Note that AddQdrantCollectionStore registers an IVectorCollectionStore in DI, but it could also register
// the QdrantVectorCollectionStore implementation directly. Code that is fine with the abstraction (no need for
// Qdrant-specific stuff) can just get injected with IVectorCollectionStore - this is important as it allows
// database-agnostic coding for the common case. Whereas code that needs to go into Qdrant-specific stuff can get
// injected with QdrantVectorCollectionStore.
}
{
// Multi-tenant non-DI usage scenario; two record stores, same schema
var client = new QdrantVectorStoreClient(/* connection info */);
var schema = new VectorStoreSchema(); // Populate actual schema
var customer1RecordStore = client.GetRecordStore<Guid, CustomerMetadata>("Customer1", schema);
await customer1RecordStore.CreateIfNotExists();
var customer2RecordStore = client.GetRecordStore<Guid, CustomerMetadata>("Customer2", schema);
await customer2RecordStore.CreateIfNotExists();
}
#region Abstraction types
// This effectively represents a view over the SDK client's capabilities
public interface IVectorCollectionStore
{
// schema is optional because we presumably support inferring it from the user-provided CLR type (attributes)
IVectorRecordStore<TKey, TRecord> GetRecordStore<TKey, TRecord>(string name, VectorStoreSchema? schema = null);
bool DropRecordStore(string name);
Task<List<string>> ListRecordStores();
// ...
}
public interface IVectorRecordStore<TKey, TRecord>
{
public string Name { get; }
Task Create();
Task<bool> CreateIfNotExists();
Task Drop();
// Data manipulation
Task<TRecord?> GetAsync(TKey key, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
IAsyncEnumerable<TRecord> GetBatchAsync(IEnumerable<TKey> keys, GetRecordOptions? options = default, CancellationToken cancellationToken = default);
Task DeleteAsync(TKey key, DeleteRecordOptions? options = default, CancellationToken cancellationToken = default);
Task DeleteBatchAsync(IEnumerable<TKey> keys, DeleteRecordOptions? options = default, CancellationToken cancellationToken = default);
Task<TKey> UpsertAsync(TRecord record, UpsertRecordOptions? options = default, CancellationToken cancellationToken = default);
IAsyncEnumerable<TKey> UpsertBatchAsync(IEnumerable<TRecord> records, UpsertRecordOptions? options = default, CancellationToken cancellationToken = default);
}
public class VectorStoreSchema;
#endregion Abstraction types
#region Sample implementation stub (Qdrant)
public class QdrantVectorStoreClient : IVectorCollectionStore
{
// This is the implementation of the database-agnostic abstraction, no Qdrant-specific things
public IVectorRecordStore<TKey, TRecord> GetRecordStore<TKey, TRecord>(string name, VectorStoreSchema? schema = null)
=> throw new NotImplementedException();
// Thiis
public QdrantVectorRecordStore<TKey, TRecord> GetRecordStore<TKey, TRecord>(string name, VectorStoreSchema? schema = null, QdrantVectorStoreConfig? qdrantConfig = null)
=> throw new NotImplementedException();
public bool DropRecordStore(string name)
=> throw new NotImplementedException();
public Task<List<string>> ListRecordStores()
=> throw new NotImplementedException();
}
public class QdrantVectorRecordStore<TKey, TRecord> : IVectorRecordStore<TKey, TRecord>
{
public string Name { get; }
// Implementation of the abstraction operations
public Task Create() => throw new NotImplementedException();
public Task<bool> CreateIfNotExists() => throw new NotImplementedException();
public Task Drop() => throw new NotImplementedException();
public Task<TRecord?> GetAsync(TKey key, GetRecordOptions? options = default, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public IAsyncEnumerable<TRecord> GetBatchAsync(IEnumerable<TKey> keys, GetRecordOptions? options = default,
CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task DeleteAsync(TKey key, DeleteRecordOptions? options = default, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task DeleteBatchAsync(IEnumerable<TKey> keys, DeleteRecordOptions? options = default,
CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<TKey> UpsertAsync(TRecord record, UpsertRecordOptions? options = default, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public IAsyncEnumerable<TKey> UpsertBatchAsync(IEnumerable<TRecord> records, UpsertRecordOptions? options = default,
CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
// Qdrant-specific operations (or variants of the above operations) can be added here
}
// Note that there could be a QdrantVectorStoreSchema, extending VectorStoreSchema to add Qdrant-specific properties;
// this would be necessary if you want to allow users to tweak Qdrant-specific stuff
// Note that this *could* simply extend VectorStoreSchema, adding Qdrant-specific properties.
// But that would make it harder to specify Qdrant-specific properties when using attributes, i.e. when not passing
// in a schema object. So it may be better to keep the abstraction type (VectorStoreSchema) and the
public class QdrantVectorStoreConfig
{
public bool UseNamedVectors { get; set; }
}
#endregion Sample implementation stub (Qdrant)
#region User types
public class CustomerMetadata;
#endregion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment