Commit ebd579d
authored
# .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 <[email protected]>
1 parent b94f3f3 commit ebd579d
File tree
30 files changed
+312
-4
lines changed- dotnet
- samples/GettingStartedWithTextSearch
- src
- IntegrationTests
- Agents/CommonInterfaceConformance/AgentWithTextSearchProviderConformance
- Connectors/Memory
- AzureAISearch
- InMemory
- Qdrant
- Data
- Plugins/Web
- Bing
- Google
- Tavily
- Plugins
- Plugins.UnitTests/Web
- Bing
- Brave
- Google
- Tavily
- Plugins.Web
- Bing
- Brave
- Google
- Tavily
- SemanticKernel.Abstractions/Data/TextSearch
- SemanticKernel.AotTests/UnitTests/Search
- SemanticKernel.Core/Data
- TextSearchBehavior
- TextSearchStore
- TextSearch
- SemanticKernel.UnitTests/Data
30 files changed
+312
-4
lines changedLines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
2 | 5 | | |
3 | 6 | | |
4 | 7 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
44 | 45 | | |
| 46 | + | |
45 | 47 | | |
46 | 48 | | |
47 | 49 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
0 commit comments