Skip to content

Commit c7a2b06

Browse files
author
Alexander Zarei
committed
Enhance text search LINQ support with OR/NOT operators and improved error messages
- Added support for OrElse (||) operators in LINQ expressions - Added NotEqual (!=) operator support with appropriate error handling - Added UnaryExpression (NOT) support with proper limitations - Enhanced Contains method support for array.Contains(property) patterns - Improved error messages with usage examples - Added constants for API parameter names for better maintainability - Added TODO comments for potential code sharing opportunities Changes apply to both BraveTextSearch and TavilyTextSearch implementations.
1 parent 92f783e commit c7a2b06

2 files changed

Lines changed: 277 additions & 32 deletions

File tree

dotnet/src/Plugins/Plugins.Web/Brave/BraveTextSearch.cs

Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ private static TextSearchFilter ConvertLinqExpressionToBraveFilter<TRecord>(Expr
186186
}
187187
else
188188
{
189-
throw new NotSupportedException($"Property '{equalityClause.FieldName}' cannot be mapped to Brave API filters. Supported properties: {string.Join(", ", s_queryParameters)}");
189+
throw new NotSupportedException(
190+
$"Property '{equalityClause.FieldName}' cannot be mapped to Brave API filters. " +
191+
$"Supported properties: {string.Join(", ", s_queryParameters)}. " +
192+
"Example: page => page.Country == \"US\" && page.SafeSearch == \"moderate\"");
190193
}
191194
}
192195
}
@@ -202,18 +205,21 @@ private static TextSearchFilter ConvertLinqExpressionToBraveFilter<TRecord>(Expr
202205
private static string? MapPropertyToBraveFilter(string propertyName) =>
203206
propertyName.ToUpperInvariant() switch
204207
{
205-
"COUNTRY" => "country",
206-
"SEARCHLANG" => "search_lang",
207-
"UILANG" => "ui_lang",
208-
"SAFESEARCH" => "safesearch",
209-
"TEXTDECORATIONS" => "text_decorations",
210-
"SPELLCHECK" => "spellcheck",
211-
"RESULTFILTER" => "result_filter",
212-
"UNITS" => "units",
213-
"EXTRASNIPPETS" => "extra_snippets",
208+
"COUNTRY" => BraveParamCountry,
209+
"SEARCHLANG" => BraveParamSearchLang,
210+
"UILANG" => BraveParamUiLang,
211+
"SAFESEARCH" => BraveParamSafeSearch,
212+
"TEXTDECORATIONS" => BraveParamTextDecorations,
213+
"SPELLCHECK" => BraveParamSpellCheck,
214+
"RESULTFILTER" => BraveParamResultFilter,
215+
"UNITS" => BraveParamUnits,
216+
"EXTRASNIPPETS" => BraveParamExtraSnippets,
214217
_ => null // Property not mappable to Brave filters
215218
};
216219

220+
// TODO: Consider extracting LINQ expression analysis logic to a shared utility class
221+
// to reduce duplication across text search connectors (Brave, Tavily, etc.).
222+
// See code review for details.
217223
/// <summary>
218224
/// Analyzes a LINQ expression and extracts filter clauses.
219225
/// </summary>
@@ -230,17 +236,35 @@ private static void AnalyzeExpression(Expression expression, List<FilterClause>
230236
AnalyzeExpression(binaryExpr.Left, filterClauses);
231237
AnalyzeExpression(binaryExpr.Right, filterClauses);
232238
}
239+
else if (binaryExpr.NodeType == ExpressionType.OrElse)
240+
{
241+
// Handle OR expressions by recursively analyzing both sides
242+
// Note: OR results in multiple filter values for the same property
243+
AnalyzeExpression(binaryExpr.Left, filterClauses);
244+
AnalyzeExpression(binaryExpr.Right, filterClauses);
245+
}
233246
else if (binaryExpr.NodeType == ExpressionType.Equal)
234247
{
235248
// Handle equality expressions
236249
ExtractEqualityClause(binaryExpr, filterClauses);
237250
}
251+
else if (binaryExpr.NodeType == ExpressionType.NotEqual)
252+
{
253+
// Handle inequality expressions (property != value)
254+
// This is supported as a negation pattern
255+
ExtractInequalityClause(binaryExpr, filterClauses);
256+
}
238257
else
239258
{
240-
throw new NotSupportedException($"Binary expression type '{binaryExpr.NodeType}' is not supported. Only AndAlso and Equal are supported.");
259+
throw new NotSupportedException($"Binary expression type '{binaryExpr.NodeType}' is not supported. Supported operators: AndAlso (&&), OrElse (||), Equal (==), NotEqual (!=).");
241260
}
242261
break;
243262

263+
case UnaryExpression unaryExpr when unaryExpr.NodeType == ExpressionType.Not:
264+
// Handle NOT expressions (negation)
265+
AnalyzeNotExpression(unaryExpr, filterClauses);
266+
break;
267+
244268
case MethodCallExpression methodCall:
245269
// Handle method calls like Contains, StartsWith, etc.
246270
ExtractMethodCallClause(methodCall, filterClauses);
@@ -283,34 +307,127 @@ private static void ExtractEqualityClause(BinaryExpression binaryExpr, List<Filt
283307
}
284308
}
285309

