diff --git a/dotnet/samples/Concepts/RAG/Bing_RagWithTextSearch.cs b/dotnet/samples/Concepts/RAG/Bing_RagWithTextSearch.cs index 585c29cb0258..421c8fbbbf46 100644 --- a/dotnet/samples/Concepts/RAG/Bing_RagWithTextSearch.cs +++ b/dotnet/samples/Concepts/RAG/Bing_RagWithTextSearch.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. + using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Data; using Microsoft.SemanticKernel.Plugins.Web.Bing; @@ -133,6 +134,7 @@ Include citations to and the date of the relevant information where it is refere )); } +#pragma warning disable CS0618 // Suppress obsolete warnings for legacy TextSearchOptions/TextSearchFilter usage /// /// Show how to create a default from an and use it to /// add grounding context to a Handlebars prompt that include full web pages. @@ -183,4 +185,5 @@ Include citations to the relevant information where it is referenced in the resp promptTemplateFactory: promptTemplateFactory )); } +#pragma warning restore CS0618 } diff --git a/dotnet/samples/Concepts/Search/Bing_FunctionCallingWithTextSearch.cs b/dotnet/samples/Concepts/Search/Bing_FunctionCallingWithTextSearch.cs index 0245eb80757e..c6987b368ea0 100644 --- a/dotnet/samples/Concepts/Search/Bing_FunctionCallingWithTextSearch.cs +++ b/dotnet/samples/Concepts/Search/Bing_FunctionCallingWithTextSearch.cs @@ -66,6 +66,7 @@ public async Task FunctionCallingWithBingTextSearchIncludingCitationsAsync() Console.WriteLine(await kernel.InvokePromptAsync("What is the Semantic Kernel? Include citations to the relevant information where it is referenced in the response.", arguments)); } +#pragma warning disable CS0618 // Suppress obsolete warnings for legacy TextSearchOptions/TextSearchFilter usage /// /// Show how to create a default from an and use it with /// function calling to have the LLM include grounding context from the Microsoft Dev Blogs site in it's response. @@ -143,4 +144,5 @@ private static KernelFunction CreateSearchBySite(BingTextSearch textSearch, Text return textSearch.CreateSearch(options); } +#pragma warning restore CS0618 } diff --git a/dotnet/samples/Concepts/Search/Bing_TextSearch.cs b/dotnet/samples/Concepts/Search/Bing_TextSearch.cs index b862360d740c..0a450cabbe3a 100644 --- a/dotnet/samples/Concepts/Search/Bing_TextSearch.cs +++ b/dotnet/samples/Concepts/Search/Bing_TextSearch.cs @@ -11,6 +11,7 @@ namespace Search; /// public class Bing_TextSearch(ITestOutputHelper output) : BaseTest(output) { +#pragma warning disable CS0618 // Suppress obsolete warnings for legacy TextSearchOptions/TextSearchFilter usage /// /// Show how to create a and use it to perform a text search. /// @@ -118,6 +119,7 @@ public async Task UsingBingTextSearchWithASiteFilterAsync() WriteHorizontalRule(); } } +#pragma warning restore CS0618 /// /// Show how to use enhanced LINQ filtering with BingTextSearch for type-safe searches. diff --git a/dotnet/samples/Concepts/Search/Google_TextSearch.cs b/dotnet/samples/Concepts/Search/Google_TextSearch.cs index 7d8c59478d7b..9a98af3c4544 100644 --- a/dotnet/samples/Concepts/Search/Google_TextSearch.cs +++ b/dotnet/samples/Concepts/Search/Google_TextSearch.cs @@ -12,6 +12,7 @@ namespace Search; /// public class Google_TextSearch(ITestOutputHelper output) : BaseTest(output) { +#pragma warning disable CS0618 // Suppress obsolete warnings for legacy TextSearchOptions/TextSearchFilter usage /// /// Show how to create a and use it to perform a text search. /// @@ -106,6 +107,7 @@ public async Task UsingGoogleTextSearchWithASiteSearchFilterAsync() Console.WriteLine(new string('-', HorizontalRuleLength)); } } +#pragma warning restore CS0618 /// /// Show how to use enhanced LINQ filtering with GoogleTextSearch including Contains, NOT, FileType, and compound AND expressions. diff --git a/dotnet/samples/Concepts/Search/Tavily_TextSearch.cs b/dotnet/samples/Concepts/Search/Tavily_TextSearch.cs index 82161b28dd63..3134690cabd0 100644 --- a/dotnet/samples/Concepts/Search/Tavily_TextSearch.cs +++ b/dotnet/samples/Concepts/Search/Tavily_TextSearch.cs @@ -11,6 +11,7 @@ namespace Search; /// public class Tavily_TextSearch(ITestOutputHelper output) : BaseTest(output) { +#pragma warning disable CS0618 // Suppress obsolete warnings for legacy TextSearchOptions/TextSearchFilter usage /// /// Show how to create a and use it to perform a text search. /// @@ -181,6 +182,7 @@ public async Task UsingTavilyTextSearchWithAnIncludeDomainFilterAsync() WriteHorizontalRule(); } } +#pragma warning restore CS0618 /// /// Show how to use enhanced LINQ filtering with TavilyTextSearch for type-safe searches with Title.Contains() support. diff --git a/dotnet/samples/GettingStartedWithTextSearch/Step3_Search_With_FunctionCalling.cs b/dotnet/samples/GettingStartedWithTextSearch/Step3_Search_With_FunctionCalling.cs index 5b7766b589d3..56cff911a80e 100644 --- a/dotnet/samples/GettingStartedWithTextSearch/Step3_Search_With_FunctionCalling.cs +++ b/dotnet/samples/GettingStartedWithTextSearch/Step3_Search_With_FunctionCalling.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. + using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; @@ -69,6 +70,7 @@ public async Task FunctionCallingWithBingTextSearchIncludingCitationsAsync() Console.WriteLine(await kernel.InvokePromptAsync("What is the Semantic Kernel? Include citations to the relevant information where it is referenced in the response.", arguments)); } +#pragma warning disable CS0618 // Suppress obsolete warnings for legacy TextSearchOptions/TextSearchFilter usage /// /// Show how to create a default from an and use it with /// function calling to have the LLM include grounding context from the Microsoft Dev Blogs site in it's response. @@ -159,5 +161,6 @@ private static KernelFunction CreateSearchBySite(BingTextSearch textSearch, Text return textSearch.CreateSearch(options); } +#pragma warning restore CS0618 #endregion } diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentWithTextSearchProviderConformance/AgentWithTextSearchProvider.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentWithTextSearchProviderConformance/AgentWithTextSearchProvider.cs index 89e0a1790648..9dfc543e2635 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentWithTextSearchProviderConformance/AgentWithTextSearchProvider.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentWithTextSearchProviderConformance/AgentWithTextSearchProvider.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable CS0618 // Obsolete TextSearchOptions/TextSearchFilter + using System; using System.Linq; using System.Threading; @@ -41,9 +43,7 @@ public abstract class AgentWithTextSearchProvider(Func creat public async Task TextSearchBehaviorStateIsUsedByAgentInternalAsync(string question, string expectedResult, params string[] ragResults) { // Arrange -#pragma warning disable CS0618 // ITextSearch is obsolete - Testing legacy interface var mockTextSearch = new Mock(); -#pragma warning restore CS0618 mockTextSearch.Setup(x => x.GetTextSearchResultsAsync( It.IsAny(), It.IsAny(), diff --git a/dotnet/src/Plugins/Plugins.Web/Bing/BingTextSearch.cs b/dotnet/src/Plugins/Plugins.Web/Bing/BingTextSearch.cs index a0fd5d1f1bee..843e24354fa6 100644 --- a/dotnet/src/Plugins/Plugins.Web/Bing/BingTextSearch.cs +++ b/dotnet/src/Plugins/Plugins.Web/Bing/BingTextSearch.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CS0618 // Non-generic ITextSearch is obsolete - provides backward compatibility during Phase 2 LINQ migration - using System; using System.Collections.Generic; using System.Linq; @@ -23,7 +21,7 @@ namespace Microsoft.SemanticKernel.Plugins.Web.Bing; /// /// A Bing Text Search implementation that can be used to perform searches using the Bing Web Search API. /// -#pragma warning disable CS0618 // ITextSearch is obsolete - this class provides backward compatibility +#pragma warning disable CS0618 // ITextSearch is obsolete public sealed class BingTextSearch : ITextSearch, ITextSearch #pragma warning restore CS0618 { @@ -47,6 +45,10 @@ public BingTextSearch(string apiKey, BingTextSearchOptions? options = null) this._options = options; } +#pragma warning disable CS0618 // Obsolete ITextSearch, TextSearchOptions, TextSearchFilter, FilterClause + + #region Legacy ITextSearch Implementation + /// public async Task> SearchAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default) { @@ -83,6 +85,10 @@ public async Task> GetSearchResultsAsync(string quer return new KernelSearchResults(this.GetResultsAsWebPageAsync(searchResponse, cancellationToken), totalCount, GetResultsMetadata(searchResponse)); } + #endregion + +#pragma warning restore CS0618 + /// async Task> ITextSearch.SearchAsync(string query, TextSearchOptions? searchOptions, CancellationToken cancellationToken) { @@ -172,67 +178,48 @@ private static void ProcessExpression(Expression expression, List<(string FieldN { switch (expression) { - case BinaryExpression binaryExpr when binaryExpr.NodeType == ExpressionType.AndAlso: + case BinaryExpression { NodeType: ExpressionType.AndAlso } andExpr: // Handle AND: page => page.Language == "en" && page.Name.Contains("AI") - ProcessExpression(binaryExpr.Left, filters); - ProcessExpression(binaryExpr.Right, filters); + ProcessExpression(andExpr.Left, filters); + ProcessExpression(andExpr.Right, filters); break; - case BinaryExpression binaryExpr when binaryExpr.NodeType == ExpressionType.OrElse: + case BinaryExpression { NodeType: ExpressionType.OrElse }: // Handle OR: Currently not directly supported by TextSearchFilter // Bing API supports OR via multiple queries, but TextSearchFilter doesn't expose this throw new NotSupportedException( "Logical OR (||) is not supported by Bing Text Search filters. " + "Consider splitting into multiple search queries."); - case UnaryExpression unaryExpr when unaryExpr.NodeType == ExpressionType.Not: + case UnaryExpression { NodeType: ExpressionType.Not }: // Handle NOT: page => !page.Language.Equals("en") throw new NotSupportedException( "Logical NOT (!) is not directly supported by Bing Text Search advanced operators. " + "Consider restructuring your filter to use positive conditions."); - case BinaryExpression binaryExpr when binaryExpr.NodeType == ExpressionType.Equal: + case BinaryExpression { NodeType: ExpressionType.Equal } binaryExpr: // Handle equality: page => page.Language == "en" ProcessEqualityExpression(binaryExpr, filters, isNegated: false); break; - case BinaryExpression binaryExpr when binaryExpr.NodeType == ExpressionType.NotEqual: + case BinaryExpression { NodeType: ExpressionType.NotEqual } binaryExpr: // Handle inequality: page => page.Language != "en" // Implemented via Bing's negation syntax (e.g., -language:en) ProcessEqualityExpression(binaryExpr, filters, isNegated: true); break; - case MethodCallExpression methodExpr when methodExpr.Method.Name == "Contains": - // Distinguish between instance method (String.Contains) and static method (Enumerable/MemoryExtensions.Contains) - if (methodExpr.Object is MemberExpression) - { - // Instance method: page.Name.Contains("value") - SUPPORTED - ProcessContainsExpression(methodExpr, filters); - } - else if (methodExpr.Object == null) - { - // Static method: could be Enumerable.Contains (C# 13-) or MemoryExtensions.Contains (C# 14+) - // Bing API doesn't support OR logic, so collection Contains patterns are not supported - if (methodExpr.Method.DeclaringType == typeof(Enumerable) || - (methodExpr.Method.DeclaringType == typeof(MemoryExtensions) && IsMemoryExtensionsContains(methodExpr))) - { - throw new NotSupportedException( - "Collection Contains filters (e.g., array.Contains(page.Property)) are not supported by Bing Search API. " + - "Bing's advanced search operators do not support OR logic across multiple values. " + - "Supported pattern: Property.Contains(\"value\") for string properties like Name, Snippet, or Url. " + - "For multiple value matching, consider alternative approaches or use a different search provider."); - } - - throw new NotSupportedException( - $"Contains() method from {methodExpr.Method.DeclaringType?.Name} is not supported."); - } - else - { - throw new NotSupportedException( - "Contains() must be called on a property (e.g., page.Name.Contains(\"value\"))."); - } + case MethodCallExpression { Method.Name: "Contains", Object: MemberExpression } methodExpr: + // Instance method: page.Name.Contains("value") - SUPPORTED + ProcessContainsExpression(methodExpr, filters); break; + case MethodCallExpression { Method.Name: "Contains", Object: null }: + // Static method: collection.Contains(page.Property) - NOT SUPPORTED + throw new NotSupportedException( + "Collection Contains filters (e.g., collection.Contains(page.Property)) are not supported by Bing Search API. " + + "Bing API doesn't support OR logic across multiple values for a single field. " + + "Consider using multiple separate queries instead."); + default: throw new NotSupportedException( $"Expression type '{expression.NodeType}' is not supported for Bing API filters. " + @@ -360,32 +347,13 @@ private static void ProcessContainsExpression(MethodCallExpression methodExpr, L } } - /// - /// Determines if a MethodCallExpression is a MemoryExtensions.Contains call (C# 14 "first-class spans" feature). - /// - /// The method call expression to check. - /// True if this is a MemoryExtensions.Contains call with supported parameters; otherwise false. - private static bool IsMemoryExtensionsContains(MethodCallExpression methodExpr) - { - // MemoryExtensions.Contains has 2-3 parameters: - // - Contains(ReadOnlySpan span, T value) - // - Contains(ReadOnlySpan span, T value, IEqualityComparer? comparer) - // We only support when comparer is null or omitted - return methodExpr.Method.Name == nameof(MemoryExtensions.Contains) && - methodExpr.Arguments.Count >= 2 && - methodExpr.Arguments.Count <= 3 && - (methodExpr.Arguments.Count == 2 || - (methodExpr.Arguments.Count == 3 && methodExpr.Arguments[2] is ConstantExpression { Value: null })); - } - /// /// Maps BingWebPage property names to Bing API filter field names for equality operations. /// /// The BingWebPage property name. /// The corresponding Bing API filter name, or null if not mappable. - private static string? MapPropertyToBingFilter(string propertyName) - { - return propertyName.ToUpperInvariant() switch + private static string? MapPropertyToBingFilter(string propertyName) => + propertyName.ToUpperInvariant() switch { // Map BingWebPage properties to Bing API equivalents "LANGUAGE" => "language", // Maps to advanced search @@ -401,16 +369,14 @@ private static bool IsMemoryExtensionsContains(MethodCallExpression methodExpr) _ => null // Property not mappable to Bing filters }; - } /// /// Maps BingWebPage property names to Bing API advanced search operators for Contains operations. /// /// The BingWebPage property name. /// The corresponding Bing advanced search operator, or null if not mappable. - private static string? MapPropertyToContainsFilter(string propertyName) - { - return propertyName.ToUpperInvariant() switch + private static string? MapPropertyToContainsFilter(string propertyName) => + propertyName.ToUpperInvariant() switch { // Map properties to Bing's contains-style operators "NAME" => "intitle", // intitle:"search term" - title contains @@ -420,7 +386,6 @@ private static bool IsMemoryExtensionsContains(MethodCallExpression methodExpr) _ => null // Property not mappable to Contains-style filters }; - } /// /// Execute a Bing search query and return the results. @@ -594,12 +559,12 @@ public TextSearchResult MapFromResultToTextSearchResult(object result) } } -#pragma warning disable CS0618 // FilterClause is obsolete - backward compatibility shim for legacy ITextSearch /// /// Extracts filter key-value pairs from a legacy . /// This shim converts the obsolete FilterClause-based format to the internal (FieldName, Value) list. /// It will be removed when the legacy ITextSearch interface is retired. /// +#pragma warning disable CS0618 // Obsolete TextSearchFilter, FilterClause private static List<(string FieldName, object Value)> ExtractFiltersFromLegacy(TextSearchFilter? filter) { var filters = new List<(string FieldName, object Value)>(); @@ -611,6 +576,11 @@ public TextSearchResult MapFromResultToTextSearchResult(object result) { filters.Add((eq.FieldName, eq.Value)); } + else + { + throw new NotSupportedException( + $"Filter clause type '{clause.GetType().Name}' is not supported by Bing Text Search. Only EqualToFilterClause is supported."); + } } } return filters; diff --git a/dotnet/src/Plugins/Plugins.Web/Brave/BraveTextSearch.cs b/dotnet/src/Plugins/Plugins.Web/Brave/BraveTextSearch.cs index 6417ecffd394..1ecdd89197b8 100644 --- a/dotnet/src/Plugins/Plugins.Web/Brave/BraveTextSearch.cs +++ b/dotnet/src/Plugins/Plugins.Web/Brave/BraveTextSearch.cs @@ -21,7 +21,7 @@ namespace Microsoft.SemanticKernel.Plugins.Web.Brave; /// /// A Brave Text Search implementation that can be used to perform searches using the Brave Web Search API. /// -#pragma warning disable CS0618 // ITextSearch is obsolete - this class provides backward compatibility +#pragma warning disable CS0618 // ITextSearch is obsolete public sealed class BraveTextSearch : ITextSearch, ITextSearch #pragma warning restore CS0618 { @@ -47,6 +47,10 @@ public BraveTextSearch(string apiKey, BraveTextSearchOptions? options = null) this._resultMapper = options?.ResultMapper ?? s_defaultResultMapper; } +#pragma warning disable CS0618 // Obsolete ITextSearch, TextSearchOptions, TextSearchFilter, FilterClause + + #region Legacy ITextSearch Implementation + /// public async Task> SearchAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = new CancellationToken()) { @@ -84,6 +88,10 @@ public async Task> GetSearchResultsAsync(string quer return new KernelSearchResults(this.GetResultsAsObjectAsync(searchResponse, cancellationToken), totalCount, GetResultsMetadata(searchResponse)); } + #endregion + +#pragma warning restore CS0618 + #region Generic ITextSearch Implementation /// @@ -163,28 +171,24 @@ private static void ExtractFiltersFromExpression(Expression expression, List<(st { switch (expression) { - case BinaryExpression binaryExpr: - if (binaryExpr.NodeType is ExpressionType.AndAlso or ExpressionType.OrElse) - { - // Handle AND/OR expressions by recursively analyzing both sides - ExtractFiltersFromExpression(binaryExpr.Left, filters, queryTerms); - ExtractFiltersFromExpression(binaryExpr.Right, filters, queryTerms); - } - else if (binaryExpr.NodeType == ExpressionType.Equal) - { - ProcessEqualityClause(binaryExpr, filters); - } - else if (binaryExpr.NodeType == ExpressionType.NotEqual) - { - ProcessInequalityClause(binaryExpr); - } - else - { - throw new NotSupportedException($"Binary expression type '{binaryExpr.NodeType}' is not supported. Supported operators: AndAlso (&&), OrElse (||), Equal (==), NotEqual (!=)."); - } + case BinaryExpression { NodeType: ExpressionType.AndAlso or ExpressionType.OrElse } binaryExpr: + // Handle AND/OR expressions by recursively analyzing both sides + ExtractFiltersFromExpression(binaryExpr.Left, filters, queryTerms); + ExtractFiltersFromExpression(binaryExpr.Right, filters, queryTerms); + break; + + case BinaryExpression { NodeType: ExpressionType.Equal } binaryExpr: + ProcessEqualityClause(binaryExpr, filters); + break; + + case BinaryExpression { NodeType: ExpressionType.NotEqual } binaryExpr: + ProcessInequalityClause(binaryExpr); break; - case UnaryExpression unaryExpr when unaryExpr.NodeType == ExpressionType.Not: + case BinaryExpression binaryExpr: + throw new NotSupportedException($"Binary expression type '{binaryExpr.NodeType}' is not supported. Supported operators: AndAlso (&&), OrElse (||), Equal (==), NotEqual (!=)."); + + case UnaryExpression { NodeType: ExpressionType.Not } unaryExpr: ProcessNotExpression(unaryExpr); break; @@ -308,29 +312,12 @@ private static void ProcessMethodCallClause(MethodCallExpression methodCall, Lis } } } - else if (methodCall.Object == null && methodCall.Arguments.Count == 2) - { - // Static method: array.Contains(property) - NOT supported - string errorMessage = "Collection Contains filters (e.g., array.Contains(page.Property)) are not supported by Brave Search API. " + - "Brave's API does not support OR logic across multiple values. "; - - if (IsMemoryExtensionsContains(methodCall)) - { - errorMessage += "Note: This occurs when using C# 14+ language features with span-based Contains methods (MemoryExtensions.Contains). "; - } - else - { - errorMessage += "Note: This occurs with standard LINQ extension methods (Enumerable.Contains). "; - } - - errorMessage += "Consider either: (1) performing multiple separate searches for each value, or " + - "(2) retrieving broader results and filtering on the client side."; - - throw new NotSupportedException(errorMessage); - } else { - throw new NotSupportedException("Unsupported Contains expression format."); + throw new NotSupportedException( + "Collection Contains filters (e.g., array.Contains(page.Property)) are not supported by Brave Search API. " + + "Consider either: (1) performing multiple separate searches for each value, or " + + "(2) retrieving broader results and filtering on the client side."); } } else @@ -642,12 +629,12 @@ public TextSearchResult MapFromResultToTextSearchResult(object result) } } -#pragma warning disable CS0618 // FilterClause is obsolete - backward compatibility shim for legacy ITextSearch /// /// Extracts filter key-value pairs from a legacy . /// This shim converts the obsolete FilterClause-based format to the internal (FieldName, Value) list. /// It will be removed when the legacy ITextSearch interface is retired. /// +#pragma warning disable CS0618 // Obsolete TextSearchFilter, FilterClause private static List<(string FieldName, object Value)> ExtractFiltersFromLegacy(TextSearchFilter? filter) { var filters = new List<(string FieldName, object Value)>(); @@ -659,6 +646,11 @@ public TextSearchResult MapFromResultToTextSearchResult(object result) { filters.Add((eq.FieldName, eq.Value)); } + else + { + throw new NotSupportedException( + $"Filter clause type '{clause.GetType().Name}' is not supported by Brave Text Search. Only EqualToFilterClause is supported."); + } } } return filters; @@ -760,41 +752,5 @@ private static void CheckQueryValidation(string queryParam, object value) } } - /// - /// Determines if a method call expression is a MemoryExtensions.Contains call (C# 14+ compatibility). - /// In C# 14+, array.Contains(property) may resolve to MemoryExtensions.Contains instead of Enumerable.Contains. - /// - /// The method call expression to check. - /// True if this is a MemoryExtensions.Contains call, false otherwise. - private static bool IsMemoryExtensionsContains(MethodCallExpression methodCall) - { - // Check if this is a static method call (Object is null) - if (methodCall.Object != null) - { - return false; - } - - // Check if it's MemoryExtensions.Contains - if (methodCall.Method.DeclaringType?.Name != "MemoryExtensions") - { - return false; - } - - // MemoryExtensions.Contains has 2-3 parameters: (ReadOnlySpan, T) or (ReadOnlySpan, T, IEqualityComparer) - if (methodCall.Arguments.Count < 2 || methodCall.Arguments.Count > 3) - { - return false; - } - - // For our text search scenarios, we don't support span comparers - if (methodCall.Arguments.Count == 3) - { - throw new NotSupportedException( - "MemoryExtensions.Contains with custom IEqualityComparer is not supported. " + - "Use simple array.Contains(property) expressions without custom comparers."); - } - - return true; - } #endregion } diff --git a/dotnet/src/Plugins/Plugins.Web/Google/GoogleTextSearch.cs b/dotnet/src/Plugins/Plugins.Web/Google/GoogleTextSearch.cs index 265675dfd839..50ac722fcc5f 100644 --- a/dotnet/src/Plugins/Plugins.Web/Google/GoogleTextSearch.cs +++ b/dotnet/src/Plugins/Plugins.Web/Google/GoogleTextSearch.cs @@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Plugins.Web.Google; /// /// A Google Text Search implementation that can be used to perform searches using the Google Web Search API. /// -#pragma warning disable CS0618 // ITextSearch is obsolete - this class provides backward compatibility +#pragma warning disable CS0618 // ITextSearch is obsolete public sealed class GoogleTextSearch : ITextSearch, ITextSearch, IDisposable #pragma warning restore CS0618 { @@ -59,6 +59,10 @@ public GoogleTextSearch( this._options = options; } +#pragma warning disable CS0618 // Obsolete ITextSearch, TextSearchOptions, TextSearchFilter, FilterClause + + #region Legacy ITextSearch Implementation + /// public async Task> GetSearchResultsAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default) { @@ -95,6 +99,10 @@ public async Task> SearchAsync(string query, TextSea return new KernelSearchResults(this.GetResultsAsStringAsync(searchResponse, cancellationToken), totalCount, GetResultsMetadata(searchResponse)); } + #endregion + +#pragma warning restore CS0618 + #region ITextSearch Implementation /// @@ -153,214 +161,120 @@ private static (int Top, int Skip, bool IncludeTotalCount, List<(string FieldNam /// Thrown when the expression cannot be converted to Google filters. private static List<(string FieldName, object Value)> ExtractFiltersFromExpression(Expression> linqExpression) { - // Handle compound AND expressions: expr1 && expr2 - if (linqExpression.Body is BinaryExpression andExpr && andExpr.NodeType == ExpressionType.AndAlso) - { - var filters = new List<(string FieldName, object Value)>(); - CollectAndCombineFilters(andExpr, filters); - return filters; - } - - // Handle simple expressions using the shared processing logic - var result = new List<(string FieldName, object Value)>(); - if (TryProcessSingleExpression(linqExpression.Body, result)) - { - return result; - } - - // Generate helpful error message with supported patterns - var supportedProperties = s_queryParameters.Select(p => - MapGoogleFilterToProperty(p)).Where(p => p != null).Distinct(); - - throw new NotSupportedException( - $"LINQ expression '{linqExpression}' cannot be converted to Google API filters. " + - $"Supported patterns: {string.Join(", ", s_supportedPatterns)}. " + - $"Supported properties: {string.Join(", ", supportedProperties)}."); + var filters = new List<(string FieldName, object Value)>(); + ProcessFilterNode(linqExpression.Body, filters); + return filters; } /// - /// Recursively collects and combines filters from compound AND expressions. + /// Recursively processes a LINQ expression tree node and builds Google API filters. + /// Handles AND combinations, equality, inequality, string Contains, and negation patterns. /// - /// The expression to process. + /// The expression node to process. /// The filter list to accumulate results into. - private static void CollectAndCombineFilters(Expression expression, List<(string FieldName, object Value)> filters) - { - if (expression is BinaryExpression binaryExpr && binaryExpr.NodeType == ExpressionType.AndAlso) - { - // Recursively process both sides of the AND - CollectAndCombineFilters(binaryExpr.Left, filters); - CollectAndCombineFilters(binaryExpr.Right, filters); - } - else - { - // Process individual expression using shared logic - TryProcessSingleExpression(expression, filters); - } - } - - /// - /// Shared logic to process a single LINQ expression and add appropriate filters. - /// Consolidates duplicate code between ExtractFiltersFromExpression and CollectAndCombineFilters. - /// - /// The expression to process. - /// The filter list to add results to. - /// True if the expression was successfully processed, false otherwise. - private static bool TryProcessSingleExpression(Expression expression, List<(string FieldName, object Value)> filters) + /// Thrown when the expression cannot be converted to Google filters. + private static void ProcessFilterNode(Expression expression, List<(string FieldName, object Value)> filters) { - // Handle equality: record.PropertyName == "value" - if (expression is BinaryExpression equalExpr && equalExpr.NodeType == ExpressionType.Equal) + switch (expression) { - return TryProcessEqualityExpression(equalExpr, filters); - } + case BinaryExpression { NodeType: ExpressionType.AndAlso } andExpr: + ProcessFilterNode(andExpr.Left, filters); + ProcessFilterNode(andExpr.Right, filters); + break; - // Handle inequality (NOT): record.PropertyName != "value" - if (expression is BinaryExpression notEqualExpr && notEqualExpr.NodeType == ExpressionType.NotEqual) - { - return TryProcessInequalityExpression(notEqualExpr, filters); - } + case BinaryExpression { NodeType: ExpressionType.Equal, Left: MemberExpression member, Right: ConstantExpression constant } + when constant.Value is not null: + { + var filterName = MapPropertyToGoogleFilter(member.Member.Name) + ?? throw new NotSupportedException( + $"Property '{member.Member.Name}' is not supported for Google API equality filters. " + + $"Supported patterns: {string.Join(", ", s_supportedPatterns)}."); + filters.Add((filterName, constant.Value)); + break; + } - // Handle Contains method calls - if (expression is MethodCallExpression methodCall && methodCall.Method.Name == "Contains") - { - // String.Contains (instance method) - supported for substring search - if (methodCall.Method.DeclaringType == typeof(string)) + case BinaryExpression { NodeType: ExpressionType.NotEqual, Left: MemberExpression member, Right: ConstantExpression constant } + when constant.Value is not null: { - return TryProcessContainsExpression(methodCall, filters); + _ = MapPropertyToGoogleFilter(member.Member.Name) + ?? throw new NotSupportedException( + $"Property '{member.Member.Name}' is not supported for Google API inequality filters. " + + $"Supported patterns: {string.Join(", ", s_supportedPatterns)}."); + filters.Add(("excludeTerms", constant.Value)); + break; } - // Collection Contains (static methods) - NOT supported due to Google API limitations - // This handles both Enumerable.Contains (C# 13-) and MemoryExtensions.Contains (C# 14+) - // User's C# language version determines which method is resolved, but both are unsupported - if (methodCall.Object == null) // Static method + case MethodCallExpression { Method.Name: "Contains", Method.DeclaringType: Type dt, Object: MemberExpression member } methodCall + when dt == typeof(string) + && methodCall.Arguments is [ConstantExpression { Value: not null } argExpr]: { - // Enumerable.Contains or MemoryExtensions.Contains - if (methodCall.Method.DeclaringType == typeof(Enumerable) || - (methodCall.Method.DeclaringType == typeof(MemoryExtensions) && IsMemoryExtensionsContains(methodCall))) - { - throw new NotSupportedException( - "Collection Contains filters (e.g., array.Contains(page.Property)) are not supported by Google Custom Search API. " + - "Google's search operators do not support OR logic across multiple values. " + - "Consider either: (1) performing multiple separate searches for each value, or " + - "(2) retrieving broader results and filtering on the client side."); - } + var filterName = MapPropertyToGoogleFilter(member.Member.Name) + ?? throw new NotSupportedException( + $"Property '{member.Member.Name}' is not supported for Google API Contains filters. " + + $"Supported patterns: {string.Join(", ", s_supportedPatterns)}."); + // For Contains operations on text fields, use orTerms (more flexible than exactTerms) + filters.Add((filterName == "exactTerms" ? "orTerms" : filterName, argExpr.Value)); + break; } - } - // Handle NOT expressions: !record.PropertyName.Contains("value") - if (expression is UnaryExpression unaryExpr && unaryExpr.NodeType == ExpressionType.Not) - { - return TryProcessNotExpression(unaryExpr, filters); - } + // Null-guard patterns (page.Property != null, page.Property == null) — silently skip + case BinaryExpression { NodeType: ExpressionType.Equal or ExpressionType.NotEqual, Right: ConstantExpression { Value: null } }: + break; - return false; - } + case MethodCallExpression { Method.Name: "Contains", Object: null }: + throw new NotSupportedException( + "Collection Contains filters (e.g., collection.Contains(page.Property)) are not supported by Google Custom Search API. " + + "Google API doesn't support OR logic across multiple values for a single field. " + + "Consider using multiple separate queries instead."); - /// - /// Checks if a method call expression is MemoryExtensions.Contains. - /// This handles C# 14's "first-class spans" feature where collection.Contains(item) resolves to - /// MemoryExtensions.Contains instead of Enumerable.Contains. - /// - private static bool IsMemoryExtensionsContains(MethodCallExpression methodExpr) - { - // MemoryExtensions.Contains has 2-3 parameters (source, value, optional comparer) - // We only support the case without a comparer (or with null comparer) - return methodExpr.Method.Name == nameof(MemoryExtensions.Contains) && - methodExpr.Arguments.Count >= 2 && - methodExpr.Arguments.Count <= 3 && - (methodExpr.Arguments.Count == 2 || - (methodExpr.Arguments.Count == 3 && methodExpr.Arguments[2] is ConstantExpression { Value: null })); - } + case UnaryExpression { NodeType: ExpressionType.Not } unaryExpr: + ProcessNegatedFilterNode(unaryExpr.Operand, filters); + break; - /// - /// Processes equality expressions: record.PropertyName == "value" - /// - private static bool TryProcessEqualityExpression(BinaryExpression equalExpr, List<(string FieldName, object Value)> filters) - { - if (equalExpr.Left is MemberExpression memberExpr && equalExpr.Right is ConstantExpression constExpr) - { - string propertyName = memberExpr.Member.Name; - object? value = constExpr.Value; - string? googleFilterName = MapPropertyToGoogleFilter(propertyName); - if (googleFilterName != null && value != null) - { - filters.Add((googleFilterName, value)); - return true; - } + default: + throw new NotSupportedException( + $"Expression type '{expression.NodeType}' is not supported for Google API filters. " + + $"Supported patterns: {string.Join(", ", s_supportedPatterns)}."); } - return false; } /// - /// Processes inequality expressions: record.PropertyName != "value" + /// Processes negated expressions (! operator), mapping them to Google's excludeTerms parameter. + /// Handles both !(property == value) and !property.Contains(value) patterns. /// - private static bool TryProcessInequalityExpression(BinaryExpression notEqualExpr, List<(string FieldName, object Value)> filters) + /// The inner expression of the NOT operation. + /// The filter list to accumulate results into. + /// Thrown when the negated expression cannot be converted to Google filters. + private static void ProcessNegatedFilterNode(Expression expression, List<(string FieldName, object Value)> filters) { - if (notEqualExpr.Left is MemberExpression memberExpr && notEqualExpr.Right is ConstantExpression constExpr) + switch (expression) { - string propertyName = memberExpr.Member.Name; - object? value = constExpr.Value; - // Map to excludeTerms for text fields - if (propertyName.ToUpperInvariant() is "TITLE" or "SNIPPET" && value != null) + case BinaryExpression { NodeType: ExpressionType.Equal, Left: MemberExpression member, Right: ConstantExpression constant } + when constant.Value is not null: { - filters.Add(("excludeTerms", value)); - return true; + _ = MapPropertyToGoogleFilter(member.Member.Name) + ?? throw new NotSupportedException( + $"Property '{member.Member.Name}' is not supported for Google API negated equality filters."); + filters.Add(("excludeTerms", constant.Value)); + break; } - } - return false; - } - /// - /// Processes Contains expressions: record.PropertyName.Contains("value") - /// - private static bool TryProcessContainsExpression(MethodCallExpression methodCall, List<(string FieldName, object Value)> filters) - { - if (methodCall.Object is MemberExpression memberExpr && - methodCall.Arguments.Count == 1 && - methodCall.Arguments[0] is ConstantExpression constExpr) - { - string propertyName = memberExpr.Member.Name; - object? value = constExpr.Value; - string? googleFilterName = MapPropertyToGoogleFilter(propertyName); - if (googleFilterName != null && value != null) + case MethodCallExpression { Method.Name: "Contains", Method.DeclaringType: Type dt, Object: MemberExpression member } methodCall + when dt == typeof(string) + && methodCall.Arguments is [ConstantExpression { Value: not null } argExpr]: { - // For Contains operations on text fields, use exactTerms or orTerms - if (googleFilterName == "exactTerms") - { - filters.Add(("orTerms", value)); // More flexible than exactTerms - } - else - { - filters.Add((googleFilterName, value)); - } - return true; + _ = MapPropertyToGoogleFilter(member.Member.Name) + ?? throw new NotSupportedException( + $"Property '{member.Member.Name}' is not supported for Google API negated Contains filters."); + filters.Add(("excludeTerms", argExpr.Value)); + break; } - } - return false; - } - /// - /// Processes NOT expressions: !record.PropertyName.Contains("value") - /// - private static bool TryProcessNotExpression(UnaryExpression unaryExpr, List<(string FieldName, object Value)> filters) - { - if (unaryExpr.Operand is MethodCallExpression notMethodCall && - notMethodCall.Method.Name == "Contains" && - notMethodCall.Method.DeclaringType == typeof(string)) - { - if (notMethodCall.Object is MemberExpression memberExpr && - notMethodCall.Arguments.Count == 1 && - notMethodCall.Arguments[0] is ConstantExpression constExpr) - { - string propertyName = memberExpr.Member.Name; - object? value = constExpr.Value; - if (propertyName.ToUpperInvariant() is "TITLE" or "SNIPPET" && value != null) - { - filters.Add(("excludeTerms", value)); - return true; - } - } + default: + throw new NotSupportedException( + $"Negated expression type '{expression.NodeType}' is not supported for Google API filters. " + + $"Supported patterns: {string.Join(", ", s_supportedPatterns)}."); } - return false; } /// @@ -368,9 +282,8 @@ private static bool TryProcessNotExpression(UnaryExpression unaryExpr, List<(str /// /// The GoogleWebPage property name. /// The corresponding Google API filter name, or null if not mappable. - private static string? MapPropertyToGoogleFilter(string propertyName) - { - return propertyName.ToUpperInvariant() switch + private static string? MapPropertyToGoogleFilter(string propertyName) => + propertyName.ToUpperInvariant() switch { // Map GoogleWebPage properties to Google API equivalents "LINK" => "siteSearch", // Maps to site search @@ -388,26 +301,6 @@ private static bool TryProcessNotExpression(UnaryExpression unaryExpr, List<(str _ => null // Property not mappable to Google filters }; - } - - /// - /// Maps Google Custom Search API filter field names back to example GoogleWebPage property names. - /// Used for generating helpful error messages. - /// - /// The Google API filter name. - /// An example property name, or null if not mappable. - private static string? MapGoogleFilterToProperty(string googleFilterName) - { - return googleFilterName switch - { - "siteSearch" => "DisplayLink", - "exactTerms" => "Title", - "orTerms" => "Title", - "excludeTerms" => "Title", - "fileType" => "FileFormat", - _ => null - }; - } #endregion @@ -492,12 +385,12 @@ public void Dispose() return await search.ExecuteAsync(cancellationToken).ConfigureAwait(false); } -#pragma warning disable CS0618 // FilterClause is obsolete - backward compatibility shim for legacy ITextSearch /// /// Extracts filter key-value pairs from a legacy . /// This shim converts the obsolete FilterClause-based format to the internal (FieldName, Value) list. /// It will be removed when the legacy ITextSearch interface is retired. /// +#pragma warning disable CS0618 // Obsolete TextSearchFilter, FilterClause private static List<(string FieldName, object Value)> ExtractFiltersFromLegacy(TextSearchFilter? filter) { var filters = new List<(string FieldName, object Value)>(); @@ -509,6 +402,11 @@ public void Dispose() { filters.Add((eq.FieldName, eq.Value)); } + else + { + throw new NotSupportedException( + $"Filter clause type '{clause.GetType().Name}' is not supported by Google Text Search. Only EqualToFilterClause is supported."); + } } } return filters; diff --git a/dotnet/src/Plugins/Plugins.Web/Tavily/TavilyTextSearch.cs b/dotnet/src/Plugins/Plugins.Web/Tavily/TavilyTextSearch.cs index bc187c426c54..41fd5cd35df1 100644 --- a/dotnet/src/Plugins/Plugins.Web/Tavily/TavilyTextSearch.cs +++ b/dotnet/src/Plugins/Plugins.Web/Tavily/TavilyTextSearch.cs @@ -21,7 +21,7 @@ namespace Microsoft.SemanticKernel.Plugins.Web.Tavily; /// /// A Tavily Text Search implementation that can be used to perform searches using the Tavily Web Search API. /// -#pragma warning disable CS0618 // ITextSearch is obsolete - this class provides backward compatibility +#pragma warning disable CS0618 // ITextSearch is obsolete public sealed class TavilyTextSearch : ITextSearch, ITextSearch #pragma warning restore CS0618 { @@ -45,6 +45,10 @@ public TavilyTextSearch(string apiKey, TavilyTextSearchOptions? options = null) this._resultMapper = options?.ResultMapper ?? s_defaultResultMapper; } +#pragma warning disable CS0618 // Obsolete ITextSearch, TextSearchOptions, TextSearchFilter, FilterClause + + #region Legacy ITextSearch Implementation + /// public async Task> SearchAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default) { @@ -81,6 +85,10 @@ public async Task> GetSearchResultsAsync(string quer return new KernelSearchResults(this.GetSearchResultsAsync(searchResponse, cancellationToken), totalCount, GetResultsMetadata(searchResponse)); } + #endregion + +#pragma warning restore CS0618 + #region Generic ITextSearch Implementation /// @@ -160,28 +168,24 @@ private static void ExtractFiltersFromExpression(Expression expression, List<(st { switch (expression) { - case BinaryExpression binaryExpr: - if (binaryExpr.NodeType is ExpressionType.AndAlso or ExpressionType.OrElse) - { - // Handle AND/OR expressions by recursively analyzing both sides - ExtractFiltersFromExpression(binaryExpr.Left, filters, queryTerms); - ExtractFiltersFromExpression(binaryExpr.Right, filters, queryTerms); - } - else if (binaryExpr.NodeType == ExpressionType.Equal) - { - ProcessEqualityClause(binaryExpr, filters); - } - else if (binaryExpr.NodeType == ExpressionType.NotEqual) - { - ProcessInequalityClause(binaryExpr); - } - else - { - throw new NotSupportedException($"Binary expression type '{binaryExpr.NodeType}' is not supported. Supported operators: AndAlso (&&), OrElse (||), Equal (==), NotEqual (!=)."); - } + case BinaryExpression { NodeType: ExpressionType.AndAlso or ExpressionType.OrElse } binaryExpr: + // Handle AND/OR expressions by recursively analyzing both sides + ExtractFiltersFromExpression(binaryExpr.Left, filters, queryTerms); + ExtractFiltersFromExpression(binaryExpr.Right, filters, queryTerms); + break; + + case BinaryExpression { NodeType: ExpressionType.Equal } binaryExpr: + ProcessEqualityClause(binaryExpr, filters); + break; + + case BinaryExpression { NodeType: ExpressionType.NotEqual } binaryExpr: + ProcessInequalityClause(binaryExpr); break; - case UnaryExpression unaryExpr when unaryExpr.NodeType == ExpressionType.Not: + case BinaryExpression binaryExpr: + throw new NotSupportedException($"Binary expression type '{binaryExpr.NodeType}' is not supported. Supported operators: AndAlso (&&), OrElse (||), Equal (==), NotEqual (!=)."); + + case UnaryExpression { NodeType: ExpressionType.Not } unaryExpr: ProcessNotExpression(unaryExpr); break; @@ -306,29 +310,12 @@ private static void ProcessMethodCallClause(MethodCallExpression methodCall, Lis } } } - else if (methodCall.Object == null && methodCall.Arguments.Count == 2) - { - // Static method: array.Contains(property) - NOT supported - string errorMessage = "Collection Contains filters (e.g., array.Contains(page.Property)) are not supported by Tavily Search API. " + - "Tavily's API does not support OR logic across multiple values. "; - - if (IsMemoryExtensionsContains(methodCall)) - { - errorMessage += "Note: This occurs when using C# 14+ language features with span-based Contains methods (MemoryExtensions.Contains). "; - } - else - { - errorMessage += "Note: This occurs with standard LINQ extension methods (Enumerable.Contains). "; - } - - errorMessage += "Consider either: (1) performing multiple separate searches for each value, or " + - "(2) retrieving broader results and filtering on the client side."; - - throw new NotSupportedException(errorMessage); - } else { - throw new NotSupportedException("Unsupported Contains expression format."); + throw new NotSupportedException( + "Collection Contains filters (e.g., array.Contains(page.Property)) are not supported by Tavily Search API. " + + "Consider either: (1) performing multiple separate searches for each value, or " + + "(2) retrieving broader results and filtering on the client side."); } } else @@ -648,12 +635,12 @@ public TextSearchResult MapFromResultToTextSearchResult(object result) } } -#pragma warning disable CS0618 // FilterClause is obsolete - backward compatibility shim for legacy ITextSearch /// /// Extracts filter key-value pairs from a legacy . /// This shim converts the obsolete FilterClause-based format to the internal (FieldName, Value) list. /// It will be removed when the legacy ITextSearch interface is retired. /// +#pragma warning disable CS0618 // Obsolete TextSearchFilter, FilterClause private static List<(string FieldName, object Value)> ExtractFiltersFromLegacy(TextSearchFilter? filter) { var filters = new List<(string FieldName, object Value)>(); @@ -665,6 +652,11 @@ public TextSearchResult MapFromResultToTextSearchResult(object result) { filters.Add((eq.FieldName, eq.Value)); } + else + { + throw new NotSupportedException( + $"Filter clause type '{clause.GetType().Name}' is not supported by Tavily Text Search. Only EqualToFilterClause is supported."); + } } } return filters; @@ -755,39 +747,5 @@ private TavilySearchRequest BuildRequestContent(string query, int top, int skip, return new(strPayload, Encoding.UTF8, "application/json"); } - /// - /// Determines if a method call expression is a MemoryExtensions.Contains call (C# 14+ compatibility). - /// In C# 14+, array.Contains(property) may resolve to MemoryExtensions.Contains instead of Enumerable.Contains. - /// - /// The method call expression to check. - /// True if this is a MemoryExtensions.Contains call, false otherwise. - private static bool IsMemoryExtensionsContains(MethodCallExpression methodCall) - { - // Check if this is a static method call (Object is null) - if (methodCall.Object != null) - { - return false; - } - - // Check if it's MemoryExtensions.Contains - if (methodCall.Method.DeclaringType?.Name != "MemoryExtensions") - { - return false; - } - - // MemoryExtensions.Contains has 2-3 parameters: (ReadOnlySpan, T) or (ReadOnlySpan, T, IEqualityComparer) - if (methodCall.Arguments.Count < 2 || methodCall.Arguments.Count > 3) - { - return false; - } // For our text search scenarios, we don't support span comparers - if (methodCall.Arguments.Count == 3) - { - throw new NotSupportedException( - "MemoryExtensions.Contains with custom IEqualityComparer is not supported. " + - "Use simple array.Contains(property) expressions without custom comparers."); - } - - return true; - } #endregion } diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchFilter.cs b/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchFilter.cs index d964fb1ecba1..872fa8ed7f2f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchFilter.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchFilter.cs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.VectorData; +#pragma warning disable CS0618 // FilterClause is obsolete - TextSearchFilter itself is obsolete and references FilterClause + namespace Microsoft.SemanticKernel.Data; /// @@ -15,6 +18,7 @@ namespace Microsoft.SemanticKernel.Data; /// service filter the search results. /// [Experimental("SKEXP0001")] +[Obsolete("Use LINQ expressions via TextSearchOptions.Filter instead. This type will be removed in a future version.")] public sealed class TextSearchFilter { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchOptions.cs b/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchOptions.cs index 9375d34abd0f..e33501a7a0b9 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchOptions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/TextSearch/TextSearchOptions.cs @@ -50,6 +50,8 @@ public sealed class TextSearchOptions /// /// Options which can be applied when using . /// +#pragma warning disable CS0618 // TextSearchFilter is obsolete - TextSearchOptions itself is obsolete +[Obsolete("Use TextSearchOptions with ITextSearch instead. This type will be removed in a future version.")] public sealed class TextSearchOptions { /// diff --git a/dotnet/src/SemanticKernel.AotTests/UnitTests/Search/MockTextSearch.cs b/dotnet/src/SemanticKernel.AotTests/UnitTests/Search/MockTextSearch.cs index 9ed0d43a87fa..098b24d7ebc0 100644 --- a/dotnet/src/SemanticKernel.AotTests/UnitTests/Search/MockTextSearch.cs +++ b/dotnet/src/SemanticKernel.AotTests/UnitTests/Search/MockTextSearch.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable CS0618 // Testing obsolete ITextSearch, TextSearchOptions backward compatibility + using Microsoft.SemanticKernel.Data; namespace SemanticKernel.AotTests.UnitTests.Search; -#pragma warning disable CS0618 // Type or member is obsolete internal sealed class MockTextSearch : ITextSearch -#pragma warning restore CS0618 // Type or member is obsolete { private readonly KernelSearchResults? _objectResults; private readonly KernelSearchResults? _textSearchResults; diff --git a/dotnet/src/SemanticKernel.Core/Data/TextSearch/TextSearchExtensions.cs b/dotnet/src/SemanticKernel.Core/Data/TextSearch/TextSearchExtensions.cs index c326b939dca2..fe91492b5aba 100644 --- a/dotnet/src/SemanticKernel.Core/Data/TextSearch/TextSearchExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/Data/TextSearch/TextSearchExtensions.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CS0618 // ITextSearch is obsolete - these extension methods provide backward compatibility +#pragma warning disable CS0618 // Obsolete ITextSearch, TextSearchOptions, TextSearchFilter, FilterClause - backward compatibility using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs b/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs index 86b0d963e84f..c83bdbcbddb9 100644 --- a/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs +++ b/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs @@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Data; /// A Vector Store Text Search implementation that can be used to perform searches using a . /// [Experimental("SKEXP0001")] -#pragma warning disable CS0618 // ITextSearch is obsolete - this class provides backward compatibility +#pragma warning disable CS0618 // ITextSearch is obsolete public sealed class VectorStoreTextSearch<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TRecord> : ITextSearch, ITextSearch #pragma warning restore CS0618 #pragma warning restore CA1711 // Identifiers should not have incorrect suffix @@ -59,12 +59,12 @@ public VectorStoreTextSearch( /// instance that can map a TRecord to a /// instance that can map a TRecord to a /// Options used to construct an instance of +#pragma warning disable CS0618 // Chains to obsolete ITextEmbeddingGenerationService constructor public VectorStoreTextSearch( IVectorSearchable vectorSearchable, IEmbeddingGenerator> embeddingGenerator, ITextSearchStringMapper? stringMapper = null, ITextSearchResultMapper? resultMapper = null, -#pragma warning disable CS0618 // Type or member is obsolete VectorStoreTextSearchOptions? options = null) : this( vectorSearchable, @@ -72,9 +72,11 @@ public VectorStoreTextSearch( stringMapper, resultMapper, options) -#pragma warning restore CS0618 // Type or member is obsolete { } +#pragma warning restore CS0618 + +#pragma warning disable CS0618 // Obsolete ITextEmbeddingGenerationService constructors /// /// Create an instance of the with the @@ -130,6 +132,8 @@ public VectorStoreTextSearch( this._resultMapper = resultMapper ?? this.CreateTextSearchResultMapper(); } +#pragma warning restore CS0618 + /// /// Create an instance of the with the /// provided for performing searches and @@ -175,6 +179,10 @@ public VectorStoreTextSearch( this._resultMapper = resultMapper ?? this.CreateTextSearchResultMapper(); } +#pragma warning disable CS0618 // Obsolete ITextSearch, TextSearchOptions + + #region Legacy ITextSearch Implementation + /// public Task> SearchAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default) { @@ -199,6 +207,10 @@ public Task> GetSearchResultsAsync(string query, Tex return Task.FromResult(new KernelSearchResults(this.GetResultsAsRecordAsync(searchResponse, cancellationToken))); } + #endregion + +#pragma warning restore CS0618 + /// Task> ITextSearch.SearchAsync(string query, TextSearchOptions? searchOptions, CancellationToken cancellationToken) { @@ -224,8 +236,10 @@ Task> ITextSearch.GetSearchResultsAsync(st } #region private +#pragma warning disable CS0618 // Obsolete ITextEmbeddingGenerationService [Obsolete("This property is obsolete.")] private readonly ITextEmbeddingGenerationService? _textEmbeddingGeneration; +#pragma warning restore CS0618 private readonly IVectorSearchable? _vectorSearchable; private readonly ITextSearchStringMapper _stringMapper; private readonly ITextSearchResultMapper _resultMapper; @@ -272,6 +286,7 @@ private TextSearchStringMapper CreateTextSearchStringMapper() }); } +#pragma warning disable CS0618 // Obsolete TextSearchOptions, FilterClause /// /// Execute a vector search and return the results using legacy filtering for backward compatibility. /// Converts legacy values to a LINQ expression tree for the modern @@ -297,6 +312,7 @@ private async IAsyncEnumerable> ExecuteVectorSearchA yield return result; } } +#pragma warning restore CS0618 /// /// Execute a vector search and return the results using modern LINQ filtering. @@ -328,7 +344,7 @@ private async IAsyncEnumerable> ExecuteVectorSearchA /// The to monitor for cancellation requests. private async IAsyncEnumerable> ExecuteVectorSearchCoreAsync(string query, VectorSearchOptions vectorSearchOptions, int top, [EnumeratorCancellation] CancellationToken cancellationToken) { -#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS0618 // Obsolete _textEmbeddingGeneration backward compatibility if (this._textEmbeddingGeneration is not null) { var vectorizedQuery = await this._textEmbeddingGeneration!.GenerateEmbeddingAsync(query, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -340,7 +356,7 @@ private async IAsyncEnumerable> ExecuteVectorSearchC yield break; } -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618 await foreach (var result in this._vectorSearchable!.SearchAsync(query, top, vectorSearchOptions, cancellationToken).WithCancellation(cancellationToken).ConfigureAwait(false)) { @@ -436,6 +452,7 @@ private async IAsyncEnumerable GetResultsAsStringAsync(IAsyncEnumerable< } } +#pragma warning disable CS0618 // Obsolete FilterClause, EqualToFilterClause /// /// Converts a collection of legacy instances to a LINQ expression tree. /// @@ -492,6 +509,7 @@ private async IAsyncEnumerable GetResultsAsStringAsync(IAsyncEnumerable< return Expression.Lambda>(combined!, parameter); } +#pragma warning restore CS0618 #endregion } diff --git a/dotnet/src/SemanticKernel.Core/Data/TextSearchBehavior/TextSearchProviderOptions.cs b/dotnet/src/SemanticKernel.Core/Data/TextSearchBehavior/TextSearchProviderOptions.cs index f6cd9008012b..d84aa5f6bbaa 100644 --- a/dotnet/src/SemanticKernel.Core/Data/TextSearchBehavior/TextSearchProviderOptions.cs +++ b/dotnet/src/SemanticKernel.Core/Data/TextSearchBehavior/TextSearchProviderOptions.cs @@ -36,7 +36,9 @@ public int Top /// /// Gets or sets the filter expression to apply to the search query. /// +#pragma warning disable CS0618 // TextSearchFilter is obsolete - backward compatibility shim public TextSearchFilter? Filter { get; init; } +#pragma warning restore CS0618 /// /// Gets or sets the time at which the text search is performed. diff --git a/dotnet/src/SemanticKernel.UnitTests/Data/MockTextSearch.cs b/dotnet/src/SemanticKernel.UnitTests/Data/MockTextSearch.cs index 01746adf623e..f31302dad0b9 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Data/MockTextSearch.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Data/MockTextSearch.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable CS0618 // Testing obsolete ITextSearch, TextSearchOptions backward compatibility + using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,9 +12,7 @@ namespace SemanticKernel.UnitTests.Data; /// /// Mock implementation of /// -#pragma warning disable CS0618 // Type or member is obsolete internal sealed class MockTextSearch(int count = 3, long totalCount = 30) : ITextSearch -#pragma warning restore CS0618 // Type or member is obsolete { /// public Task> GetSearchResultsAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs b/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs index 75f4b090590e..c948240a5646 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs @@ -12,7 +12,7 @@ namespace SemanticKernel.UnitTests.Data; public class VectorStoreTextSearchTests : VectorStoreTextSearchTestBase { -#pragma warning disable CS0618 // VectorStoreTextSearch with ITextEmbeddingGenerationService is obsolete +#pragma warning disable CS0618 // Testing obsolete TextSearchFilter backward compatibility [Fact] public void CanCreateVectorStoreTextSearchWithEmbeddingGenerationService() { @@ -29,7 +29,6 @@ public void CanCreateVectorStoreTextSearchWithEmbeddingGenerationService() // Assert. Assert.NotNull(sut); } -#pragma warning restore CS0618 [Fact] public void CanCreateVectorStoreTextSearchWithIVectorSearch() @@ -129,7 +128,6 @@ public async Task CanGetSearchResultsWithEmbeddingGeneratorAsync() Assert.All(results, result => Assert.IsType(result)); } -#pragma warning disable CS0618 // VectorStoreTextSearch with ITextEmbeddingGenerationService is obsolete [Fact] public async Task CanSearchWithEmbeddingGenerationServiceAsync() { @@ -168,7 +166,6 @@ public async Task CanGetSearchResultsWithEmbeddingGenerationServiceAsync() Assert.Equal(2, results.Count); } -#pragma warning restore CS0618 // VectorStoreTextSearch with ITextEmbeddingGenerationService is obsolete [Fact] public async Task CanFilterGetSearchResultsWithVectorizedSearchAsync() @@ -209,6 +206,8 @@ public async Task CanFilterGetSearchResultsWithVectorizedSearchAsync() Assert.Equal("Odd", result2?.Tag); } +#pragma warning restore CS0618 + #region Generic Interface Tests (ITextSearch) [Fact] diff --git a/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/AnyTagEqualToFilterClause.cs b/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/AnyTagEqualToFilterClause.cs index f40e63faa940..c9419d087732 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/AnyTagEqualToFilterClause.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/AnyTagEqualToFilterClause.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System; + namespace Microsoft.Extensions.VectorData; /// /// Represents a filter clause that filters by checking if a field consisting of a list of values contains a specific value. /// +[Obsolete("Use LINQ expressions via VectorSearchOptions.Filter instead. This type will be removed in a future version.")] public sealed class AnyTagEqualToFilterClause : FilterClause { /// diff --git a/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/EqualToFilterClause.cs b/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/EqualToFilterClause.cs index 89865732bd75..03fc678abc8b 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/EqualToFilterClause.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/EqualToFilterClause.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System; + namespace Microsoft.Extensions.VectorData; /// /// Represents a filter clause that filters using equality of a field value. /// +[Obsolete("Use LINQ expressions via VectorSearchOptions.Filter instead. This type will be removed in a future version.")] public sealed class EqualToFilterClause : FilterClause { /// diff --git a/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/FilterClause.cs b/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/FilterClause.cs index be72560ffc2f..97c8869a27ea 100644 --- a/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/FilterClause.cs +++ b/dotnet/src/VectorData/VectorData.Abstractions/FilterClauses/FilterClause.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; + namespace Microsoft.Extensions.VectorData; /// @@ -9,6 +11,7 @@ namespace Microsoft.Extensions.VectorData; /// A is used to request that the underlying search service should /// filter search results based on the specified criteria. /// +[Obsolete("Use LINQ expressions via VectorSearchOptions.Filter instead. This type will be removed in a future version.")] public abstract class FilterClause { ///