@@ -24,7 +24,7 @@ namespace Microsoft.SemanticKernel.Connectors.PgVector;
2424/// <typeparam name="TKey">The type of the key.</typeparam>
2525/// <typeparam name="TRecord">The type of the record.</typeparam>
2626#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
27- public class PostgresCollection < TKey , TRecord > : VectorStoreCollection < TKey , TRecord >
27+ public class PostgresCollection < TKey , TRecord > : VectorStoreCollection < TKey , TRecord > , IKeywordHybridSearchable < TRecord >
2828#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
2929 where TKey : notnull
3030 where TRecord : class
@@ -52,6 +52,9 @@ public class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRe
5252 /// <summary>The default options for vector search.</summary>
5353 private static readonly VectorSearchOptions < TRecord > s_defaultVectorSearchOptions = new ( ) ;
5454
55+ /// <summary>The default options for hybrid search.</summary>
56+ private static readonly HybridSearchOptions < TRecord > s_defaultHybridSearchOptions = new ( ) ;
57+
5558 /// <summary>
5659 /// Initializes a new instance of the <see cref="PostgresCollection{TKey, TRecord}"/> class.
5760 /// </summary>
@@ -396,43 +399,7 @@ public override async IAsyncEnumerable<VectorSearchResult<TRecord>> SearchAsync<
396399 }
397400
398401 var vectorProperty = this . _model . GetVectorPropertyOrSingle ( options ) ;
399-
400- object vector = searchValue switch
401- {
402- // Dense float32
403- ReadOnlyMemory < float > r => r ,
404- float [ ] f => new ReadOnlyMemory < float > ( f ) ,
405- Embedding < float > e => e . Vector ,
406- _ when vectorProperty . EmbeddingGenerator is IEmbeddingGenerator < TInput , Embedding < float > > generator
407- => await generator . GenerateVectorAsync ( searchValue , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ,
408-
409- #if NET
410- // Dense float16
411- ReadOnlyMemory < Half > r => r ,
412- Half [ ] f => new ReadOnlyMemory < Half > ( f ) ,
413- Embedding < Half > e => e . Vector ,
414- _ when vectorProperty. EmbeddingGenerator is IEmbeddingGenerator < TInput , Embedding < Half > > generator
415- => await generator . GenerateVectorAsync ( searchValue , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ,
416- #endif
417-
418- // Dense Binary
419- BitArray b => b ,
420- BinaryEmbedding e => e . Vector ,
421- _ when vectorProperty . EmbeddingGenerator is IEmbeddingGenerator < TInput , BinaryEmbedding > generator
422- => await generator . GenerateAsync ( searchValue , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ,
423-
424- // Sparse
425- SparseVector sv => sv ,
426- // TODO: Add a PG-specific SparseVectorEmbedding type
427-
428- _ => vectorProperty . EmbeddingGenerator is null
429- ? throw new NotSupportedException ( VectorDataStrings . InvalidSearchInputAndNoEmbeddingGeneratorWasConfigured ( searchValue . GetType ( ) , PostgresModelBuilder . SupportedVectorTypes ) )
430- : throw new InvalidOperationException ( VectorDataStrings . IncompatibleEmbeddingGeneratorWasConfiguredForInputType ( typeof ( TInput ) , vectorProperty . EmbeddingGenerator . GetType ( ) ) )
431- } ;
432-
433- var pgVector = PostgresPropertyMapping . MapVectorForStorageModel ( vector ) ;
434-
435- Verify . NotNull ( pgVector ) ;
402+ var pgVector = await this . ConvertSearchInputToVectorAsync ( searchValue , vectorProperty , cancellationToken ) . ConfigureAwait ( false ) ;
436403
437404 // Simulating skip/offset logic locally, since OFFSET can work only with LIMIT in combination
438405 // 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<TInput, BinaryEm
460427 }
461428 }
462429
430+ /// <inheritdoc />
431+ public async IAsyncEnumerable < VectorSearchResult < TRecord > > HybridSearchAsync < TInput > (
432+ TInput searchValue ,
433+ ICollection < string > keywords ,
434+ int top ,
435+ HybridSearchOptions < TRecord > ? options = null ,
436+ [ EnumeratorCancellation ] CancellationToken cancellationToken = default )
437+ where TInput : notnull
438+ {
439+ Verify . NotNull ( searchValue ) ;
440+ Verify . NotNull ( keywords ) ;
441+ Verify . NotLessThan ( top , 1 ) ;
442+
443+ options ??= s_defaultHybridSearchOptions ;
444+ if ( options . IncludeVectors && this . _model . EmbeddingGenerationRequired )
445+ {
446+ throw new NotSupportedException ( VectorDataStrings . IncludeVectorsNotSupportedWithEmbeddingGeneration ) ;
447+ }
448+
449+ var vectorProperty = this . _model . GetVectorPropertyOrSingle < TRecord > ( new ( ) { VectorProperty = options . VectorProperty } ) ;
450+ var textProperty = this . _model . GetFullTextDataPropertyOrSingle ( options . AdditionalProperty ) ;
451+ var pgVector = await this . ConvertSearchInputToVectorAsync ( searchValue , vectorProperty , cancellationToken ) . ConfigureAwait ( false ) ;
452+
453+ using var connection = await this . _dataSource . OpenConnectionAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
454+ using var command = connection . CreateCommand ( ) ;
455+ PostgresSqlBuilder . BuildHybridSearchCommand ( command , this . _schema , this . Name , this . _model , vectorProperty , textProperty , pgVector , keywords ,
456+ #pragma warning disable CS0618 // VectorSearchFilter is obsolete
457+ options . OldFilter ,
458+ #pragma warning restore CS0618 // VectorSearchFilter is obsolete
459+ options . Filter , options . Skip , options . IncludeVectors , top , options . ScoreThreshold ) ;
460+
461+ using var reader = await connection . ExecuteWithErrorHandlingAsync (
462+ this . _collectionMetadata ,
463+ "HybridSearch" ,
464+ ( ) => command . ExecuteReaderAsync ( cancellationToken ) ,
465+ cancellationToken ) . ConfigureAwait ( false ) ;
466+
467+ while ( await reader . ReadWithErrorHandlingAsync ( this . _collectionMetadata , "HybridSearch" , cancellationToken ) . ConfigureAwait ( false ) )
468+ {
469+ yield return new VectorSearchResult < TRecord > (
470+ this . _mapper . MapFromStorageToDataModel ( reader , options . IncludeVectors ) ,
471+ reader . GetDouble ( reader . GetOrdinal ( PostgresConstants . DistanceColumnName ) ) ) ;
472+ }
473+ }
474+
463475 #endregion Search
464476
465477 /// <inheritdoc />
@@ -513,11 +525,11 @@ private async Task InternalCreateCollectionAsync(bool ifNotExists, CancellationT
513525 batch . BatchCommands . Add (
514526 new NpgsqlBatchCommand ( PostgresSqlBuilder . BuildCreateTableSql ( this . _schema , this . Name , this . _model , pgVersion , ifNotExists ) ) ) ;
515527
516- foreach ( var ( column , kind , function , isVector ) in PostgresPropertyMapping . GetIndexInfo ( this . _model . Properties ) )
528+ foreach ( var ( column , kind , function , isVector , isFullText , fullTextLanguage ) in PostgresPropertyMapping . GetIndexInfo ( this . _model . Properties ) )
517529 {
518530 batch . BatchCommands . Add (
519531 new NpgsqlBatchCommand (
520- PostgresSqlBuilder . BuildCreateIndexSql ( this . _schema , this . Name , column , kind , function , isVector , ifNotExists ) ) ) ;
532+ PostgresSqlBuilder . BuildCreateIndexSql ( this . _schema , this . Name , column , kind , function , isVector , isFullText , fullTextLanguage , ifNotExists ) ) ) ;
521533 }
522534
523535 await batch . ExecuteNonQueryAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
@@ -535,4 +547,48 @@ private Task<T> RunOperationAsync<T>(string operationName, Func<Task<T>> operati
535547 this . _collectionMetadata ,
536548 operationName ,
537549 operation ) ;
550+
551+ /// <summary>
552+ /// Converts a search input value to a PostgreSQL vector representation, generating embeddings if necessary.
553+ /// </summary>
554+ private async Task < object > ConvertSearchInputToVectorAsync < TInput > ( TInput searchValue , VectorPropertyModel vectorProperty , CancellationToken cancellationToken )
555+ where TInput : notnull
556+ {
557+ object vector = searchValue switch
558+ {
559+ // Dense float32
560+ ReadOnlyMemory < float > r => r ,
561+ float [ ] f => new ReadOnlyMemory < float > ( f ) ,
562+ Embedding < float > e => e . Vector ,
563+ _ when vectorProperty . EmbeddingGenerator is IEmbeddingGenerator < TInput , Embedding < float > > generator
564+ => await generator . GenerateVectorAsync ( searchValue , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ,
565+
566+ #if NET
567+ // Dense float16
568+ ReadOnlyMemory < Half > r => r ,
569+ Half [ ] f => new ReadOnlyMemory < Half > ( f ) ,
570+ Embedding < Half > e => e . Vector ,
571+ _ when vectorProperty. EmbeddingGenerator is IEmbeddingGenerator < TInput , Embedding < Half > > generator
572+ => await generator . GenerateVectorAsync ( searchValue , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ,
573+ #endif
574+
575+ // Dense Binary
576+ BitArray b => b ,
577+ BinaryEmbedding e => e . Vector ,
578+ _ when vectorProperty . EmbeddingGenerator is IEmbeddingGenerator < TInput , BinaryEmbedding > generator
579+ => await generator . GenerateAsync ( searchValue , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ,
580+
581+ // Sparse
582+ SparseVector sv => sv ,
583+ // TODO: Add a PG-specific SparseVectorEmbedding type
584+
585+ _ => vectorProperty . EmbeddingGenerator is null
586+ ? throw new NotSupportedException ( VectorDataStrings . InvalidSearchInputAndNoEmbeddingGeneratorWasConfigured ( searchValue . GetType ( ) , PostgresModelBuilder . SupportedVectorTypes ) )
587+ : throw new InvalidOperationException ( VectorDataStrings . IncompatibleEmbeddingGeneratorWasConfiguredForInputType ( typeof ( TInput ) , vectorProperty . EmbeddingGenerator . GetType ( ) ) )
588+ } ;
589+
590+ var pgVector = PostgresPropertyMapping . MapVectorForStorageModel ( vector ) ;
591+ Verify . NotNull ( pgVector ) ;
592+ return pgVector ;
593+ }
538594}
0 commit comments