310+
/// <summary>
311+
/// Extracts an inequality filter clause from a binary not-equal expression.
312+
/// </summary>
313+
/// <param name="binaryExpr">The binary not-equal expression.</param>
314+
/// <param name="filterClauses">The list to add the extracted clause to.</param>
315+
private static void ExtractInequalityClause(BinaryExpression binaryExpr, List<FilterClause> filterClauses)
316+
{
317+
// Note: Inequality is tracked but handled differently depending on the property
318+
// For now, we log a warning that inequality filtering may not work as expected
319+
string? propertyName = null;
320+
object? value = null;
321+
322+
if (binaryExpr.Left is MemberExpression leftMember)
323+
{
324+
propertyName = leftMember.Member.Name;
325+
value = ExtractValue(binaryExpr.Right);
326+
}
327+
else if (binaryExpr.Right is MemberExpression rightMember)
328+
{
329+
propertyName = rightMember.Member.Name;
330+
value = ExtractValue(binaryExpr.Left);
331+
}
332+
333+
if (propertyName != null && value != null)
334+
{
335+
// Add a marker for inequality - this will need special handling in conversion
336+
// For now, we don't add it to filter clauses as Brave API doesn't support direct negation
337+
throw new NotSupportedException($"Inequality operator (!=) is not directly supported for property '{propertyName}'. Use NOT operator instead: !(page.{propertyName} == value).");
338+
}
339+
340+
throw new NotSupportedException("Unable to extract property name and value from inequality expression.");
341+
}
342+
343+
/// <summary>
344+
/// Analyzes a NOT (negation) expression.
345+
/// </summary>
346+
/// <param name="unaryExpr">The unary NOT expression.</param>
347+
/// <param name="filterClauses">The list to add extracted filter clauses to.</param>
348+
private static void AnalyzeNotExpression(UnaryExpression unaryExpr, List<FilterClause> filterClauses)
349+
{
350+
// NOT expressions are complex for web search APIs
351+
// We support simple cases like !(page.SafeSearch == "off")
352+
if (unaryExpr.Operand is BinaryExpression binaryExpr && binaryExpr.NodeType == ExpressionType.Equal)
353+
{
354+
// This is !(property == value), which we can handle for some properties
355+
throw new NotSupportedException("NOT operator (!) with equality is not directly supported. Most web search APIs don't support negative filtering.");
356+
}
357+
358+
throw new NotSupportedException("NOT operator (!) is only supported with simple equality expressions.");
359+
}
360+
286361
/// <summary>
287362
/// Extracts a filter clause from a method call expression (e.g., Contains, StartsWith).
288363
/// </summary>
289364
/// <param name="methodCall">The method call expression.</param>
290365
/// <param name="filterClauses">The list to add the extracted clause to.</param>
291366
private static void ExtractMethodCallClause(MethodCallExpression methodCall, List<FilterClause> filterClauses)
292367
{
293-
if (methodCall.Method.Name == "Contains" && methodCall.Object is MemberExpression member)
368+
if (methodCall.Method.Name == "Contains")
294369
{
295-
var propertyName = member.Member.Name;
296-
var value = ExtractValue(methodCall.Arguments[0]);
370+
// Check if this is property.Contains(value) or array.Contains(property)
371+
if (methodCall.Object is MemberExpression member)
372+
{
373+
// This is property.Contains(value) - e.g., page.ResultFilter.Contains("web")
374+
var propertyName = member.Member.Name;
375+
var value = ExtractValue(methodCall.Arguments[0]);
297376

298-
if (value != null)
377+
if (value != null)
378+
{
379+
// For Contains, we'll map it to equality for certain properties
380+
if (propertyName.Equals("ResultFilter", StringComparison.OrdinalIgnoreCase))
381+
{
382+
filterClauses.Add(new EqualToFilterClause(propertyName, value));
383+
}
384+
else
385+
{
386+
throw new NotSupportedException($"Contains method is only supported for ResultFilter property, not '{propertyName}'.");
387+
}
388+
}
389+
}
390+
else if (methodCall.Object == null && methodCall.Arguments.Count == 2)
299391
{
300-
// For Contains, we'll map it to equality for certain properties
301-
if (propertyName.Equals("ResultFilter", StringComparison.OrdinalIgnoreCase))
392+
// This is array.Contains(property) - e.g., new[] { "US", "GB" }.Contains(page.Country)
393+
// This is an extension method call where the first argument is the array
394+
var arrayExpr = methodCall.Arguments[0];
395+
var propertyExpr = methodCall.Arguments[1];
396+
397+
if (propertyExpr is MemberExpression propertyMember)
302398
{
303-
filterClauses.Add(new EqualToFilterClause(propertyName, value));
399+
var propertyName = propertyMember.Member.Name;
400+
var arrayValue = ExtractValue(arrayExpr);
401+
402+
if (arrayValue is System.Collections.IEnumerable enumerable)
403+
{
404+
// Convert to OR expressions - each value becomes an equality clause
405+
foreach (var value in enumerable)
406+
{
407+
if (value != null)
408+
{
409+
filterClauses.Add(new EqualToFilterClause(propertyName, value));
410+
}
411+
}
412+
}
413+
else
414+
{
415+
throw new NotSupportedException($"Contains argument must be an array or collection, got: {arrayValue?.GetType().Name}");
416+
}
304417
}
305418
else
306419
{
307-
throw new NotSupportedException($"Contains method is only supported for ResultFilter property, not '{propertyName}'.");
420+
throw new NotSupportedException("Contains with inline collection requires a property reference as the second argument.");
308421
}
309422
}
423+
else
424+
{
425+
throw new NotSupportedException("Unsupported Contains expression format.");
426+
}
310427
}
311428
else
312429
{
313-
throw new NotSupportedException($"Method '{methodCall.Method.Name}' is not supported in Brave search filters.");
430+
throw new NotSupportedException($"Method '{methodCall.Method.Name}' is not supported in Brave search filters. Only 'Contains' is supported.");
314431
}
315432
}
316433

