From 16ec4db9cdb30b54f3ac339fa80266629e226135 Mon Sep 17 00:00:00 2001 From: Alexander Zarei Date: Fri, 31 Oct 2025 01:19:14 -0700 Subject: [PATCH] Change ITextSearch.GetSearchResultsAsync to return KernelSearchResults - Change interface return type from KernelSearchResults to KernelSearchResults - Update VectorStoreTextSearch implementation with new GetResultsAsTRecordAsync helper - Keep GetResultsAsRecordAsync for legacy ITextSearch interface backward compatibility - Update 3 unit tests to use strongly-typed DataModel instead of object Benefits: - Improved type safety - no more casting required - Better IntelliSense and developer experience - Zero breaking changes to legacy ITextSearch interface - All 19 unit tests pass This is Part 2.1 of the Issue #10456 multi-PR chain, refining the API . --- .../Data/TextSearch/ITextSearch.cs | 4 +-- .../Data/TextSearch/VectorStoreTextSearch.cs | 26 +++++++++++++++++-- .../Data/VectorStoreTextSearchTests.cs | 15 ++++++----- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/ITextSearch.cs b/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/ITextSearch.cs index 57da1a9ec677..e955af86bc6c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/ITextSearch.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/ITextSearch.cs @@ -36,12 +36,12 @@ Task> GetTextSearchResultsAsync( CancellationToken cancellationToken = default); /// - /// Perform a search for content related to the specified query and return values representing the search results. + /// Perform a search for content related to the specified query and return strongly-typed values representing the search results. /// /// What to search for. /// Options used when executing a text search. /// The to monitor for cancellation requests. The default is . - Task> GetSearchResultsAsync( + Task> GetSearchResultsAsync( string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default); diff --git a/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs b/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs index 121ff9b6c7bb..f1b18483c43a 100644 --- a/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs +++ b/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs @@ -213,11 +213,11 @@ Task> ITextSearch.GetTextSearchRe } /// - Task> ITextSearch.GetSearchResultsAsync(string query, TextSearchOptions? searchOptions, CancellationToken cancellationToken) + Task> ITextSearch.GetSearchResultsAsync(string query, TextSearchOptions? searchOptions, CancellationToken cancellationToken) { var searchResponse = this.ExecuteVectorSearchAsync(query, searchOptions, cancellationToken); - return Task.FromResult(new KernelSearchResults(this.GetResultsAsRecordAsync(searchResponse, cancellationToken))); + return Task.FromResult(new KernelSearchResults(this.GetResultsAsTRecordAsync(searchResponse, cancellationToken))); } #region private @@ -367,6 +367,28 @@ private async IAsyncEnumerable GetResultsAsRecordAsync(IAsyncEnumerable< } } + /// + /// Return the search results as strongly-typed instances. + /// + /// Response containing the records matching the query. + /// Cancellation token + private async IAsyncEnumerable GetResultsAsTRecordAsync(IAsyncEnumerable>? searchResponse, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (searchResponse is null) + { + yield break; + } + + await foreach (var result in searchResponse.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (result.Record is not null) + { + yield return result.Record; + await Task.Yield(); + } + } + } + /// /// Return the search results as instances of . /// diff --git a/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs b/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs index 8dd095710c06..75f4b090590e 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs @@ -78,12 +78,14 @@ public async Task CanGetSearchResultAsync() { // Arrange. var sut = await CreateVectorStoreTextSearchAsync(); + ITextSearch typeSafeInterface = sut; // Act. - KernelSearchResults searchResults = await sut.GetSearchResultsAsync("What is the Semantic Kernel?", new() { Top = 2, Skip = 0 }); + KernelSearchResults searchResults = await typeSafeInterface.GetSearchResultsAsync("What is the Semantic Kernel?", new TextSearchOptions { Top = 2, Skip = 0 }); var results = await searchResults.Results.ToListAsync(); Assert.Equal(2, results.Count); + Assert.All(results, result => Assert.IsType(result)); } [Fact] @@ -117,12 +119,14 @@ public async Task CanGetSearchResultsWithEmbeddingGeneratorAsync() { // Arrange. var sut = await CreateVectorStoreTextSearchWithEmbeddingGeneratorAsync(); + ITextSearch typeSafeInterface = sut; // Act. - KernelSearchResults searchResults = await sut.GetSearchResultsAsync("What is the Semantic Kernel?", new() { Top = 2, Skip = 0 }); + KernelSearchResults searchResults = await typeSafeInterface.GetSearchResultsAsync("What is the Semantic Kernel?", new TextSearchOptions { Top = 2, Skip = 0 }); var results = await searchResults.Results.ToListAsync(); Assert.Equal(2, results.Count); + Assert.All(results, result => Assert.IsType(result)); } #pragma warning disable CS0618 // VectorStoreTextSearch with ITextEmbeddingGenerationService is obsolete @@ -270,17 +274,16 @@ public async Task LinqGetSearchResultsAsync() Filter = r => r.Tag == "Even" }; - KernelSearchResults searchResults = await typeSafeInterface.GetSearchResultsAsync( + KernelSearchResults searchResults = await typeSafeInterface.GetSearchResultsAsync( "What is the Semantic Kernel?", searchOptions); var results = await searchResults.Results.ToListAsync(); - // Assert - Results should be DataModel objects with Tag == "Even" + // Assert - Results should be strongly-typed DataModel objects with Tag == "Even" Assert.NotEmpty(results); Assert.All(results, result => { - var dataModel = Assert.IsType(result); - Assert.Equal("Even", dataModel.Tag); + Assert.Equal("Even", result.Tag); // Direct property access - no cast needed! }); }