Skip to content

Commit 395413d

Browse files
committed
Add unit tests for generic ITextSearch<BingWebPage> methods
- Added 9 semantic verification tests for LINQ filter translation - Tests verify correct Bing API query parameter generation - Fixed inequality operator bug discovered during testing - Addresses reviewer feedback on PR microsoft#13188
1 parent c9d76a8 commit 395413d

File tree

2 files changed

+244
-5
lines changed

2 files changed

+244
-5
lines changed

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

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,237 @@ public async Task DoesNotBuildsUriForInvalidQueryParameterAsync()
231231
Assert.Equal("Unknown equality filter clause field name 'fooBar', must be one of answerCount,cc,freshness,mkt,promote,responseFilter,safeSearch,setLang,textDecorations,textFormat,contains,ext,filetype,inanchor,inbody,intitle,ip,language,loc,location,prefer,site,feed,hasfeed,url (Parameter 'searchOptions')", e.Message);
232232
}
233233

234+
#region Generic ITextSearch<BingWebPage> Interface Tests
235+
236+
[Fact]
237+
public async Task GenericSearchAsyncWithLanguageEqualityFilterProducesCorrectBingQueryAsync()
238+
{
239+
// Arrange
240+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
241+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
242+
243+
// Act
244+
var searchOptions = new TextSearchOptions<BingWebPage>
245+
{
246+
Top = 4,
247+
Skip = 0,
248+
Filter = page => page.Language == "en"
249+
};
250+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
251+
252+
// Assert - Verify LINQ expression converted to Bing's language: operator
253+
var requestUris = this._messageHandlerStub.RequestUris;
254+
Assert.Single(requestUris);
255+
Assert.NotNull(requestUris[0]);
256+
Assert.Contains("language%3Aen", requestUris[0]!.AbsoluteUri);
257+
Assert.Contains("count=4", requestUris[0]!.AbsoluteUri);
258+
Assert.Contains("offset=0", requestUris[0]!.AbsoluteUri);
259+
}
260+
261+
[Fact]
262+
public async Task GenericSearchAsyncWithLanguageInequalityFilterProducesCorrectBingQueryAsync()
263+
{
264+
// Arrange
265+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
266+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
267+
268+
// Act
269+
var searchOptions = new TextSearchOptions<BingWebPage>
270+
{
271+
Top = 4,
272+
Skip = 0,
273+
Filter = page => page.Language != "fr"
274+
};
275+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
276+
277+
// Assert - Verify LINQ inequality expression converted to Bing's negation syntax (-language:fr)
278+
var requestUris = this._messageHandlerStub.RequestUris;
279+
Assert.Single(requestUris);
280+
Assert.NotNull(requestUris[0]);
281+
Assert.Contains("-language%3Afr", requestUris[0]!.AbsoluteUri);
282+
}
283+
284+
[Fact]
285+
public async Task GenericSearchAsyncWithContainsFilterProducesCorrectBingQueryAsync()
286+
{
287+
// Arrange
288+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
289+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
290+
291+
// Act
292+
var searchOptions = new TextSearchOptions<BingWebPage>
293+
{
294+
Top = 4,
295+
Skip = 0,
296+
Filter = page => page.Name.Contains("Microsoft")
297+
};
298+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
299+
300+
// Assert - Verify LINQ Contains() converted to Bing's intitle: operator
301+
var requestUris = this._messageHandlerStub.RequestUris;
302+
Assert.Single(requestUris);
303+
Assert.NotNull(requestUris[0]);
304+
Assert.Contains("intitle%3AMicrosoft", requestUris[0]!.AbsoluteUri);
305+
}
306+
307+
[Fact]
308+
public async Task GenericSearchAsyncWithComplexAndFilterProducesCorrectBingQueryAsync()
309+
{
310+
// Arrange
311+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
312+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
313+
314+
// Act
315+
var searchOptions = new TextSearchOptions<BingWebPage>
316+
{
317+
Top = 4,
318+
Skip = 0,
319+
Filter = page => page.Language == "en" && page.Name.Contains("AI")
320+
};
321+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
322+
323+
// Assert - Verify LINQ AND expression produces both Bing operators
324+
var requestUris = this._messageHandlerStub.RequestUris;
325+
Assert.Single(requestUris);
326+
Assert.NotNull(requestUris[0]);
327+
Assert.Contains("language%3Aen", requestUris[0]!.AbsoluteUri);
328+
Assert.Contains("intitle%3AAI", requestUris[0]!.AbsoluteUri);
329+
}
330+
331+
[Fact]
332+
public async Task GenericGetTextSearchResultsAsyncWithUrlFilterProducesCorrectBingQueryAsync()
333+
{
334+
// Arrange
335+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
336+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
337+
338+
// Act
339+
var searchOptions = new TextSearchOptions<BingWebPage>
340+
{
341+
Top = 4,
342+
Skip = 0,
343+
Filter = page => page.Url.Contains("microsoft.com")
344+
};
345+
KernelSearchResults<TextSearchResult> result = await textSearch.GetTextSearchResultsAsync("What is the Semantic Kernel?", searchOptions);
346+
347+
// Assert - Verify LINQ Url.Contains() converted to Bing's url: operator
348+
var requestUris = this._messageHandlerStub.RequestUris;
349+
Assert.Single(requestUris);
350+
Assert.NotNull(requestUris[0]);
351+
Assert.Contains("url%3Amicrosoft.com", requestUris[0]!.AbsoluteUri);
352+
353+
// Also verify result structure
354+
Assert.NotNull(result);
355+
Assert.NotNull(result.Results);
356+
}
357+
358+
[Fact]
359+
public async Task GenericGetSearchResultsAsyncWithSnippetContainsFilterProducesCorrectBingQueryAsync()
360+
{
361+
// Arrange
362+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
363+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
364+
365+
// Act
366+
var searchOptions = new TextSearchOptions<BingWebPage>
367+
{
368+
Top = 4,
369+
Skip = 0,
370+
Filter = page => page.Snippet.Contains("semantic")
371+
};
372+
KernelSearchResults<object> result = await textSearch.GetSearchResultsAsync("What is the Semantic Kernel?", searchOptions);
373+
374+
// Assert - Verify LINQ Snippet.Contains() converted to Bing's inbody: operator
375+
var requestUris = this._messageHandlerStub.RequestUris;
376+
Assert.Single(requestUris);
377+
Assert.NotNull(requestUris[0]);
378+
Assert.Contains("inbody%3Asemantic", requestUris[0]!.AbsoluteUri);
379+
380+
// Verify result structure
381+
Assert.NotNull(result);
382+
Assert.NotNull(result.Results);
383+
}
384+
385+
[Fact]
386+
public async Task GenericSearchAsyncWithDisplayUrlEqualityFilterProducesCorrectBingQueryAsync()
387+
{
388+
// Arrange
389+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(SiteFilterDevBlogsResponseJson));
390+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
391+
392+
// Act
393+
var searchOptions = new TextSearchOptions<BingWebPage>
394+
{
395+
Top = 4,
396+
Skip = 0,
397+
Filter = page => page.DisplayUrl == "devblogs.microsoft.com"
398+
};
399+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
400+
401+
// Assert - Verify LINQ DisplayUrl equality converted to Bing's site: operator
402+
var requestUris = this._messageHandlerStub.RequestUris;
403+
Assert.Single(requestUris);
404+
Assert.NotNull(requestUris[0]);
405+
Assert.Contains("site%3Adevblogs.microsoft.com", requestUris[0]!.AbsoluteUri);
406+
}
407+
408+
[Fact]
409+
public async Task GenericSearchAsyncWithMultipleAndConditionsProducesCorrectBingQueryAsync()
410+
{
411+
// Arrange
412+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
413+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
414+
415+
// Act
416+
var searchOptions = new TextSearchOptions<BingWebPage>
417+
{
418+
Top = 4,
419+
Skip = 0,
420+
Filter = page => page.Language == "en" && page.DisplayUrl.Contains("microsoft.com") && page.Name.Contains("Semantic")
421+
};
422+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
423+
424+
// Assert - Verify all LINQ conditions converted correctly
425+
var requestUris = this._messageHandlerStub.RequestUris;
426+
Assert.Single(requestUris);
427+
Assert.NotNull(requestUris[0]);
428+
string uri = requestUris[0]!.AbsoluteUri;
429+
Assert.Contains("language%3Aen", uri);
430+
Assert.Contains("site%3Amicrosoft.com", uri); // DisplayUrl.Contains() → site: operator
431+
Assert.Contains("intitle%3ASemantic", uri);
432+
}
433+
434+
[Fact]
435+
public async Task GenericSearchAsyncWithNoFilterReturnsResultsSuccessfullyAsync()
436+
{
437+
// Arrange
438+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
439+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
440+
441+
// Act - No filter specified
442+
var searchOptions = new TextSearchOptions<BingWebPage>
443+
{
444+
Top = 10,
445+
Skip = 0
446+
};
447+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
448+
449+
// Assert - Verify basic query without filter operators
450+
var requestUris = this._messageHandlerStub.RequestUris;
451+
Assert.Single(requestUris);
452+
Assert.NotNull(requestUris[0]);
453+
Assert.DoesNotContain("language%3A", requestUris[0]!.AbsoluteUri);
454+
Assert.DoesNotContain("intitle%3A", requestUris[0]!.AbsoluteUri);
455+
456+
// Verify results
457+
Assert.NotNull(result);
458+
Assert.NotNull(result.Results);
459+
var resultList = await result.Results.ToListAsync();
460+
Assert.Equal(10, resultList.Count);
461+
}
462+
463+
#endregion
464+
234465
/// <inheritdoc/>
235466
public void Dispose()
236467
{

dotnet/src/Plugins/Plugins.Web/Bing/BingTextSearch.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,10 @@ private static void ProcessEqualityExpression(BinaryExpression binaryExpr, TextS
212212
{
213213
if (isNegated)
214214
{
215-
// For inequality, use Bing's negation syntax by prepending '-' to the filter name
216-
// Example: -language:en excludes pages in English
217-
filter.Equality($"-{bingFilterName}", value);
215+
// For inequality, wrap the value with a negation marker
216+
// This will be processed in BuildQuery to prepend '-' to the advanced search operator
217+
// Example: language:en becomes -language:en (excludes pages in English)
218+
filter.Equality(bingFilterName, $"-{value}");
218219
}
219220
else
220221
{
@@ -504,14 +505,21 @@ private static string BuildQuery(string query, TextSearchOptions searchOptions)
504505
{
505506
if (filterClause is EqualToFilterClause equalityFilterClause)
506507
{
508+
// Check if value starts with '-' indicating negation (for inequality != operator)
509+
string? valueStr = equalityFilterClause.Value?.ToString();
510+
bool isNegated = valueStr?.StartsWith("-", StringComparison.Ordinal) == true;
511+
string actualValue = isNegated && valueStr != null ? valueStr.Substring(1) : valueStr ?? string.Empty;
512+
507513
if (s_advancedSearchKeywords.Contains(equalityFilterClause.FieldName, StringComparer.OrdinalIgnoreCase) && equalityFilterClause.Value is not null)
508514
{
509-
fullQuery.Append($"+{equalityFilterClause.FieldName}%3A").Append(Uri.EscapeDataString(equalityFilterClause.Value.ToString()!));
515+
// For advanced search keywords, prepend '-' if negated to exclude results
516+
string prefix = isNegated ? "-" : "";
517+
fullQuery.Append($"+{prefix}{equalityFilterClause.FieldName}%3A").Append(Uri.EscapeDataString(actualValue));
510518
}
511519
else if (s_queryParameters.Contains(equalityFilterClause.FieldName, StringComparer.OrdinalIgnoreCase) && equalityFilterClause.Value is not null)
512520
{
513521
string? queryParam = s_queryParameters.FirstOrDefault(s => s.Equals(equalityFilterClause.FieldName, StringComparison.OrdinalIgnoreCase));
514-
queryParams.Append('&').Append(queryParam!).Append('=').Append(Uri.EscapeDataString(equalityFilterClause.Value.ToString()!));
522+
queryParams.Append('&').Append(queryParam!).Append('=').Append(Uri.EscapeDataString(actualValue));
515523
}
516524
else
517525
{

0 commit comments

Comments
 (0)