-
Notifications
You must be signed in to change notification settings - Fork 4k
.Net MEVD: DI for relational DBs #12043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
a0e3563
bbb2842
492fbd7
652e62a
f4f0a36
55e5bf6
88bc437
523b319
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is literally a copy of the SqlServer file with a |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,84 +1,227 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using Microsoft.Data.Sqlite; | ||
using System; | ||
using Microsoft.Extensions.AI; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.VectorData; | ||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.Connectors.SqliteVec; | ||
|
||
namespace Microsoft.SemanticKernel; | ||
namespace Microsoft.Extensions.DependencyInjection; | ||
|
||
/// <summary> | ||
/// Extension methods to register SQLite <see cref="VectorStore"/> instances on an <see cref="IServiceCollection"/>. | ||
/// </summary> | ||
public static class SqliteServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// Register a SQLite <see cref="VectorStore"/> with the specified service ID. | ||
/// <see cref="SqliteConnection"/> instance will be initialized, connection will be opened and vector search extension with be loaded. | ||
/// Registers a <see cref="SqliteVectorStore"/> as <see cref="VectorStore"/>, with the specified connection string and service lifetime. | ||
/// </summary> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to register the <see cref="VectorStore"/> on.</param> | ||
/// <param name="connectionString">Connection string for <see cref="SqliteConnection"/>.</param> | ||
/// <param name="options">Optional options to further configure the <see cref="VectorStore"/>.</param> | ||
/// <param name="serviceId">An optional service id to use as the service key.</param> | ||
/// <returns>Service collection.</returns> | ||
/// <inheritdoc cref="AddVectorStore"/> | ||
public static IServiceCollection AddSqliteVectorStore( | ||
this IServiceCollection services, | ||
string connectionString, | ||
SqliteVectorStoreOptions? options = default, | ||
string? serviceId = default) | ||
=> services.AddKeyedSingleton<VectorStore>( | ||
serviceId, | ||
(sp, _) => new SqliteVectorStore(connectionString, options ?? sp.GetService<SqliteVectorStoreOptions>() ?? new() { EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>() })); | ||
Func<IServiceProvider, string> connectionStringProvider, | ||
adamsitnik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Func<IServiceProvider, SqliteVectorStoreOptions>? optionsProvider = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
=> AddVectorStore(services, serviceKey: null, connectionStringProvider, optionsProvider, lifetime); | ||
|
||
/// <inheritdoc cref="AddVectorStore"/> | ||
public static IServiceCollection AddKeyedSqliteVectorStore( | ||
this IServiceCollection services, | ||
object serviceKey, | ||
Func<IServiceProvider, string> connectionStringProvider, | ||
Func<IServiceProvider, SqliteVectorStoreOptions>? optionsProvider = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
{ | ||
Verify.NotNull(serviceKey); | ||
|
||
return AddVectorStore(services, serviceKey, connectionStringProvider, optionsProvider, lifetime); | ||
} | ||
|
||
/// <summary> | ||
/// Registers a keyed <see cref="SqliteVectorStore"/> as <see cref="VectorStore"/>, with the specified connection string and service lifetime. | ||
/// </summary> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to register the <see cref="VectorStore"/> on.</param> | ||
/// <param name="serviceKey">The key with which to associate the vector store.</param> | ||
/// <param name="connectionStringProvider">The connection string provider.</param> | ||
/// <param name="optionsProvider">Options provider to further configure the vector store.</param> | ||
/// <param name="lifetime">The service lifetime for the store. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param> | ||
/// <returns>The service collection.</returns> | ||
private static IServiceCollection AddVectorStore( | ||
IServiceCollection services, | ||
object? serviceKey, | ||
Func<IServiceProvider, string> connectionStringProvider, | ||
Func<IServiceProvider, SqliteVectorStoreOptions>? optionsProvider, | ||
ServiceLifetime lifetime) | ||
{ | ||
Verify.NotNull(services); | ||
Verify.NotNull(connectionStringProvider); | ||
|
||
services.Add(new ServiceDescriptor(typeof(SqliteVectorStore), serviceKey, (sp, _) => | ||
{ | ||
var connectionString = connectionStringProvider(sp); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I still slightly prefer just inlining these variables and making everything a single lambda expression without a block - but not important. |
||
var options = GetStoreOptions(sp, optionsProvider); | ||
return new SqliteVectorStore(connectionString, options); | ||
}, lifetime)); | ||
|
||
services.Add(new ServiceDescriptor(typeof(VectorStore), serviceKey, | ||
static (sp, key) => sp.GetRequiredKeyedService<SqliteVectorStore>(key), lifetime)); | ||
|
||
return services; | ||
} | ||
|
||
/// <summary> | ||
/// Registers a <see cref="SqliteCollection{TKey, TRecord}"/> as <see cref="VectorStoreCollection{TKey, TRecord}"/>, with the specified connection string and service lifetime. | ||
/// </summary> | ||
/// <inheritdoc cref="AddCollection{TKey, TRecord}(IServiceCollection, object?, string, Func{IServiceProvider, string}, Func{IServiceProvider, SqliteCollectionOptions}?, ServiceLifetime)"/> | ||
public static IServiceCollection AddSqliteCollection<TKey, TRecord>( | ||
this IServiceCollection services, | ||
string collectionName, | ||
Func<IServiceProvider, string> connectionStringProvider, | ||
Func<IServiceProvider, SqliteCollectionOptions>? optionsProvider = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
where TKey : notnull | ||
where TRecord : class | ||
=> AddCollection<TKey, TRecord>(services, serviceKey: null, collectionName, connectionStringProvider, optionsProvider, lifetime); | ||
|
||
/// <inheritdoc cref="AddCollection{TKey, TRecord}(IServiceCollection, object?, string, Func{IServiceProvider, string}, Func{IServiceProvider, SqliteCollectionOptions}?, ServiceLifetime)"/> | ||
public static IServiceCollection AddKeyedSqliteCollection<TKey, TRecord>( | ||
this IServiceCollection services, | ||
object serviceKey, | ||
string collectionName, | ||
Func<IServiceProvider, string> connectionStringProvider, | ||
Func<IServiceProvider, SqliteCollectionOptions>? optionsProvider = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
where TKey : notnull | ||
where TRecord : class | ||
{ | ||
Verify.NotNull(serviceKey); | ||
|
||
return AddCollection<TKey, TRecord>(services, serviceKey, collectionName, connectionStringProvider, optionsProvider, lifetime); | ||
} | ||
|
||
/// <summary> | ||
/// Register a SQLite <see cref="VectorStoreCollection{TKey, TRecord}"/> and <see cref="IVectorSearchable{TRecord}"/> with the specified service ID. | ||
/// <see cref="SqliteConnection"/> instance will be initialized, connection will be opened and vector search extension with be loaded. | ||
/// Registers a keyed <see cref="SqliteCollection{TKey, TRecord}"/> as <see cref="VectorStoreCollection{TKey, TRecord}"/>, with the specified connection string and service lifetime. | ||
/// </summary> | ||
/// <typeparam name="TKey">The type of the key.</typeparam> | ||
/// <typeparam name="TRecord">The type of the record.</typeparam> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to register the <see cref="VectorStoreCollection{TKey, TRecord}"/> on.</param> | ||
/// <param name="serviceKey">The key with which to associate the collection.</param> | ||
/// <param name="collectionName">The name of the collection.</param> | ||
/// <param name="connectionString">Connection string for <see cref="SqliteConnection"/>.</param> | ||
/// <param name="options">Optional options to further configure the <see cref="VectorStoreCollection{TKey, TRecord}"/>.</param> | ||
/// <param name="serviceId">An optional service id to use as the service key.</param> | ||
/// <returns>Service collection.</returns> | ||
public static IServiceCollection AddSqliteVectorStoreRecordCollection<TKey, TRecord>( | ||
/// <param name="connectionStringProvider">The connection string provider.</param> | ||
/// <param name="optionsProvider">Options provider to further configure the collection.</param> | ||
/// <param name="lifetime">The service lifetime for the store. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param> | ||
/// <returns>The service collection.</returns> | ||
private static IServiceCollection AddCollection<TKey, TRecord>( | ||
this IServiceCollection services, | ||
object? serviceKey, | ||
string collectionName, | ||
string connectionString, | ||
SqliteCollectionOptions? options = default, | ||
string? serviceId = default) | ||
Func<IServiceProvider, string> connectionStringProvider, | ||
Func<IServiceProvider, SqliteCollectionOptions?>? optionsProvider = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
where TKey : notnull | ||
where TRecord : class | ||
{ | ||
services.AddKeyedSingleton<VectorStoreCollection<TKey, TRecord>>( | ||
serviceId, | ||
(sp, _) => ( | ||
new SqliteCollection<TKey, TRecord>( | ||
connectionString, | ||
collectionName, | ||
options ?? sp.GetService<SqliteCollectionOptions>() ?? new() | ||
{ | ||
EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>() | ||
}) | ||
as VectorStoreCollection<TKey, TRecord>)!); | ||
|
||
AddVectorizedSearch<TKey, TRecord>(services, serviceId); | ||
Verify.NotNull(services); | ||
Verify.NotNullOrWhiteSpace(collectionName); | ||
Verify.NotNull(connectionStringProvider); | ||
|
||
services.Add(new ServiceDescriptor(typeof(SqliteCollection<TKey, TRecord>), serviceKey, (sp, _) => | ||
{ | ||
var connectionString = connectionStringProvider(sp); | ||
var options = GetCollectionOptions(sp, optionsProvider); | ||
return new SqliteCollection<TKey, TRecord>(connectionString, collectionName, options); | ||
}, lifetime)); | ||
|
||
services.Add(new ServiceDescriptor(typeof(VectorStoreCollection<TKey, TRecord>), serviceKey, | ||
static (sp, key) => sp.GetRequiredKeyedService<SqliteCollection<TKey, TRecord>>(key), lifetime)); | ||
|
||
services.Add(new ServiceDescriptor(typeof(IVectorSearchable<TRecord>), serviceKey, | ||
static (sp, key) => sp.GetRequiredKeyedService<SqliteCollection<TKey, TRecord>>(key), lifetime)); | ||
|
||
// Once HybridSearch supports get implemented by SqliteCollection | ||
// we need to add IKeywordHybridSearchable abstraction here as well. | ||
|
||
return services; | ||
} | ||
|
||
/// <summary> | ||
/// Also register the <see cref="VectorStoreCollection{TKey, TRecord}"/> with the given <paramref name="serviceId"/> as a <see cref="IVectorSearchable{TRecord}"/>. | ||
/// Registers a <see cref="SqliteCollection{TKey, TRecord}"/> as <see cref="VectorStoreCollection{TKey, TRecord}"/>, with the specified connection string and service lifetime. | ||
/// </summary> | ||
/// <typeparam name="TKey">The type of the key.</typeparam> | ||
/// <typeparam name="TRecord">The type of the data model that the collection should contain.</typeparam> | ||
/// <param name="services">The service collection to register on.</param> | ||
/// <param name="serviceId">The service id that the registrations should use.</param> | ||
private static void AddVectorizedSearch<TKey, TRecord>(IServiceCollection services, string? serviceId) where TRecord : class | ||
/// <inheritdoc cref="AddCollection{TKey, TRecord}(IServiceCollection, object?, string, string, SqliteCollectionOptions?, ServiceLifetime)"/>/> | ||
public static IServiceCollection AddSqliteCollection<TKey, TRecord>( | ||
this IServiceCollection services, | ||
string collectionName, | ||
string connectionString, | ||
SqliteCollectionOptions? options = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
where TKey : notnull | ||
where TRecord : class | ||
=> AddCollection<TKey, TRecord>(services, serviceKey: null, collectionName, connectionString, options, lifetime); | ||
|
||
/// <inheritdoc cref="AddCollection{TKey, TRecord}(IServiceCollection, object?, string, string, SqliteCollectionOptions?, ServiceLifetime)"/>/> | ||
public static IServiceCollection AddKeyedSqliteCollection<TKey, TRecord>( | ||
this IServiceCollection services, | ||
object serviceKey, | ||
string collectionName, | ||
string connectionString, | ||
SqliteCollectionOptions? options = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
where TKey : notnull | ||
=> services.AddKeyedSingleton<IVectorSearchable<TRecord>>( | ||
serviceId, | ||
(sp, _) => sp.GetRequiredKeyedService<VectorStoreCollection<TKey, TRecord>>(serviceId)); | ||
where TRecord : class | ||
{ | ||
Verify.NotNull(serviceKey); | ||
|
||
return AddCollection<TKey, TRecord>(services, serviceKey, collectionName, connectionString, options, lifetime); | ||
} | ||
|
||
/// <summary> | ||
/// Registers a keyed <see cref="SqliteCollection{TKey, TRecord}"/> as <see cref="VectorStoreCollection{TKey, TRecord}"/>, with the specified connection string and service lifetime. | ||
/// </summary> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to register the <see cref="VectorStoreCollection{TKey, TRecord}"/> on.</param> | ||
/// <param name="serviceKey">The key with which to associate the collection.</param> | ||
/// <param name="collectionName">The name of the collection.</param> | ||
/// <param name="connectionString">The connection string.</param> | ||
/// <param name="options">Options to further configure the collection.</param> | ||
/// <param name="lifetime">The service lifetime for the store. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param> | ||
/// <returns>The service collection.</returns> | ||
private static IServiceCollection AddCollection<TKey, TRecord>( | ||
this IServiceCollection services, | ||
object? serviceKey, | ||
string collectionName, | ||
string connectionString, | ||
SqliteCollectionOptions? options = null, | ||
ServiceLifetime lifetime = ServiceLifetime.Singleton) | ||
where TKey : notnull | ||
where TRecord : class | ||
{ | ||
Verify.NotNullOrWhiteSpace(connectionString); | ||
|
||
return AddCollection<TKey, TRecord>(services, serviceKey, collectionName, _ => connectionString, _ => options, lifetime); | ||
} | ||
|
||
private static SqliteVectorStoreOptions? GetStoreOptions(IServiceProvider sp, Func<IServiceProvider, SqliteVectorStoreOptions?>? optionsProvider) | ||
{ | ||
var options = optionsProvider?.Invoke(sp); | ||
if (options?.EmbeddingGenerator is not null) | ||
{ | ||
return options; // The user has provided everything, there is nothing to change. | ||
} | ||
|
||
var embeddingGenerator = sp.GetService<IEmbeddingGenerator>(); | ||
return embeddingGenerator is null | ||
? options // There is nothing to change. | ||
: new(options, embeddingGenerator); // Create a brand new copy in order to avoid modifying the original options. | ||
} | ||
|
||
private static SqliteCollectionOptions? GetCollectionOptions(IServiceProvider sp, Func<IServiceProvider, SqliteCollectionOptions?>? optionsProvider) | ||
{ | ||
var options = optionsProvider?.Invoke(sp); | ||
if (options?.EmbeddingGenerator is not null) | ||
{ | ||
return options; // The user has provided everything, there is nothing to change. | ||
} | ||
|
||
var embeddingGenerator = sp.GetService<IEmbeddingGenerator>(); | ||
return embeddingGenerator is null | ||
? options // There is nothing to change. | ||
: new(options, embeddingGenerator); // Create a brand new copy in order to avoid modifying the original options. | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have the copy ctor just to make it more likely to not miss extending the copy logic when a new property gets introduced in the future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it's internal so it doesn't matter much, but I'd just have this a simple copy constructor - accepting only another PostgresCollectionOptions instance - and then set the embedding generator as usual at the call site:
This would make this usable for other needs and just be more standard (same with the other option constructors).