diff --git a/dotnet/src/VectorData/PgVector/PostgresCollection.cs b/dotnet/src/VectorData/PgVector/PostgresCollection.cs
index a0c2273fabc8..f9061387378b 100644
--- a/dotnet/src/VectorData/PgVector/PostgresCollection.cs
+++ b/dotnet/src/VectorData/PgVector/PostgresCollection.cs
@@ -24,7 +24,7 @@ namespace Microsoft.SemanticKernel.Connectors.PgVector;
/// The type of the key.
/// The type of the record.
#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
-public class PostgresCollection : VectorStoreCollection
+public class PostgresCollection : VectorStoreCollection, IKeywordHybridSearchable
#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
where TKey : notnull
where TRecord : class
@@ -52,6 +52,9 @@ public class PostgresCollection : VectorStoreCollectionThe default options for vector search.
private static readonly VectorSearchOptions s_defaultVectorSearchOptions = new();
+ /// The default options for hybrid search.
+ private static readonly HybridSearchOptions s_defaultHybridSearchOptions = new();
+
///
/// Initializes a new instance of the class.
///
@@ -396,43 +399,7 @@ public override async IAsyncEnumerable> SearchAsync<
}
var vectorProperty = this._model.GetVectorPropertyOrSingle(options);
-
- object vector = searchValue switch
- {
- // Dense float32
- ReadOnlyMemory r => r,
- float[] f => new ReadOnlyMemory(f),
- Embedding e => e.Vector,
- _ when vectorProperty.EmbeddingGenerator is IEmbeddingGenerator> generator
- => await generator.GenerateVectorAsync(searchValue, cancellationToken: cancellationToken).ConfigureAwait(false),
-
-#if NET
- // Dense float16
- ReadOnlyMemory r => r,
- Half[] f => new ReadOnlyMemory(f),
- Embedding e => e.Vector,
- _ when vectorProperty.EmbeddingGenerator is IEmbeddingGenerator> generator
- => await generator.GenerateVectorAsync(searchValue, cancellationToken: cancellationToken).ConfigureAwait(false),
-#endif
-
- // Dense Binary
- BitArray b => b,
- BinaryEmbedding e => e.Vector,
- _ when vectorProperty.EmbeddingGenerator is IEmbeddingGenerator generator
- => await generator.GenerateAsync(searchValue, cancellationToken: cancellationToken).ConfigureAwait(false),
-
- // Sparse
- SparseVector sv => sv,
- // TODO: Add a PG-specific SparseVectorEmbedding type
-
- _ => vectorProperty.EmbeddingGenerator is null
- ? throw new NotSupportedException(VectorDataStrings.InvalidSearchInputAndNoEmbeddingGeneratorWasConfigured(searchValue.GetType(), PostgresModelBuilder.SupportedVectorTypes))
- : throw new InvalidOperationException(VectorDataStrings.IncompatibleEmbeddingGeneratorWasConfiguredForInputType(typeof(TInput), vectorProperty.EmbeddingGenerator.GetType()))
- };
-
- var pgVector = PostgresPropertyMapping.MapVectorForStorageModel(vector);
-
- Verify.NotNull(pgVector);
+ var pgVector = await this.ConvertSearchInputToVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
// Simulating skip/offset logic locally, since OFFSET can work only with LIMIT in combination
// and LIMIT is not supported in vector search extension, instead of LIMIT - "k" parameter is used.
@@ -460,6 +427,51 @@ _ when vectorProperty.EmbeddingGenerator is IEmbeddingGenerator
+ public async IAsyncEnumerable> HybridSearchAsync(
+ TInput searchValue,
+ ICollection keywords,
+ int top,
+ HybridSearchOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ where TInput : notnull
+ {
+ Verify.NotNull(searchValue);
+ Verify.NotNull(keywords);
+ Verify.NotLessThan(top, 1);
+
+ options ??= s_defaultHybridSearchOptions;
+ if (options.IncludeVectors && this._model.EmbeddingGenerationRequired)
+ {
+ throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration);
+ }
+
+ var vectorProperty = this._model.GetVectorPropertyOrSingle(new() { VectorProperty = options.VectorProperty });
+ var textProperty = this._model.GetFullTextDataPropertyOrSingle(options.AdditionalProperty);
+ var pgVector = await this.ConvertSearchInputToVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false);
+
+ using var connection = await this._dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
+ using var command = connection.CreateCommand();
+ PostgresSqlBuilder.BuildHybridSearchCommand(command, this._schema, this.Name, this._model, vectorProperty, textProperty, pgVector, keywords,
+#pragma warning disable CS0618 // VectorSearchFilter is obsolete
+ options.OldFilter,
+#pragma warning restore CS0618 // VectorSearchFilter is obsolete
+ options.Filter, options.Skip, options.IncludeVectors, top, options.ScoreThreshold);
+
+ using var reader = await connection.ExecuteWithErrorHandlingAsync(
+ this._collectionMetadata,
+ "HybridSearch",
+ () => command.ExecuteReaderAsync(cancellationToken),
+ cancellationToken).ConfigureAwait(false);
+
+ while (await reader.ReadWithErrorHandlingAsync(this._collectionMetadata, "HybridSearch", cancellationToken).ConfigureAwait(false))
+ {
+ yield return new VectorSearchResult(
+ this._mapper.MapFromStorageToDataModel(reader, options.IncludeVectors),
+ reader.GetDouble(reader.GetOrdinal(PostgresConstants.DistanceColumnName)));
+ }
+ }
+
#endregion Search
///
@@ -513,11 +525,11 @@ private async Task InternalCreateCollectionAsync(bool ifNotExists, CancellationT
batch.BatchCommands.Add(
new NpgsqlBatchCommand(PostgresSqlBuilder.BuildCreateTableSql(this._schema, this.Name, this._model, pgVersion, ifNotExists)));
- foreach (var (column, kind, function, isVector) in PostgresPropertyMapping.GetIndexInfo(this._model.Properties))
+ foreach (var (column, kind, function, isVector, isFullText, fullTextLanguage) in PostgresPropertyMapping.GetIndexInfo(this._model.Properties))
{
batch.BatchCommands.Add(
new NpgsqlBatchCommand(
- PostgresSqlBuilder.BuildCreateIndexSql(this._schema, this.Name, column, kind, function, isVector, ifNotExists)));
+ PostgresSqlBuilder.BuildCreateIndexSql(this._schema, this.Name, column, kind, function, isVector, isFullText, fullTextLanguage, ifNotExists)));
}
await batch.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
@@ -535,4 +547,48 @@ private Task RunOperationAsync(string operationName, Func> operati
this._collectionMetadata,
operationName,
operation);
+
+ ///
+ /// Converts a search input value to a PostgreSQL vector representation, generating embeddings if necessary.
+ ///
+ private async Task