diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index c2062d3ed7fd..36b306d6562f 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -407,6 +407,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AotCompatibility", "samples
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.AotTests", "src\SemanticKernel.AotTests\SemanticKernel.AotTests.csproj", "{39EAB599-742F-417D-AF80-95F90376BB18}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.Postgres.UnitTests", "src\Connectors\Connectors.Postgres.UnitTests\Connectors.Postgres.UnitTests.csproj", "{232E1153-6366-4175-A982-D66B30AAD610}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.Utilities.UnitTests", "src\Experimental\Process.Utilities.UnitTests\Process.Utilities.UnitTests.csproj", "{DAC54048-A39A-4739-8307-EA5A291F2EA0}"
EndProject
Global
@@ -1046,6 +1048,12 @@ Global
{6F591D05-5F7F-4211-9042-42D8BCE60415}.Publish|Any CPU.Build.0 = Debug|Any CPU
{6F591D05-5F7F-4211-9042-42D8BCE60415}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F591D05-5F7F-4211-9042-42D8BCE60415}.Release|Any CPU.Build.0 = Release|Any CPU
+ {232E1153-6366-4175-A982-D66B30AAD610}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {232E1153-6366-4175-A982-D66B30AAD610}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {232E1153-6366-4175-A982-D66B30AAD610}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
+ {232E1153-6366-4175-A982-D66B30AAD610}.Publish|Any CPU.Build.0 = Debug|Any CPU
+ {232E1153-6366-4175-A982-D66B30AAD610}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {232E1153-6366-4175-A982-D66B30AAD610}.Release|Any CPU.Build.0 = Release|Any CPU
{E82B640C-1704-430D-8D71-FD8ED3695468}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E82B640C-1704-430D-8D71-FD8ED3695468}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E82B640C-1704-430D-8D71-FD8ED3695468}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
@@ -1216,6 +1224,7 @@ Global
{E82B640C-1704-430D-8D71-FD8ED3695468} = {5A7028A7-4DDF-4E4F-84A9-37CE8F8D7E89}
{6ECFDF04-2237-4A85-B114-DAA34923E9E6} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{39EAB599-742F-417D-AF80-95F90376BB18} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
+ {232E1153-6366-4175-A982-D66B30AAD610} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{DAC54048-A39A-4739-8307-EA5A291F2EA0} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj
index e35c2bef0dca..90ce6af1d1e0 100644
--- a/dotnet/samples/Concepts/Concepts.csproj
+++ b/dotnet/samples/Concepts/Concepts.csproj
@@ -102,6 +102,9 @@
+
+ Always
+ PreserveNewest
diff --git a/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreInfra.cs b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreInfra.cs
index ea498f20c5ab..2681231c80d7 100644
--- a/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreInfra.cs
+++ b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStoreInfra.cs
@@ -10,6 +10,51 @@ namespace Memory.VectorStoreFixtures;
///
internal static class VectorStoreInfra
{
+ ///
+ /// Setup the postgres pgvector container by pulling the image and running it.
+ ///
+ /// The docker client to create the container with.
+ /// The id of the container.
+ public static async Task SetupPostgresContainerAsync(DockerClient client)
+ {
+ await client.Images.CreateImageAsync(
+ new ImagesCreateParameters
+ {
+ FromImage = "pgvector/pgvector",
+ Tag = "pg16",
+ },
+ null,
+ new Progress());
+
+ var container = await client.Containers.CreateContainerAsync(new CreateContainerParameters()
+ {
+ Image = "pgvector/pgvector:pg16",
+ HostConfig = new HostConfig()
+ {
+ PortBindings = new Dictionary>
+ {
+ {"5432", new List {new() {HostPort = "5432" } }},
+ },
+ PublishAllPorts = true
+ },
+ ExposedPorts = new Dictionary
+ {
+ { "5432", default },
+ },
+ Env = new List
+ {
+ "POSTGRES_USER=postgres",
+ "POSTGRES_PASSWORD=example",
+ },
+ });
+
+ await client.Containers.StartContainerAsync(
+ container.ID,
+ new ContainerStartParameters());
+
+ return container.ID;
+ }
+
///
/// Setup the qdrant container by pulling the image and running it.
///
diff --git a/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStorePostgresContainerFixture.cs b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStorePostgresContainerFixture.cs
new file mode 100644
index 000000000000..a8d4738fe133
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStoreFixtures/VectorStorePostgresContainerFixture.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Docker.DotNet;
+using Npgsql;
+
+namespace Memory.VectorStoreFixtures;
+
+///
+/// Fixture to use for creating a Postgres container before tests and delete it after tests.
+///
+public class VectorStorePostgresContainerFixture : IAsyncLifetime
+{
+ private DockerClient? _dockerClient;
+ private string? _postgresContainerId;
+
+ public async Task InitializeAsync()
+ {
+ }
+
+ public async Task ManualInitializeAsync()
+ {
+ if (this._postgresContainerId == null)
+ {
+ // Connect to docker and start the docker container.
+ using var dockerClientConfiguration = new DockerClientConfiguration();
+ this._dockerClient = dockerClientConfiguration.CreateClient();
+ this._postgresContainerId = await VectorStoreInfra.SetupPostgresContainerAsync(this._dockerClient);
+
+ // Delay until the Postgres server is ready.
+ var connectionString = "Host=localhost;Port=5432;Username=postgres;Password=example;Database=postgres;";
+ var succeeded = false;
+ var attemptCount = 0;
+ while (!succeeded && attemptCount++ < 10)
+ {
+ try
+ {
+ NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);
+ dataSourceBuilder.UseVector();
+ using var dataSource = dataSourceBuilder.Build();
+ NpgsqlConnection connection = await dataSource.OpenConnectionAsync().ConfigureAwait(false);
+
+ await using (connection)
+ {
+ // Create extension vector if it doesn't exist
+ await using (NpgsqlCommand command = new("CREATE EXTENSION IF NOT EXISTS vector", connection))
+ {
+ await command.ExecuteNonQueryAsync();
+ }
+ }
+ }
+ catch (Exception)
+ {
+ await Task.Delay(1000);
+ }
+ }
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (this._dockerClient != null && this._postgresContainerId != null)
+ {
+ // Delete docker container.
+ await VectorStoreInfra.DeleteContainerAsync(this._dockerClient, this._postgresContainerId);
+ }
+ }
+}
diff --git a/dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs b/dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs
new file mode 100644
index 000000000000..a15cc696e094
--- /dev/null
+++ b/dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.Identity;
+using Memory.VectorStoreFixtures;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
+using Microsoft.SemanticKernel.Connectors.Postgres;
+using Npgsql;
+
+namespace Memory;
+
+///
+/// An example showing how to use common code, that can work with any vector database, with a Postgres database.
+/// The common code is in the class.
+/// The common code ingests data into the vector store and then searches over that data.
+/// This example is part of a set of examples each showing a different vector database.
+///
+/// For other databases, see the following classes:
+///
+///
+///
+///
+/// To run this sample, you need a local instance of Docker running, since the associated fixture will try and start a Postgres container in the local docker instance.
+///
+public class VectorStore_VectorSearch_MultiStore_Postgres(ITestOutputHelper output, VectorStorePostgresContainerFixture PostgresFixture) : BaseTest(output), IClassFixture
+{
+ ///
+ /// The connection string to the Postgres database hosted in the docker container.
+ ///
+ private const string ConnectionString = "Host=localhost;Port=5432;Username=postgres;Password=example;Database=postgres;";
+
+ [Fact]
+ public async Task ExampleWithDIAsync()
+ {
+ // Use the kernel for DI purposes.
+ var kernelBuilder = Kernel
+ .CreateBuilder();
+
+ // Register an embedding generation service with the DI container.
+ kernelBuilder.AddAzureOpenAITextEmbeddingGeneration(
+ deploymentName: TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
+ endpoint: TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
+ credential: new AzureCliCredential());
+
+ // Initialize the Postgres docker container via the fixtures and register the Postgres VectorStore.
+ await PostgresFixture.ManualInitializeAsync();
+ kernelBuilder.Services.AddPostgresVectorStore(ConnectionString);
+
+ // Register the test output helper common processor with the DI container.
+ kernelBuilder.Services.AddSingleton(this.Output);
+ kernelBuilder.Services.AddTransient();
+
+ // Build the kernel.
+ var kernel = kernelBuilder.Build();
+
+ // Build a common processor object using the DI container.
+ var processor = kernel.GetRequiredService();
+
+ // Run the process and pass a key generator function to it, to generate unique record keys.
+ // The key generator function is required, since different vector stores may require different key types.
+ // E.g. Postgres supports Guid and ulong keys, but others may support strings only.
+ await processor.IngestDataAndSearchAsync("skglossaryWithDI", () => Guid.NewGuid());
+ }
+
+ [Fact]
+ public async Task ExampleWithoutDIAsync()
+ {
+ // Create an embedding generation service.
+ var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService(
+ TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
+ TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
+ new AzureCliCredential());
+
+ // Initialize the Postgres docker container via the fixtures and construct the Postgres VectorStore.
+ await PostgresFixture.ManualInitializeAsync();
+ var dataSourceBuilder = new NpgsqlDataSourceBuilder(ConnectionString);
+ dataSourceBuilder.UseVector();
+ await using var dataSource = dataSourceBuilder.Build();
+ var vectorStore = new PostgresVectorStore(dataSource);
+
+ // Create the common processor that works for any vector store.
+ var processor = new VectorStore_VectorSearch_MultiStore_Common(vectorStore, textEmbeddingGenerationService, this.Output);
+
+ // Run the process and pass a key generator function to it, to generate unique record keys.
+ // The key generator function is required, since different vector stores may require different key types.
+ // E.g. Postgres supports Guid and ulong keys, but others may support strings only.
+ await processor.IngestDataAndSearchAsync("skglossaryWithoutDI", () => Guid.NewGuid());
+ }
+}
diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md
index 15584b88685c..710ef6dfa5bb 100644
--- a/dotnet/samples/Concepts/README.md
+++ b/dotnet/samples/Concepts/README.md
@@ -204,3 +204,88 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom
### TextToImage - Using [`TextToImage`](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs) services to generate images
- [OpenAI_TextToImage](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/TextToImage/OpenAI_TextToImageDalle3.cs)
+
+## Configuration
+
+### Option 1: Use Secret Manager
+
+Concept samples will require secrets and credentials, to access OpenAI, Azure OpenAI,
+Bing and other resources.
+
+We suggest using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)
+to avoid the risk of leaking secrets into the repository, branches and pull requests.
+You can also use environment variables if you prefer.
+
+To set your secrets with Secret Manager:
+
+```
+cd dotnet/src/samples/Concepts
+
+dotnet user-secrets init
+
+dotnet user-secrets set "OpenAI:ServiceId" "gpt-3.5-turbo-instruct"
+dotnet user-secrets set "OpenAI:ModelId" "gpt-3.5-turbo-instruct"
+dotnet user-secrets set "OpenAI:ChatModelId" "gpt-4"
+dotnet user-secrets set "OpenAI:ApiKey" "..."
+
+...
+```
+
+### Option 2: Use Configuration File
+1. Create a `appsettings.Development.json` file next to the `Concepts.csproj` file. This file will be ignored by git,
+ the content will not end up in pull requests, so it's safe for personal settings. Keep the file safe.
+2. Edit `appsettings.Development.json` and set the appropriate configuration for the samples you are running.
+
+For example:
+
+```json
+{
+ "OpenAI": {
+ "ServiceId": "gpt-3.5-turbo-instruct",
+ "ModelId": "gpt-3.5-turbo-instruct",
+ "ChatModelId": "gpt-4",
+ "ApiKey": "sk-...."
+ },
+ "AzureOpenAI": {
+ "ServiceId": "azure-gpt-35-turbo-instruct",
+ "DeploymentName": "gpt-35-turbo-instruct",
+ "ChatDeploymentName": "gpt-4",
+ "Endpoint": "https://contoso.openai.azure.com/",
+ "ApiKey": "...."
+ },
+ // etc.
+}
+```
+
+### Option 3: Use Environment Variables
+You may also set the settings in your environment variables. The environment variables will override the settings in the `appsettings.Development.json` file.
+
+When setting environment variables, use a double underscore (i.e. "\_\_") to delineate between parent and child properties. For example:
+
+- bash:
+
+ ```bash
+ export OpenAI__ApiKey="sk-...."
+ export AzureOpenAI__ApiKey="...."
+ export AzureOpenAI__DeploymentName="gpt-35-turbo-instruct"
+ export AzureOpenAI__ChatDeploymentName="gpt-4"
+ export AzureOpenAIEmbeddings__DeploymentName="azure-text-embedding-ada-002"
+ export AzureOpenAI__Endpoint="https://contoso.openai.azure.com/"
+ export HuggingFace__ApiKey="...."
+ export Bing__ApiKey="...."
+ export Postgres__ConnectionString="...."
+ ```
+
+- PowerShell:
+
+ ```ps
+ $env:OpenAI__ApiKey = "sk-...."
+ $env:AzureOpenAI__ApiKey = "...."
+ $env:AzureOpenAI__DeploymentName = "gpt-35-turbo-instruct"
+ $env:AzureOpenAI__ChatDeploymentName = "gpt-4"
+ $env:AzureOpenAIEmbeddings__DeploymentName = "azure-text-embedding-ada-002"
+ $env:AzureOpenAI__Endpoint = "https://contoso.openai.azure.com/"
+ $env:HuggingFace__ApiKey = "...."
+ $env:Bing__ApiKey = "...."
+ $env:Postgres__ConnectionString = "...."
+ ```
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/Connectors.Memory.Postgres.csproj b/dotnet/src/Connectors/Connectors.Memory.Postgres/Connectors.Memory.Postgres.csproj
index ad132bde113d..7673d1e92a93 100644
--- a/dotnet/src/Connectors/Connectors.Memory.Postgres/Connectors.Memory.Postgres.csproj
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/Connectors.Memory.Postgres.csproj
@@ -27,4 +27,9 @@
+
+
+
+
+
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresDbClient.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresDbClient.cs
index 70747990e2fd..2056260eb292 100644
--- a/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresDbClient.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresDbClient.cs
@@ -11,6 +11,11 @@ namespace Microsoft.SemanticKernel.Connectors.Postgres;
///
/// Interface for client managing postgres database operations.
///
+///
+/// This interface is used with the PostgresMemoryStore, which is being deprecated.
+/// Use the interface with the PostgresVectorStore
+/// and related classes instead.
+///
public interface IPostgresDbClient
{
///
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreCollectionSqlBuilder.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreCollectionSqlBuilder.cs
new file mode 100644
index 000000000000..d130d2f13b44
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreCollectionSqlBuilder.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.VectorData;
+using Pgvector;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+///
+/// Interface for constructing SQL commands for Postgres vector store collections.
+///
+internal interface IPostgresVectorStoreCollectionSqlBuilder
+{
+ ///
+ /// Builds a SQL command to check if a table exists in the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The built SQL command.
+ ///
+ /// The command must return a single row with a single column named "table_name" if the table exists.
+ ///
+ PostgresSqlCommandInfo BuildDoesTableExistCommand(string schema, string tableName);
+
+ ///
+ /// Builds a SQL command to fetch all tables in the Postgres vector store.
+ ///
+ /// The schema of the tables.
+ PostgresSqlCommandInfo BuildGetTablesCommand(string schema);
+
+ ///
+ /// Builds a SQL command to create a table in the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The properties of the table.
+ /// Specifies whether to include IF NOT EXISTS in the command.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildCreateTableCommand(string schema, string tableName, IReadOnlyList properties, bool ifNotExists = true);
+
+ ///
+ /// Builds a SQL command to create a vector index in the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The name of the vector column.
+ /// The kind of index to create.
+ /// The distance function to use for the index.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildCreateVectorIndexCommand(string schema, string tableName, string vectorColumnName, string indexKind, string distanceFunction);
+
+ ///
+ /// Builds a SQL command to drop a table in the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildDropTableCommand(string schema, string tableName);
+
+ ///
+ /// Builds a SQL command to upsert a record in the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The key column of the table.
+ /// The row to upsert.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildUpsertCommand(string schema, string tableName, string keyColumn, Dictionary row);
+
+ ///
+ /// Builds a SQL command to upsert a batch of records in the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The key column of the table.
+ /// The rows to upsert.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildUpsertBatchCommand(string schema, string tableName, string keyColumn, List> rows);
+
+ ///
+ /// Builds a SQL command to get a record from the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The properties of the table.
+ /// The key of the record to get.
+ /// Specifies whether to include vectors in the record.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildGetCommand(string schema, string tableName, IReadOnlyList properties, TKey key, bool includeVectors = false) where TKey : notnull;
+
+ ///
+ /// Builds a SQL command to get a batch of records from the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The properties of the table.
+ /// The keys of the records to get.
+ /// Specifies whether to include vectors in the records.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildGetBatchCommand(string schema, string tableName, IReadOnlyList properties, List keys, bool includeVectors = false) where TKey : notnull;
+
+ ///
+ /// Builds a SQL command to delete a record from the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The key column of the table.
+ /// The key of the record to delete.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildDeleteCommand(string schema, string tableName, string keyColumn, TKey key);
+
+ ///
+ /// Builds a SQL command to delete a batch of records from the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The key column of the table.
+ /// The keys of the records to delete.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildDeleteBatchCommand(string schema, string tableName, string keyColumn, List keys);
+
+ ///
+ /// Builds a SQL command to get the nearest match from the Postgres vector store.
+ ///
+ /// The schema of the table.
+ /// The name of the table.
+ /// The properties of the table.
+ /// The property which the vectors to compare are stored in.
+ /// The vector to match.
+ /// The filter conditions for the query.
+ /// The number of records to skip.
+ /// Specifies whether to include vectors in the result.
+ /// The maximum number of records to return.
+ /// The built SQL command info.
+ PostgresSqlCommandInfo BuildGetNearestMatchCommand(string schema, string tableName, IReadOnlyList properties, VectorStoreRecordVectorProperty vectorProperty, Vector vectorValue, VectorSearchFilter? filter, int? skip, bool includeVectors, int limit);
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreDbClient.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreDbClient.cs
new file mode 100644
index 000000000000..59aa9829c568
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreDbClient.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.VectorData;
+using Npgsql;
+using Pgvector;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+///
+/// Internal interface for client managing postgres database operations.
+///
+internal interface IPostgresVectorStoreDbClient
+{
+ ///
+ /// The used to connect to the database.
+ ///
+ public NpgsqlDataSource DataSource { get; }
+
+ ///
+ /// Check if a table exists.
+ ///
+ /// The name assigned to a table of entries.
+ /// The to monitor for cancellation requests. The default is .
+ ///
+ Task DoesTableExistsAsync(string tableName, CancellationToken cancellationToken = default);
+
+ ///
+ /// Get all tables.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// A group of tables.
+ IAsyncEnumerable GetTablesAsync(CancellationToken cancellationToken = default);
+ ///
+ /// Create a table. Also creates an index on vector columns if the table has vector properties defined.
+ ///
+ /// The name assigned to a table of entries.
+ /// The properties of the record definition that define the table.
+ /// Specifies whether to include IF NOT EXISTS in the command.
+ /// The to monitor for cancellation requests. The default is .
+ ///
+ Task CreateTableAsync(string tableName, IReadOnlyList properties, bool ifNotExists = true, CancellationToken cancellationToken = default);
+
+ ///
+ /// Drop a table.
+ ///
+ /// The name assigned to a table of entries.
+ /// The to monitor for cancellation requests. The default is .
+ Task DeleteTableAsync(string tableName, CancellationToken cancellationToken = default);
+
+ ///
+ /// Upsert entry into a table.
+ ///
+ /// The name assigned to a table of entries.
+ /// The row to upsert into the table.
+ /// The key column of the table.
+ /// The to monitor for cancellation requests. The default is .
+ ///
+ Task UpsertAsync(string tableName, Dictionary row, string keyColumn, CancellationToken cancellationToken = default);
+
+ ///
+ /// Upsert multiple entries into a table.
+ ///
+ /// The name assigned to a table of entries.
+ /// The rows to upsert into the table.
+ /// The key column of the table.
+ /// The to monitor for cancellation requests. The default is .
+ ///
+ Task UpsertBatchAsync(string tableName, IEnumerable> rows, string keyColumn, CancellationToken cancellationToken = default);
+
+ ///
+ /// Get a entry by its key.
+ ///
+ /// The name assigned to a table of entries.
+ /// The key of the entry to get.
+ /// The properties to include in the entry.
+ /// If true, the vectors will be included in the entry.
+ /// The to monitor for cancellation requests. The default is .
+ /// The row if the key is found, otherwise null.
+ Task?> GetAsync(string tableName, TKey key, IReadOnlyList properties, bool includeVectors = false, CancellationToken cancellationToken = default)
+ where TKey : notnull;
+
+ ///
+ /// Get multiple entries by their keys.
+ ///
+ /// The name assigned to a table of entries.
+ /// The keys of the entries to get.
+ /// The properties of the table.
+ /// If true, the vectors will be included in the entries.
+ /// The to monitor for cancellation requests. The default is .
+ /// The rows that match the given keys.
+ IAsyncEnumerable> GetBatchAsync(string tableName, IEnumerable keys, IReadOnlyList properties, bool includeVectors = false, CancellationToken cancellationToken = default)
+ where TKey : notnull;
+
+ ///
+ /// Delete a entry by its key.
+ ///
+ /// The name assigned to a table of entries.
+ /// The name of the key column.
+ /// The key of the entry to delete.
+ /// The to monitor for cancellation requests. The default is .
+ ///
+ Task DeleteAsync(string tableName, string keyColumn, TKey key, CancellationToken cancellationToken = default);
+
+ ///
+ /// Delete multiple entries by their keys.
+ ///
+ /// The name assigned to a table of entries.
+ /// The name of the key column.
+ /// The keys of the entries to delete.
+ /// The to monitor for cancellation requests. The default is .
+ ///
+ Task DeleteBatchAsync(string tableName, string keyColumn, IEnumerable keys, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the nearest matches to the .
+ ///
+ /// The name assigned to a table of entries.
+ /// The properties to retrieve.
+ /// The property which the vectors to compare are stored in.
+ /// The to compare the table's vector with.
+ /// The maximum number of similarity results to return.
+ /// Optional conditions to filter the results.
+ /// The number of entries to skip.
+ /// If true, the vectors will be returned in the entries.
+ /// The to monitor for cancellation requests. The default is .
+ /// An asynchronous stream of objects that the nearest matches to the .
+ IAsyncEnumerable<(Dictionary Row, double Distance)> GetNearestMatchesAsync(string tableName, IReadOnlyList properties, VectorStoreRecordVectorProperty vectorProperty, Vector vectorValue, int limit,
+ VectorSearchFilter? filter = default, int? skip = default, bool includeVectors = false, CancellationToken cancellationToken = default);
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreRecordCollectionFactory.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreRecordCollectionFactory.cs
new file mode 100644
index 000000000000..5bf0d9cad789
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/IPostgresVectorStoreRecordCollectionFactory.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.VectorData;
+using Npgsql;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+///
+/// Interface for constructing Postgres instances when using to retrieve these.
+///
+public interface IPostgresVectorStoreRecordCollectionFactory
+{
+ ///
+ /// Constructs a new instance of the .
+ ///
+ /// The data type of the record key.
+ /// The data model to use for adding, updating and retrieving data from storage.
+ /// The Postgres data source.
+ /// The name of the collection to connect to.
+ /// An optional record definition that defines the schema of the record type. If not present, attributes on will be used.
+ /// The new instance of .
+ IVectorStoreRecordCollection CreateVectorStoreRecordCollection(NpgsqlDataSource dataSource, string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition)
+ where TKey : notnull;
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresConstants.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresConstants.cs
new file mode 100644
index 000000000000..f8784890e83a
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresConstants.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.VectorData;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+internal static class PostgresConstants
+{
+ /// The name of this database for telemetry purposes.
+ public const string DatabaseName = "Postgres";
+
+ /// A of types that a key on the provided model may have.
+ public static readonly HashSet SupportedKeyTypes =
+ [
+ typeof(short),
+ typeof(int),
+ typeof(long),
+ typeof(string),
+ typeof(Guid),
+ ];
+
+ /// A of types that data properties on the provided model may have.
+ public static readonly HashSet SupportedDataTypes =
+ [
+ typeof(bool),
+ typeof(bool?),
+ typeof(short),
+ typeof(short?),
+ typeof(int),
+ typeof(int?),
+ typeof(long),
+ typeof(long?),
+ typeof(float),
+ typeof(float?),
+ typeof(double),
+ typeof(double?),
+ typeof(decimal),
+ typeof(decimal?),
+ typeof(string),
+ typeof(DateTime),
+ typeof(DateTime?),
+ typeof(DateTimeOffset),
+ typeof(DateTimeOffset?),
+ typeof(Guid),
+ typeof(Guid?),
+ typeof(byte[]),
+ ];
+
+ /// A of types that enumerable data properties on the provided model may use as their element types.
+ public static readonly HashSet SupportedEnumerableDataElementTypes =
+ [
+ typeof(bool),
+ typeof(short),
+ typeof(int),
+ typeof(long),
+ typeof(float),
+ typeof(double),
+ typeof(decimal),
+ typeof(string),
+ typeof(DateTime),
+ typeof(DateTimeOffset),
+ typeof(Guid),
+ ];
+
+ /// A of types that vector properties on the provided model may have.
+ public static readonly HashSet SupportedVectorTypes =
+ [
+ typeof(ReadOnlyMemory),
+ typeof(ReadOnlyMemory?)
+ ];
+
+ /// The default schema name.
+ public const string DefaultSchema = "public";
+
+ /// The name of the column that returns distance value in the database.
+ /// It is used in the similarity search query. Must not conflict with model property.
+ public const string DistanceColumnName = "sk_pg_distance";
+
+ /// The default index kind.
+ /// Defaults to "Flat", which means no indexing.
+ public const string DefaultIndexKind = IndexKind.Flat;
+
+ /// The default distance function.
+ public const string DefaultDistanceFunction = DistanceFunction.CosineDistance;
+
+ public static readonly Dictionary IndexMaxDimensions = new()
+ {
+ { IndexKind.Hnsw, 2000 },
+ };
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresDbClient.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresDbClient.cs
index 1dc1ffef3c1d..88741c236531 100644
--- a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresDbClient.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresDbClient.cs
@@ -15,6 +15,11 @@ namespace Microsoft.SemanticKernel.Connectors.Postgres;
///
/// An implementation of a client for Postgres. This class is used to managing postgres database operations.
///
+///
+/// This class is used with the PostgresMemoryStore, which is being deprecated.
+/// Use the class with the PostgresVectorStore
+/// and related classes instead.
+///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "We need to build the full table name using schema and collection, it does not support parameterized passing.")]
public class PostgresDbClient : IPostgresDbClient
{
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresGenericDataModelMapper.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresGenericDataModelMapper.cs
new file mode 100644
index 000000000000..9679bb22f44e
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresGenericDataModelMapper.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.VectorData;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+internal sealed class PostgresGenericDataModelMapper : IVectorStoreRecordMapper, Dictionary>
+ where TKey : notnull
+{
+ /// with helpers for reading vector store model properties and their attributes.
+ private readonly VectorStoreRecordPropertyReader _propertyReader;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// ///
+ /// with helpers for reading vector store model properties and their attributes.
+ public PostgresGenericDataModelMapper(VectorStoreRecordPropertyReader propertyReader)
+ {
+ Verify.NotNull(propertyReader);
+
+ this._propertyReader = propertyReader;
+
+ // Validate property types.
+ this._propertyReader.VerifyDataProperties(PostgresConstants.SupportedDataTypes, PostgresConstants.SupportedEnumerableDataElementTypes);
+ this._propertyReader.VerifyVectorProperties(PostgresConstants.SupportedVectorTypes);
+ }
+ public Dictionary MapFromDataToStorageModel(VectorStoreGenericDataModel dataModel)
+ {
+ var properties = new Dictionary
+ {
+ // Add key property
+ { this._propertyReader.KeyPropertyStoragePropertyName, dataModel.Key }
+ };
+
+ // Add data properties
+ if (dataModel.Data is not null)
+ {
+ foreach (var property in this._propertyReader.DataProperties)
+ {
+ if (dataModel.Data.TryGetValue(property.DataModelPropertyName, out var dataValue))
+ {
+ properties.Add(this._propertyReader.GetStoragePropertyName(property.DataModelPropertyName), dataValue);
+ }
+ }
+ }
+
+ // Add vector properties
+ if (dataModel.Vectors is not null)
+ {
+ foreach (var property in this._propertyReader.VectorProperties)
+ {
+ if (dataModel.Vectors.TryGetValue(property.DataModelPropertyName, out var vectorValue))
+ {
+ var result = PostgresVectorStoreRecordPropertyMapping.MapVectorForStorageModel(vectorValue);
+ properties.Add(this._propertyReader.GetStoragePropertyName(property.DataModelPropertyName), result);
+ }
+ }
+ }
+
+ return properties;
+ }
+
+ public VectorStoreGenericDataModel MapFromStorageToDataModel(Dictionary storageModel, StorageToDataModelMapperOptions options)
+ {
+ TKey key;
+ var dataProperties = new Dictionary();
+ var vectorProperties = new Dictionary();
+
+ // Process key property.
+ if (storageModel.TryGetValue(this._propertyReader.KeyPropertyStoragePropertyName, out var keyObject) && keyObject is not null)
+ {
+ key = (TKey)keyObject;
+ }
+ else
+ {
+ throw new VectorStoreRecordMappingException("No key property was found in the record retrieved from storage.");
+ }
+
+ // Process data properties.
+ foreach (var property in this._propertyReader.DataProperties)
+ {
+ if (storageModel.TryGetValue(this._propertyReader.GetStoragePropertyName(property.DataModelPropertyName), out var dataValue))
+ {
+ dataProperties.Add(property.DataModelPropertyName, dataValue);
+ }
+ }
+
+ // Process vector properties
+ if (options.IncludeVectors)
+ {
+ foreach (var property in this._propertyReader.VectorProperties)
+ {
+ if (storageModel.TryGetValue(this._propertyReader.GetStoragePropertyName(property.DataModelPropertyName), out var vectorValue))
+ {
+ vectorProperties.Add(property.DataModelPropertyName, PostgresVectorStoreRecordPropertyMapping.MapVectorForDataModel(vectorValue));
+ }
+ }
+ }
+
+ return new VectorStoreGenericDataModel(key) { Data = dataProperties, Vectors = vectorProperties };
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresServiceCollectionExtensions.cs
new file mode 100644
index 000000000000..983b8e7db443
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresServiceCollectionExtensions.cs
@@ -0,0 +1,172 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.VectorData;
+using Microsoft.SemanticKernel.Connectors.Postgres;
+using Npgsql;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Extension methods to register Postgres instances on an .
+///
+public static class PostgresServiceCollectionExtensions
+{
+ ///
+ /// Register a Postgres with the specified service ID and where the NpgsqlDataSource is retrieved from the dependency injection container.
+ ///
+ /// The to register the on.
+ /// Optional options to further configure the .
+ /// An optional service id to use as the service key.
+ /// The service collection.
+ public static IServiceCollection AddPostgresVectorStore(this IServiceCollection services, PostgresVectorStoreOptions? options = default, string? serviceId = default)
+ {
+ // Since we are not constructing the data source, add the IVectorStore as transient, since we
+ // cannot make assumptions about how data source is being managed.
+ services.AddKeyedTransient(
+ serviceId,
+ (sp, obj) =>
+ {
+ var dataSource = sp.GetRequiredService();
+ var selectedOptions = options ?? sp.GetService();
+
+ return new PostgresVectorStore(
+ dataSource,
+ selectedOptions);
+ });
+
+ return services;
+ }
+
+ ///
+ /// Register a Postgres with the specified service ID and where an NpgsqlDataSource is constructed using the provided parameters.
+ ///
+ /// The to register the on.
+ /// Postgres database connection string.
+ /// Optional options to further configure the .
+ /// An optional service id to use as the service key.
+ /// The service collection.
+ public static IServiceCollection AddPostgresVectorStore(this IServiceCollection services, string connectionString, PostgresVectorStoreOptions? options = default, string? serviceId = default)
+ {
+ string? npgsqlServiceId = serviceId == null ? default : $"{serviceId}_NpgsqlDataSource";
+ // Register NpgsqlDataSource to ensure proper disposal.
+ services.AddKeyedSingleton(
+ npgsqlServiceId,
+ (sp, obj) =>
+ {
+ NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);
+ dataSourceBuilder.UseVector();
+ return dataSourceBuilder.Build();
+ });
+
+ services.AddKeyedSingleton(
+ serviceId,
+ (sp, obj) =>
+ {
+ var dataSource = sp.GetRequiredKeyedService(npgsqlServiceId);
+ var selectedOptions = options ?? sp.GetService();
+
+ return new PostgresVectorStore(
+ dataSource,
+ selectedOptions);
+ });
+
+ return services;
+ }
+
+ ///
+ /// Register a Postgres and with the specified service ID
+ /// and where the NpgsqlDataSource is retrieved from the dependency injection container.
+ ///
+ /// The type of the key.
+ /// The type of the record.
+ /// The to register the on.
+ /// The name of the collection.
+ /// Optional options to further configure the .
+ /// An optional service id to use as the service key.
+ /// Service collection.
+ public static IServiceCollection AddPostgresVectorStoreRecordCollection(
+ this IServiceCollection services,
+ string collectionName,
+ PostgresVectorStoreRecordCollectionOptions? options = default,
+ string? serviceId = default)
+ where TKey : notnull
+ {
+ services.AddKeyedTransient>(
+ serviceId,
+ (sp, obj) =>
+ {
+ var dataSource = sp.GetRequiredService();
+ var selectedOptions = options ?? sp.GetService>();
+
+ return (new PostgresVectorStoreRecordCollection(dataSource, collectionName, selectedOptions) as IVectorStoreRecordCollection)!;
+ });
+
+ AddVectorizedSearch(services, serviceId);
+
+ return services;
+ }
+
+ ///
+ /// Register a Postgres and with the specified service ID
+ /// and where the NpgsqlDataSource is constructed using the provided parameters.
+ ///
+ /// The type of the key.
+ /// The type of the record.
+ /// The to register the on.
+ /// The name of the collection.
+ /// Postgres database connection string.
+ /// Optional options to further configure the .
+ /// An optional service id to use as the service key.
+ /// Service collection.
+ public static IServiceCollection AddPostgresVectorStoreRecordCollection(
+ this IServiceCollection services,
+ string collectionName,
+ string connectionString,
+ PostgresVectorStoreRecordCollectionOptions? options = default,
+ string? serviceId = default)
+ where TKey : notnull
+ {
+ string? npgsqlServiceId = serviceId == null ? default : $"{serviceId}_NpgsqlDataSource";
+ // Register NpgsqlDataSource to ensure proper disposal.
+ services.AddKeyedSingleton(
+ npgsqlServiceId,
+ (sp, obj) =>
+ {
+ NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);
+ dataSourceBuilder.UseVector();
+ return dataSourceBuilder.Build();
+ });
+
+ services.AddKeyedSingleton>(
+ serviceId,
+ (sp, obj) =>
+ {
+ var dataSource = sp.GetRequiredKeyedService(npgsqlServiceId);
+
+ return (new PostgresVectorStoreRecordCollection(dataSource, collectionName, options) as IVectorStoreRecordCollection)!;
+ });
+
+ AddVectorizedSearch(services, serviceId);
+
+ return services;
+ }
+
+ ///
+ /// Also register the with the given as a .
+ ///
+ /// The type of the key.
+ /// The type of the data model that the collection should contain.
+ /// The service collection to register on.
+ /// The service id that the registrations should use.
+ private static void AddVectorizedSearch(IServiceCollection services, string? serviceId)
+ where TKey : notnull
+ {
+ services.AddKeyedTransient>(
+ serviceId,
+ (sp, obj) =>
+ {
+ return sp.GetRequiredKeyedService>(serviceId);
+ });
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresSqlCommandInfo.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresSqlCommandInfo.cs
new file mode 100644
index 000000000000..fb520188b84b
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresSqlCommandInfo.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Npgsql;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+///
+/// Represents a SQL command for Postgres.
+///
+internal class PostgresSqlCommandInfo
+{
+ ///
+ /// Gets or sets the SQL command text.
+ ///
+ public string CommandText { get; set; }
+ ///
+ /// Gets or sets the parameters for the SQL command.
+ ///
+ public List? Parameters { get; set; } = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The SQL command text.
+ /// The parameters for the SQL command.
+ public PostgresSqlCommandInfo(string commandText, List? parameters = null)
+ {
+ this.CommandText = commandText;
+ this.Parameters = parameters;
+ }
+
+ ///
+ /// Converts this instance to an .
+ ///
+ [SuppressMessage("Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "User input is passed using command parameters.")]
+ public NpgsqlCommand ToNpgsqlCommand(NpgsqlConnection connection, NpgsqlTransaction? transaction = null)
+ {
+ NpgsqlCommand cmd = connection.CreateCommand();
+ if (transaction != null)
+ {
+ cmd.Transaction = transaction;
+ }
+ cmd.CommandText = this.CommandText;
+ if (this.Parameters != null)
+ {
+ foreach (var parameter in this.Parameters)
+ {
+ cmd.Parameters.Add(parameter);
+ }
+ }
+ return cmd;
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStore.cs
new file mode 100644
index 000000000000..99bbc8e320b5
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStore.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.Extensions.VectorData;
+using Npgsql;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+///
+/// Represents a vector store implementation using PostgreSQL.
+///
+public class PostgresVectorStore : IVectorStore
+{
+ private readonly IPostgresVectorStoreDbClient _postgresClient;
+ private readonly NpgsqlDataSource? _dataSource;
+ private readonly PostgresVectorStoreOptions _options;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Postgres data source.
+ /// Optional configuration options for this class
+ public PostgresVectorStore(NpgsqlDataSource dataSource, PostgresVectorStoreOptions? options = default)
+ {
+ this._dataSource = dataSource;
+ this._options = options ?? new PostgresVectorStoreOptions();
+ this._postgresClient = new PostgresVectorStoreDbClient(this._dataSource, this._options.Schema);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An instance of .
+ /// Optional configuration options for this class
+ internal PostgresVectorStore(IPostgresVectorStoreDbClient postgresDbClient, PostgresVectorStoreOptions? options = default)
+ {
+ this._postgresClient = postgresDbClient;
+ this._options = options ?? new PostgresVectorStoreOptions();
+ }
+
+ ///
+ public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default)
+ {
+ const string OperationName = "ListCollectionNames";
+ return PostgresVectorStoreUtils.WrapAsyncEnumerableAsync(
+ this._postgresClient.GetTablesAsync(cancellationToken),
+ OperationName
+ );
+ }
+
+ ///
+ public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null)
+ where TKey : notnull
+ {
+ if (!PostgresConstants.SupportedKeyTypes.Contains(typeof(TKey)))
+ {
+ throw new NotSupportedException($"Unsupported key type: {typeof(TKey)}");
+ }
+
+ if (this._options.VectorStoreCollectionFactory is not null)
+ {
+ return this._options.VectorStoreCollectionFactory.CreateVectorStoreRecordCollection(this._postgresClient.DataSource, name, vectorStoreRecordDefinition);
+ }
+
+ var recordCollection = new PostgresVectorStoreRecordCollection(
+ this._postgresClient,
+ name,
+ new PostgresVectorStoreRecordCollectionOptions() { Schema = this._options.Schema, VectorStoreRecordDefinition = vectorStoreRecordDefinition }
+ );
+
+ return recordCollection as IVectorStoreRecordCollection ?? throw new InvalidOperationException("Failed to cast record collection.");
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStoreCollectionSqlBuilder.cs b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStoreCollectionSqlBuilder.cs
new file mode 100644
index 000000000000..eda009b6a1ae
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStoreCollectionSqlBuilder.cs
@@ -0,0 +1,453 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.Extensions.VectorData;
+using Npgsql;
+using NpgsqlTypes;
+using Pgvector;
+
+namespace Microsoft.SemanticKernel.Connectors.Postgres;
+
+///
+/// Provides methods to build SQL commands for managing vector store collections in PostgreSQL.
+///
+internal class PostgresVectorStoreCollectionSqlBuilder : IPostgresVectorStoreCollectionSqlBuilder
+{
+ ///
+ public PostgresSqlCommandInfo BuildDoesTableExistCommand(string schema, string tableName)
+ {
+ return new PostgresSqlCommandInfo(
+ commandText: @"
+ SELECT table_name
+ FROM information_schema.tables
+ WHERE table_schema = $1
+ AND table_type = 'BASE TABLE'
+ AND table_name = $2",
+ parameters: [
+ new NpgsqlParameter() { Value = schema },
+ new NpgsqlParameter() { Value = tableName }
+ ]
+ );
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildGetTablesCommand(string schema)
+ {
+ return new PostgresSqlCommandInfo(
+ commandText: @"
+ SELECT table_name
+ FROM information_schema.tables
+ WHERE table_schema = $1
+ AND table_type = 'BASE TABLE'",
+ parameters: [new NpgsqlParameter() { Value = schema }]
+ );
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildCreateTableCommand(string schema, string tableName, IReadOnlyList properties, bool ifNotExists = true)
+ {
+ if (string.IsNullOrWhiteSpace(tableName))
+ {
+ throw new ArgumentException("Table name cannot be null or whitespace", nameof(tableName));
+ }
+
+ VectorStoreRecordKeyProperty? keyProperty = default;
+ List dataProperties = new();
+ List vectorProperties = new();
+
+ foreach (var property in properties)
+ {
+ if (property is VectorStoreRecordKeyProperty keyProp)
+ {
+ if (keyProperty != null)
+ {
+ // Should be impossible, as property reader should have already validated that
+ // multiple key properties are not allowed.
+ throw new ArgumentException("Record definition cannot have more than one key property.");
+ }
+ keyProperty = keyProp;
+ }
+ else if (property is VectorStoreRecordDataProperty dataProp)
+ {
+ dataProperties.Add(dataProp);
+ }
+ else if (property is VectorStoreRecordVectorProperty vectorProp)
+ {
+ vectorProperties.Add(vectorProp);
+ }
+ else
+ {
+ throw new NotSupportedException($"Property type {property.GetType().Name} is not supported by this store.");
+ }
+ }
+
+ if (keyProperty == null)
+ {
+ throw new ArgumentException("Record definition must have a key property.");
+ }
+
+ var keyName = keyProperty.StoragePropertyName ?? keyProperty.DataModelPropertyName;
+
+ StringBuilder createTableCommand = new();
+ createTableCommand.AppendLine($"CREATE TABLE {(ifNotExists ? "IF NOT EXISTS " : "")}{schema}.\"{tableName}\" (");
+
+ // Add the key column
+ var keyPgTypeInfo = PostgresVectorStoreRecordPropertyMapping.GetPostgresTypeName(keyProperty.PropertyType);
+ createTableCommand.AppendLine($" \"{keyName}\" {keyPgTypeInfo.PgType} {(keyPgTypeInfo.IsNullable ? "" : "NOT NULL")},");
+
+ // Add the data columns
+ foreach (var dataProperty in dataProperties)
+ {
+ string columnName = dataProperty.StoragePropertyName ?? dataProperty.DataModelPropertyName;
+ var dataPgTypeInfo = PostgresVectorStoreRecordPropertyMapping.GetPostgresTypeName(dataProperty.PropertyType);
+ createTableCommand.AppendLine($" \"{columnName}\" {dataPgTypeInfo.PgType} {(dataPgTypeInfo.IsNullable ? "" : "NOT NULL")},");
+ }
+
+ // Add the vector columns
+ foreach (var vectorProperty in vectorProperties)
+ {
+ string columnName = vectorProperty.StoragePropertyName ?? vectorProperty.DataModelPropertyName;
+ var vectorPgTypeInfo = PostgresVectorStoreRecordPropertyMapping.GetPgVectorTypeName(vectorProperty);
+ createTableCommand.AppendLine($" \"{columnName}\" {vectorPgTypeInfo.PgType} {(vectorPgTypeInfo.IsNullable ? "" : "NOT NULL")},");
+ }
+
+ createTableCommand.AppendLine($" PRIMARY KEY (\"{keyName}\")");
+
+ createTableCommand.AppendLine(");");
+
+ return new PostgresSqlCommandInfo(commandText: createTableCommand.ToString());
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildCreateVectorIndexCommand(string schema, string tableName, string vectorColumnName, string indexKind, string distanceFunction)
+ {
+ // Only support creating HNSW index creation through the connector.
+ var indexTypeName = indexKind switch
+ {
+ IndexKind.Hnsw => "hnsw",
+ _ => throw new NotSupportedException($"Index kind '{indexKind}' is not supported for table creation. If you need to create an index of this type, please do so manually. Only HNSW indexes are supported through the vector store.")
+ };
+
+ distanceFunction ??= PostgresConstants.DefaultDistanceFunction; // Default to Cosine distance
+
+ var indexOps = distanceFunction switch
+ {
+ DistanceFunction.CosineDistance => "vector_cosine_ops",
+ DistanceFunction.CosineSimilarity => "vector_cosine_ops",
+ DistanceFunction.DotProductSimilarity => "vector_ip_ops",
+ DistanceFunction.EuclideanDistance => "vector_l2_ops",
+ DistanceFunction.ManhattanDistance => "vector_l1_ops",
+ _ => throw new NotSupportedException($"Distance function {distanceFunction} is not supported.")
+ };
+
+ var indexName = $"{tableName}_{vectorColumnName}_index";
+
+ return new PostgresSqlCommandInfo(
+ commandText: $@"
+ CREATE INDEX {indexName} ON {schema}.""{tableName}"" USING {indexTypeName} (""{vectorColumnName}"" {indexOps});"
+ );
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildDropTableCommand(string schema, string tableName)
+ {
+ return new PostgresSqlCommandInfo(
+ commandText: $@"DROP TABLE IF EXISTS {schema}.""{tableName}"""
+ );
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildUpsertCommand(string schema, string tableName, string keyColumn, Dictionary row)
+ {
+ var columns = row.Keys.ToList();
+ var columnNames = string.Join(", ", columns.Select(k => $"\"{k}\""));
+ var valuesParams = string.Join(", ", columns.Select((k, i) => $"${i + 1}"));
+ var columnsWithIndex = columns.Select((k, i) => (col: k, idx: i));
+ var updateColumnsWithParams = string.Join(", ", columnsWithIndex.Where(c => c.col != keyColumn).Select(c => $"\"{c.col}\"=${c.idx + 1}"));
+ var commandText = $@"
+ INSERT INTO {schema}.""{tableName}"" ({columnNames})
+ VALUES({valuesParams})
+ ON CONFLICT (""{keyColumn}"")
+ DO UPDATE SET {updateColumnsWithParams};";
+
+ return new PostgresSqlCommandInfo(commandText)
+ {
+ Parameters = columns.Select(c =>
+ PostgresVectorStoreRecordPropertyMapping.GetNpgsqlParameter(row[c])
+ ).ToList()
+ };
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildUpsertBatchCommand(string schema, string tableName, string keyColumn, List> rows)
+ {
+ if (rows == null || rows.Count == 0)
+ {
+ throw new ArgumentException("Rows cannot be null or empty", nameof(rows));
+ }
+
+ var firstRow = rows[0];
+ var columns = firstRow.Keys.ToList();
+
+ // Generate column names and parameter placeholders
+ var columnNames = string.Join(", ", columns.Select(c => $"\"{c}\""));
+ var valuePlaceholders = string.Join(", ", columns.Select((c, i) => $"${i + 1}"));
+ var valuesRows = string.Join(", ",
+ rows.Select((row, rowIndex) =>
+ $"({string.Join(", ",
+ columns.Select((c, colIndex) => $"${rowIndex * columns.Count + colIndex + 1}"))})"));
+
+ // Generate the update set clause
+ var updateSetClause = string.Join(", ", columns.Where(c => c != keyColumn).Select(c => $"\"{c}\" = EXCLUDED.\"{c}\""));
+
+ // Generate the SQL command
+ var commandText = $@"
+ INSERT INTO {schema}.""{tableName}"" ({columnNames})
+ VALUES {valuesRows}
+ ON CONFLICT (""{keyColumn}"")
+ DO UPDATE SET {updateSetClause}; ";
+
+ // Generate the parameters
+ var parameters = new List();
+ for (int rowIndex = 0; rowIndex < rows.Count; rowIndex++)
+ {
+ var row = rows[rowIndex];
+ foreach (var column in columns)
+ {
+ parameters.Add(new NpgsqlParameter()
+ {
+ Value = row[column] ?? DBNull.Value
+ });
+ }
+ }
+
+ return new PostgresSqlCommandInfo(commandText, parameters);
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildGetCommand(string schema, string tableName, IReadOnlyList properties, TKey key, bool includeVectors = false)
+ where TKey : notnull
+ {
+ List queryColumns = new();
+ string? keyColumn = null;
+
+ foreach (var property in properties)
+ {
+ if (property is VectorStoreRecordKeyProperty keyProperty)
+ {
+ if (keyColumn != null)
+ {
+ throw new ArgumentException("Record definition cannot have more than one key property.");
+ }
+ keyColumn = keyProperty.StoragePropertyName ?? keyProperty.DataModelPropertyName;
+ queryColumns.Add($"\"{keyColumn}\"");
+ }
+ else if (property is VectorStoreRecordDataProperty dataProperty)
+ {
+ string columnName = dataProperty.StoragePropertyName ?? dataProperty.DataModelPropertyName;
+ queryColumns.Add($"\"{columnName}\"");
+ }
+ else if (property is VectorStoreRecordVectorProperty vectorProperty && includeVectors)
+ {
+ string columnName = vectorProperty.StoragePropertyName ?? vectorProperty.DataModelPropertyName;
+ queryColumns.Add($"\"{columnName}\"");
+ }
+ }
+
+ Verify.NotNull(keyColumn, "Record definition must have a key property.");
+
+ var queryColumnList = string.Join(", ", queryColumns);
+
+ return new PostgresSqlCommandInfo(
+ commandText: $@"
+ SELECT {queryColumnList}
+ FROM {schema}.""{tableName}""
+ WHERE ""{keyColumn}"" = ${1};",
+ parameters: [new NpgsqlParameter() { Value = key }]
+ );
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildGetBatchCommand(string schema, string tableName, IReadOnlyList properties, List keys, bool includeVectors = false)
+ where TKey : notnull
+ {
+ NpgsqlDbType? keyType = PostgresVectorStoreRecordPropertyMapping.GetNpgsqlDbType(typeof(TKey)) ?? throw new ArgumentException($"Unsupported key type {typeof(TKey).Name}");
+
+ if (keys == null || keys.Count == 0)
+ {
+ throw new ArgumentException("Keys cannot be null or empty", nameof(keys));
+ }
+
+ var keyProperty = properties.OfType().FirstOrDefault() ?? throw new ArgumentException("Properties must contain a key property", nameof(properties));
+ var keyColumn = keyProperty.StoragePropertyName ?? keyProperty.DataModelPropertyName;
+
+ // Generate the column names
+ var columns = properties
+ .Where(p => includeVectors || p is not VectorStoreRecordVectorProperty)
+ .Select(p => p.StoragePropertyName ?? p.DataModelPropertyName)
+ .ToList();
+
+ var columnNames = string.Join(", ", columns.Select(c => $"\"{c}\""));
+ var keyParams = string.Join(", ", keys.Select((k, i) => $"${i + 1}"));
+
+ // Generate the SQL command
+ var commandText = $@"
+ SELECT {columnNames}
+ FROM {schema}.""{tableName}""
+ WHERE ""{keyColumn}"" = ANY($1);";
+
+ return new PostgresSqlCommandInfo(commandText)
+ {
+ Parameters = [new NpgsqlParameter() { Value = keys.ToArray(), NpgsqlDbType = NpgsqlDbType.Array | keyType.Value }]
+ };
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildDeleteCommand(string schema, string tableName, string keyColumn, TKey key)
+ {
+ return new PostgresSqlCommandInfo(
+ commandText: $@"
+ DELETE FROM {schema}.""{tableName}""
+ WHERE ""{keyColumn}"" = ${1};",
+ parameters: [new NpgsqlParameter() { Value = key }]
+ );
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildDeleteBatchCommand(string schema, string tableName, string keyColumn, List keys)
+ {
+ NpgsqlDbType? keyType = PostgresVectorStoreRecordPropertyMapping.GetNpgsqlDbType(typeof(TKey)) ?? throw new ArgumentException($"Unsupported key type {typeof(TKey).Name}");
+ if (keys == null || keys.Count == 0)
+ {
+ throw new ArgumentException("Keys cannot be null or empty", nameof(keys));
+ }
+
+ for (int i = 0; i < keys.Count; i++)
+ {
+ if (keys[i] == null)
+ {
+ throw new ArgumentException("Keys cannot contain null values", nameof(keys));
+ }
+ }
+
+ var commandText = $@"
+ DELETE FROM {schema}.""{tableName}""
+ WHERE ""{keyColumn}"" = ANY($1);";
+
+ return new PostgresSqlCommandInfo(commandText)
+ {
+ Parameters = [new NpgsqlParameter() { Value = keys, NpgsqlDbType = NpgsqlDbType.Array | keyType.Value }]
+ };
+ }
+
+ ///
+ public PostgresSqlCommandInfo BuildGetNearestMatchCommand(
+ string schema, string tableName, IReadOnlyList properties, VectorStoreRecordVectorProperty vectorProperty, Vector vectorValue,
+ VectorSearchFilter? filter, int? skip, bool includeVectors, int limit)
+ {
+ var columns = string.Join(" ,",
+ properties
+ .Select(property => property.StoragePropertyName ?? property.DataModelPropertyName)
+ .Select(column => $"\"{column}\"")
+ );
+
+ var distanceFunction = vectorProperty.DistanceFunction ?? PostgresConstants.DefaultDistanceFunction;
+ var distanceOp = distanceFunction switch
+ {
+ DistanceFunction.CosineDistance => "<=>",
+ DistanceFunction.CosineSimilarity => "<=>",
+ DistanceFunction.EuclideanDistance => "<->",
+ DistanceFunction.ManhattanDistance => "<+>",
+ DistanceFunction.DotProductSimilarity => "<#>",
+ null or "" => "<->", // Default to Euclidean distance
+ _ => throw new NotSupportedException($"Distance function {vectorProperty.DistanceFunction} is not supported.")
+ };
+
+ var vectorColumn = vectorProperty.StoragePropertyName ?? vectorProperty.DataModelPropertyName;
+ // Start where clause params at 2, vector takes param 1.
+ var where = GenerateWhereClause(schema, tableName, properties, filter, startParamIndex: 2);
+
+ var commandText = $@"
+ SELECT {columns}, ""{vectorColumn}"" {distanceOp} $1 AS ""{PostgresConstants.DistanceColumnName}""
+ FROM {schema}.""{tableName}"" {where.Clause}
+ ORDER BY {PostgresConstants.DistanceColumnName}
+ LIMIT {limit}";
+
+ if (skip.HasValue) { commandText += $" OFFSET {skip.Value}"; }
+
+ // For cosine similarity, we need to take 1 - cosine distance.
+ // However, we can't use an expression in the ORDER BY clause or else the index won't be used.
+ // Instead we'll wrap the query in a subquery and modify the distance in the outer query.
+ if (vectorProperty.DistanceFunction == DistanceFunction.CosineSimilarity)
+ {
+ commandText = $@"
+ SELECT {columns}, 1 - ""{PostgresConstants.DistanceColumnName}"" AS ""{PostgresConstants.DistanceColumnName}""
+ FROM ({commandText}) AS subquery";
+ }
+
+ // For inner product, we need to take -1 * inner product.
+ // However, we can't use an expression in the ORDER BY clause or else the index won't be used.
+ // Instead we'll wrap the query in a subquery and modify the distance in the outer query.
+ if (vectorProperty.DistanceFunction == DistanceFunction.DotProductSimilarity)
+ {
+ commandText = $@"
+ SELECT {columns}, -1 * ""{PostgresConstants.DistanceColumnName}"" AS ""{PostgresConstants.DistanceColumnName}""
+ FROM ({commandText}) AS subquery";
+ }
+
+ return new PostgresSqlCommandInfo(commandText)
+ {
+ Parameters = [new NpgsqlParameter() { Value = vectorValue }, .. where.Parameters.Select(p => new NpgsqlParameter() { Value = p })]
+ };
+ }
+
+ internal static (string Clause, List