Created
July 1, 2024 14:29
-
-
Save roji/f792e6e1c84abbeea7e0f1c63275cb97 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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