@@ -349,8 +466,19 @@ private static void ExtractMethodCallClause(MethodCallExpression methodCall, Lis
349466
private static readonly ITextSearchStringMapper s_defaultStringMapper = new DefaultTextSearchStringMapper();
350467
private static readonly ITextSearchResultMapper s_defaultResultMapper = new DefaultTextSearchResultMapper();
351468

469+
// Constants for Brave API parameter names
470+
private const string BraveParamCountry = "country";
471+
private const string BraveParamSearchLang = "search_lang";
472+
private const string BraveParamUiLang = "ui_lang";
473+
private const string BraveParamSafeSearch = "safesearch";
474+
private const string BraveParamTextDecorations = "text_decorations";
475+
private const string BraveParamSpellCheck = "spellcheck";
476+
private const string BraveParamResultFilter = "result_filter";
477+
private const string BraveParamUnits = "units";
478+
private const string BraveParamExtraSnippets = "extra_snippets";
479+
352480
// See https://api-dashboard.search.brave.com/app/documentation/web-search/query#WebSearchAPIQueryParameters
353-
private static readonly string[] s_queryParameters = ["country", "search_lang", "ui_lang", "safesearch", "text_decorations", "spellcheck", "result_filter", "units", "extra_snippets"];
481+
private static readonly string[] s_queryParameters = [BraveParamCountry, BraveParamSearchLang, BraveParamUiLang, BraveParamSafeSearch, BraveParamTextDecorations, BraveParamSpellCheck, BraveParamResultFilter, BraveParamUnits, BraveParamExtraSnippets];
354482

355483
private static readonly string[] s_safeSearch = ["off", "moderate", "strict"];
356484

0 commit comments

Comments
 (0)