Skip to content

.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

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ namespace Microsoft.SemanticKernel.Connectors.PgVector;
/// </summary>
public sealed class PostgresCollectionOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="PostgresCollectionOptions"/> class.
/// </summary>
public PostgresCollectionOptions()
{
}

internal PostgresCollectionOptions(PostgresCollectionOptions? source, IEmbeddingGenerator embeddingGenerator)
Copy link
Member Author

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

Copy link
Member

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:

var newOptions = new PostgresCollectionOptions(source)
{
    EmbeddingGenerator = embeddingGenerator;
}

This would make this usable for other needs and just be more standard (same with the other option constructors).

{
this.Schema = source?.Schema ?? PostgresVectorStoreOptions.Default.Schema;
this.VectorStoreRecordDefinition = source?.VectorStoreRecordDefinition;
this.EmbeddingGenerator = embeddingGenerator;
}

/// <summary>
/// Gets or sets the database schema.
/// </summary>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ public sealed class PostgresVectorStoreOptions
{
internal static readonly PostgresVectorStoreOptions Default = new();

/// <summary>
/// Initializes a new instance of the <see cref="PostgresVectorStoreOptions"/> class.
/// </summary>
public PostgresVectorStoreOptions()
{
}

internal PostgresVectorStoreOptions(PostgresVectorStoreOptions? source, IEmbeddingGenerator embeddingGenerator)
{
this.Schema = source?.Schema ?? Default.Schema;
this.EmbeddingGenerator = embeddingGenerator;
}

/// <summary>
/// Gets or sets the database schema.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ public sealed class SqliteCollectionOptions
{
internal static readonly SqliteCollectionOptions Default = new();

/// <summary>
/// Initializes a new instance of the <see cref="SqliteCollectionOptions"/> class.
/// </summary>
public SqliteCollectionOptions()
{
}

internal SqliteCollectionOptions(SqliteCollectionOptions? source, IEmbeddingGenerator embeddingGenerator)
{
this.VectorStoreRecordDefinition = source?.VectorStoreRecordDefinition;
this.VectorVirtualTableName = source?.VectorVirtualTableName;
this.VectorSearchExtensionName = source?.VectorSearchExtensionName;
this.EmbeddingGenerator = embeddingGenerator;
}

/// <summary>
/// Gets or sets an optional record definition that defines the schema of the record type.
/// </summary>
Expand Down
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is literally a copy of the SqlServer file with a SqlServer -> Sqlite replace

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,
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);
Copy link
Member

Choose a reason for hiding this comment

The 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.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ public sealed class SqliteVectorStoreOptions
{
internal static readonly SqliteVectorStoreOptions Default = new();

/// <summary>
/// Initializes a new instance of the <see cref="SqliteVectorStoreOptions"/> class.
/// </summary>
public SqliteVectorStoreOptions()
{
}

internal SqliteVectorStoreOptions(SqliteVectorStoreOptions? source, IEmbeddingGenerator embeddingGenerator)
{
this.VectorVirtualTableName = source?.VectorVirtualTableName;
this.EmbeddingGenerator = embeddingGenerator;
}

/// <summary>
/// Custom virtual table name to store vectors.
/// </summary>
Expand Down
Loading
Loading