Skip to content

Commit ebd579d

Browse files
authored
.NET: Add ITextSearch<TRecord> with LINQ filtering and deprecate legacy ITextSearch (#10456) (#13179)
# .NET: Add LINQ-based ITextSearch<TRecord> interface and deprecate legacy ITextSearch (#10456) ## Summary This PR implements **Option 3** from the architectural decision process for Issue #10456: introduces a new generic `ITextSearch<TRecord>` interface with type-safe LINQ filtering while maintaining the legacy `ITextSearch` interface marked as `[Obsolete]` for backward compatibility. **Zero breaking changes** - existing code continues working unchanged. ## What Changed ### New Generic Interface (Recommended Path) ```csharp public interface ITextSearch<TRecord> { Task<KernelSearchResults<string>> SearchAsync( string query, TextSearchOptions<TRecord>? searchOptions = null, CancellationToken cancellationToken = default); // + GetTextSearchResults and GetSearchResults methods } // Type-safe LINQ filtering with IntelliSense var options = new TextSearchOptions<CorporateDocument> { Filter = doc => doc.Department == "HR" && doc.IsActive && doc.CreatedDate > DateTime.Now.AddYears(-2) }; ``` **Benefits:** - ✅ Compile-time type safety - ✅ IntelliSense support for property names - ✅ Full LINQ expression support - ✅ No RequiresDynamicCode attributes - ✅ AOT-compatible (simple equality/comparison patterns) ### Legacy Interface (Deprecated) ```csharp [Obsolete("Use ITextSearch<TRecord> with LINQ-based filtering instead. This interface will be removed in a future version.")] public interface ITextSearch { Task<KernelSearchResults<string>> SearchAsync( string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default); } // Legacy clause-based filtering (still works) var options = new TextSearchOptions { Filter = new TextSearchFilter().Equality("Department", "HR") }; ``` **Migration Message:** Users see deprecation warning directing them to modern `ITextSearch<TRecord>` with LINQ filtering. ## Implementation Details ### Dual-Path Architecture `VectorStoreTextSearch<TRecord>` implements both interfaces with independent code paths: **Legacy Path (Non-Generic):** ```csharp async IAsyncEnumerable<VectorSearchResult<TRecord>> ExecuteVectorSearchAsync( string query, TextSearchOptions options) { var vectorOptions = new VectorSearchOptions<TRecord> { #pragma warning disable CS0618 // VectorSearchFilter is obsolete OldFilter = options.Filter?.FilterClauses != null ? new VectorSearchFilter(options.Filter.FilterClauses) : null #pragma warning restore CS0618 }; // ... execute search } ``` **Modern Path (Generic):** ```csharp async IAsyncEnumerable<VectorSearchResult<TRecord>> ExecuteVectorSearchAsync( string query, TextSearchOptions<TRecord> options) { var vectorOptions = new VectorSearchOptions<TRecord> { Filter = options.Filter // Direct LINQ passthrough }; // ... execute search } ``` **Key Characteristics:** - Two independent methods (no translation layer, no conversion overhead) - Legacy path uses obsolete `VectorSearchFilter` with pragma suppressions (temporary during transition) - Modern path uses LINQ expressions directly (no obsolete APIs) - Both paths are AOT-compatible (no dynamic code generation) ## Files Changed ### Interfaces & Options - `ITextSearch.cs`: Added `ITextSearch<TRecord>` interface, marked legacy `ITextSearch` as `[Obsolete]` - `TextSearchOptions.cs`: Added generic `TextSearchOptions<TRecord>` class ### Implementation - `VectorStoreTextSearch.cs`: Implemented dual interface pattern (~30 lines for both paths) ### Backward Compatibility (Pragma Suppressions) Added `#pragma warning disable CS0618` to **27 files** that use the obsolete interface: **Production (11 files):** - Web search connectors (Bing, Google, Brave, Tavily) - Extension methods (WebServiceCollectionExtensions, TextSearchExtensions) - Core implementations (TextSearchProvider, TextSearchStore, VectorStoreTextSearch) **Tests/Samples (16 files):** - Integration tests (Agents, AzureAISearch, InMemory, Qdrant, Web plugins) - Unit tests (Bing, Brave, Google, Tavily) - Sample tutorials (Step1_Web_Search, Step2_Search_For_RAG) - Mock implementations ### Tests - Added 7 new tests for LINQ filtering scenarios - Maintained 10 existing legacy tests (unchanged) - Added `DataModelWithTags` to test base for collection filtering ## Validation Results - ✅ **Build**: 0 errors, 0 warnings with `--warnaserror` - ✅ **Tests**: 1,581/1,581 passed (100%) - ✅ **Format**: Clean - ✅ **AOT Compatibility**: All checks passed - ✅ **CI/CD**: Run #29857 succeeded ## Breaking Changes **None.** This is a non-breaking addition: - Legacy `ITextSearch` interface continues working (marked `[Obsolete]`) - Existing implementations (Bing, Google, Azure AI Search) unchanged - Migration to `ITextSearch<TRecord>` is opt-in via deprecation warning ## Multi-PR Context This is **PR 2 of 6** in the structured implementation for Issue #10456: - **PR1** ✅: Generic interfaces foundation - **PR2** ← YOU ARE HERE: Dual interface pattern + deprecation - **PR3-PR6**: Connector migrations (Bing, Google, Brave, Azure AI Search) ## Architectural Decision **Option 3 Approved** by Mark Wallace and Westey-m: > "We typically follow the pattern of obsoleting the old API when we introduce the new pattern. This avoids breaking changes which are very disruptive for projects that have a transient dependency." - Mark Wallace > "I prefer a clean separation between the old and new abstractions. Being able to obsolete the old ones and point users at the new ones is definitely valuable." - Westey-m ### Options Considered: 1. **Native LINQ Only**: Replace `TextSearchFilter` entirely (breaking change) 2. **Translation Layer**: Convert `TextSearchFilter` to LINQ internally (RequiresDynamicCode cascade, AOT issues) 3. **Dual Interface** ✅: Add `ITextSearch<TRecord>` + deprecate legacy (no breaking changes, clean separation) See ADR comments in conversation for detailed architectural analysis. ## Migration Guide **Before (Legacy - Now Obsolete):** ```csharp ITextSearch search = ...; var options = new TextSearchOptions { Filter = new TextSearchFilter() .Equality("Department", "HR") .Equality("IsActive", "true") }; var results = await search.SearchAsync("query", options); ``` **After (Modern - Recommended):** ```csharp ITextSearch<CorporateDocument> search = ...; var options = new TextSearchOptions<CorporateDocument> { Filter = doc => doc.Department == "HR" && doc.IsActive }; var results = await search.SearchAsync("query", options); ``` ## Next Steps PR3-PR6 will migrate connector implementations (Bing, Google, Brave, Azure AI Search) to use `ITextSearch<TRecord>` with LINQ filtering, demonstrating the modern pattern while maintaining backward compatibility. --------- Co-authored-by: Alexander Zarei <alzarei@users.noreply.github.com>
1 parent b94f3f3 commit ebd579d

File tree

30 files changed

+312
-4
lines changed

30 files changed

+312
-4
lines changed

dotnet/samples/GettingStartedWithTextSearch/Step1_Web_Search.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // ITextSearch is obsolete - Sample demonstrates legacy interface usage
4+
35
using Microsoft.SemanticKernel.Data;
46
using Microsoft.SemanticKernel.Plugins.Web.Bing;
57
using Microsoft.SemanticKernel.Plugins.Web.Google;

dotnet/samples/GettingStartedWithTextSearch/Step2_Search_For_RAG.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
2+
3+
#pragma warning disable CS0618 // ITextSearch is obsolete - Sample demonstrates legacy interface usage
4+
25
using System.Text.RegularExpressions;
36
using HtmlAgilityPack;
47
using Microsoft.SemanticKernel;

dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentWithTextSearchProviderConformance/AgentWithTextSearchProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public abstract class AgentWithTextSearchProvider<TFixture>(Func<TFixture> creat
4141
public async Task TextSearchBehaviorStateIsUsedByAgentInternalAsync(string question, string expectedResult, params string[] ragResults)
4242
{
4343
// Arrange
44+
#pragma warning disable CS0618 // ITextSearch is obsolete - Testing legacy interface
4445
var mockTextSearch = new Mock<ITextSearch>();
46+
#pragma warning restore CS0618
4547
mockTextSearch.Setup(x => x.GetTextSearchResultsAsync(
4648
It.IsAny<string>(),
4749
It.IsAny<TextSearchOptions>(),

dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchTextSearchTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // ITextSearch is obsolete
4+
35
using System;
46
using System.Threading.Tasks;
57
using Azure.AI.OpenAI;

dotnet/src/IntegrationTests/Connectors/Memory/InMemory/InMemoryVectorStoreTextSearchTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // ITextSearch is obsolete
4+
35
using System;
46
using System.Threading.Tasks;
57
using Microsoft.Extensions.AI;

dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantTextSearchTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // ITextSearch is obsolete
4+
35
using System;
46
using System.Threading.Tasks;
57
using Microsoft.SemanticKernel.Connectors.Qdrant;

dotnet/src/IntegrationTests/Data/BaseTextSearchTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // Type or member is obsolete - Testing legacy non-generic ITextSearch interface
4+
35
using System;
46
using System.Collections.Generic;
57
using System.Linq;

dotnet/src/IntegrationTests/Plugins/Web/Bing/BingTextSearchTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // ITextSearch is obsolete
4+
35
using System.Threading.Tasks;
46
using Microsoft.Extensions.Configuration;
57
using Microsoft.SemanticKernel.Data;

dotnet/src/IntegrationTests/Plugins/Web/Google/GoogleTextSearchTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // ITextSearch is obsolete
4+
35
using System.Threading.Tasks;
46
using Microsoft.Extensions.Configuration;
57
using Microsoft.SemanticKernel.Data;

dotnet/src/IntegrationTests/Plugins/Web/Tavily/TavilyTextSearchTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
#pragma warning disable CS0618 // ITextSearch is obsolete
4+
35
using System.Threading.Tasks;
46
using Microsoft.Extensions.Configuration;
57
using Microsoft.SemanticKernel.Data;

0 commit comments

Comments
 (0)