Skip to content

Commit a56d256

Browse files
committed
.Net: Add provider-specific default search parameters to Bing/Google connectors
Add request-side API parameters as instance-level defaults on BingTextSearchOptions and GoogleTextSearchOptions. These parameters (e.g., market, language, safe-search) control request behavior but have no representation in response models, making them unreachable via the LINQ filter system. BingTextSearchOptions: Market, Freshness, SafeSearch, CountryCode, SetLanguage, ResponseFilter, AnswerCount, Promote, TextDecorations, TextFormat. GoogleTextSearchOptions: CountryRestrict, DateRestrict, GeoLocation, InterfaceLanguage, LinkSite, LanguageRestrict, Rights, DuplicateContentFilter. Defaults are applied to every search request unless overridden by a per-request filter clause. Filter values always take precedence. Addresses Roji's Remark 3 on PR microsoft#13384.
1 parent 7d2a06f commit a56d256

6 files changed

Lines changed: 529 additions & 2 deletions

File tree

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

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#pragma warning disable CS0618 // ITextSearch is obsolete
44
#pragma warning disable CS8602 // Dereference of a possibly null reference - Test LINQ expressions access BingWebPage properties guaranteed non-null in test context
5+
#pragma warning disable CA1859 // Use concrete types when possible for improved performance - tests intentionally use interface types
56

67
using System;
78
using System.IO;
@@ -930,6 +931,167 @@ public async Task StringContainsStillWorksWithLINQFiltersAsync()
930931

931932
#endregion
932933

934+
#region Default Search Parameter Tests
935+
936+
[Fact]
937+
public async Task DefaultMarketIsAppliedToSearchRequestAsync()
938+
{
939+
// Arrange
940+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
941+
var textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient, Market = "en-US" });
942+
943+
// Act
944+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query");
945+
946+
// Assert
947+
var requestUris = this._messageHandlerStub.RequestUris;
948+
Assert.Single(requestUris);
949+
Assert.Contains("mkt=en-US", requestUris[0]!.AbsoluteUri);
950+
}
951+
952+
[Fact]
953+
public async Task DefaultFreshnessIsAppliedToSearchRequestAsync()
954+
{
955+
// Arrange
956+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
957+
var textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient, Freshness = "Month" });
958+
959+
// Act
960+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query");
961+
962+
// Assert
963+
var requestUris = this._messageHandlerStub.RequestUris;
964+
Assert.Single(requestUris);
965+
Assert.Contains("freshness=Month", requestUris[0]!.AbsoluteUri);
966+
}
967+
968+
[Fact]
969+
public async Task DefaultSafeSearchIsAppliedToSearchRequestAsync()
970+
{
971+
// Arrange
972+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
973+
var textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient, SafeSearch = "Strict" });
974+
975+
// Act
976+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query");
977+
978+
// Assert
979+
var requestUris = this._messageHandlerStub.RequestUris;
980+
Assert.Single(requestUris);
981+
Assert.Contains("safeSearch=Strict", requestUris[0]!.AbsoluteUri);
982+
}
983+
984+
[Fact]
985+
public async Task MultipleDefaultParametersAreAppliedToSearchRequestAsync()
986+
{
987+
// Arrange
988+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
989+
var textSearch = new BingTextSearch(apiKey: "ApiKey", options: new()
990+
{
991+
HttpClient = this._httpClient,
992+
Market = "en-US",
993+
Freshness = "Week",
994+
SafeSearch = "Moderate",
995+
TextDecorations = true
996+
});
997+
998+
// Act
999+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query");
1000+
1001+
// Assert
1002+
var requestUris = this._messageHandlerStub.RequestUris;
1003+
Assert.Single(requestUris);
1004+
string uri = requestUris[0]!.AbsoluteUri;
1005+
Assert.Contains("mkt=en-US", uri);
1006+
Assert.Contains("freshness=Week", uri);
1007+
Assert.Contains("safeSearch=Moderate", uri);
1008+
Assert.Contains("textDecorations=true", uri);
1009+
}
1010+
1011+
[Fact]
1012+
public async Task FilterOverridesDefaultParameterAsync()
1013+
{
1014+
// Arrange: Set Market as default, then override via legacy filter
1015+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
1016+
var textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient, Market = "en-US" });
1017+
var searchOptions = new TextSearchOptions { Top = 4, Skip = 0, Filter = new TextSearchFilter().Equality("mkt", "fr-FR") };
1018+
1019+
// Act
1020+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", searchOptions);
1021+
1022+
// Assert: Only fr-FR should appear, not en-US
1023+
var requestUris = this._messageHandlerStub.RequestUris;
1024+
Assert.Single(requestUris);
1025+
string uri = requestUris[0]!.AbsoluteUri;
1026+
Assert.Contains("mkt=fr-FR", uri);
1027+
Assert.DoesNotContain("mkt=en-US", uri);
1028+
}
1029+
1030+
[Fact]
1031+
public async Task DefaultParametersWorkWithGenericInterfaceAsync()
1032+
{
1033+
// Arrange
1034+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
1035+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient, Market = "de-DE" });
1036+
1037+
// Act
1038+
var searchOptions = new TextSearchOptions<BingWebPage> { Top = 4, Skip = 0 };
1039+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", searchOptions);
1040+
1041+
// Assert
1042+
var requestUris = this._messageHandlerStub.RequestUris;
1043+
Assert.Single(requestUris);
1044+
Assert.Contains("mkt=de-DE", requestUris[0]!.AbsoluteUri);
1045+
}
1046+
1047+
[Fact]
1048+
public async Task DefaultParametersCoexistWithLinqFiltersAsync()
1049+
{
1050+
// Arrange
1051+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
1052+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new()
1053+
{
1054+
HttpClient = this._httpClient,
1055+
Market = "en-US",
1056+
Freshness = "Month"
1057+
});
1058+
1059+
// Act: LINQ filter for response-side property + default request-side params
1060+
var searchOptions = new TextSearchOptions<BingWebPage>
1061+
{
1062+
Top = 4,
1063+
Skip = 0,
1064+
Filter = page => page.Language == "en"
1065+
};
1066+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", searchOptions);
1067+
1068+
// Assert: Both LINQ filter and defaults should appear
1069+
var requestUris = this._messageHandlerStub.RequestUris;
1070+
Assert.Single(requestUris);
1071+
string uri = requestUris[0]!.AbsoluteUri;
1072+
Assert.Contains("language%3Aen", uri);
1073+
Assert.Contains("mkt=en-US", uri);
1074+
Assert.Contains("freshness=Month", uri);
1075+
}
1076+
1077+
[Fact]
1078+
public async Task DefaultAnswerCountIsAppliedCorrectlyAsync()
1079+
{
1080+
// Arrange
1081+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
1082+
var textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient, AnswerCount = 3 });
1083+
1084+
// Act
1085+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query");
1086+
1087+
// Assert
1088+
var requestUris = this._messageHandlerStub.RequestUris;
1089+
Assert.Single(requestUris);
1090+
Assert.Contains("answerCount=3", requestUris[0]!.AbsoluteUri);
1091+
}
1092+
1093+
#endregion
1094+
9331095
/// <inheritdoc/>
9341096
public void Dispose()
9351097
{

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

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

33
#pragma warning disable CS0618 // ITextSearch is obsolete
4+
#pragma warning disable CA1859 // Use concrete types when possible for improved performance - tests intentionally use interface types
45

56
using System;
67
using System.IO;
@@ -733,6 +734,157 @@ await textSearch.SearchAsync("test",
733734

734735
#endregion
735736

737+
#region Default Search Parameter Tests
738+
739+
[Fact]
740+
public async Task DefaultGeoLocationIsAppliedToSearchRequestAsync()
741+
{
742+
// Arrange
743+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
744+
using var textSearch = new GoogleTextSearch(
745+
initializer: new() { ApiKey = "ApiKey", HttpClientFactory = this._clientFactory },
746+
searchEngineId: "SearchEngineId",
747+
options: new GoogleTextSearchOptions { GeoLocation = "us" });
748+
749+
// Act
750+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", new TextSearchOptions { Top = 4, Skip = 0 });
751+
752+
// Assert
753+
var requestUris = this._messageHandlerStub.RequestUris;
754+
Assert.Single(requestUris);
755+
Assert.Contains("gl=us", requestUris[0]!.AbsoluteUri);
756+
}
757+
758+
[Fact]
759+
public async Task DefaultInterfaceLanguageIsAppliedToSearchRequestAsync()
760+
{
761+
// Arrange
762+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
763+
using var textSearch = new GoogleTextSearch(
764+
initializer: new() { ApiKey = "ApiKey", HttpClientFactory = this._clientFactory },
765+
searchEngineId: "SearchEngineId",
766+
options: new GoogleTextSearchOptions { InterfaceLanguage = "en" });
767+
768+
// Act
769+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", new TextSearchOptions { Top = 4, Skip = 0 });
770+
771+
// Assert
772+
var requestUris = this._messageHandlerStub.RequestUris;
773+
Assert.Single(requestUris);
774+
Assert.Contains("hl=en", requestUris[0]!.AbsoluteUri);
775+
}
776+
777+
[Fact]
778+
public async Task MultipleDefaultParametersAreAppliedToSearchRequestAsync()
779+
{
780+
// Arrange
781+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
782+
using var textSearch = new GoogleTextSearch(
783+
initializer: new() { ApiKey = "ApiKey", HttpClientFactory = this._clientFactory },
784+
searchEngineId: "SearchEngineId",
785+
options: new GoogleTextSearchOptions
786+
{
787+
GeoLocation = "us",
788+
InterfaceLanguage = "en",
789+
LanguageRestrict = "lang_en",
790+
DuplicateContentFilter = "1"
791+
});
792+
793+
// Act
794+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", new TextSearchOptions { Top = 4, Skip = 0 });
795+
796+
// Assert
797+
var requestUris = this._messageHandlerStub.RequestUris;
798+
Assert.Single(requestUris);
799+
string uri = requestUris[0]!.AbsoluteUri;
800+
Assert.Contains("gl=us", uri);
801+
Assert.Contains("hl=en", uri);
802+
Assert.Contains("lr=lang_en", uri);
803+
Assert.Contains("filter=1", uri);
804+
}
805+
806+
[Fact]
807+
public async Task FilterOverridesDefaultParameterAsync()
808+
{
809+
// Arrange: Set GeoLocation as default, then override via legacy filter
810+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
811+
using var textSearch = new GoogleTextSearch(
812+
initializer: new() { ApiKey = "ApiKey", HttpClientFactory = this._clientFactory },
813+
searchEngineId: "SearchEngineId",
814+
options: new GoogleTextSearchOptions { GeoLocation = "us" });
815+
var searchOptions = new TextSearchOptions { Top = 4, Skip = 0, Filter = new TextSearchFilter().Equality("gl", "de") };
816+
817+
// Act
818+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", searchOptions);
819+
820+
// Assert: Only de should appear, not us
821+
var requestUris = this._messageHandlerStub.RequestUris;
822+
Assert.Single(requestUris);
823+
string uri = requestUris[0]!.AbsoluteUri;
824+
Assert.Contains("gl=de", uri);
825+
Assert.DoesNotContain("gl=us", uri);
826+
}
827+
828+
[Fact]
829+
public async Task DefaultParametersWorkWithGenericInterfaceAsync()
830+
{
831+
// Arrange
832+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
833+
ITextSearch<GoogleWebPage> textSearch = new GoogleTextSearch(
834+
initializer: new() { ApiKey = "ApiKey", HttpClientFactory = this._clientFactory },
835+
searchEngineId: "SearchEngineId",
836+
options: new GoogleTextSearchOptions { InterfaceLanguage = "fr" });
837+
838+
// Act
839+
var searchOptions = new TextSearchOptions<GoogleWebPage> { Top = 4, Skip = 0 };
840+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", searchOptions);
841+
842+
// Assert
843+
var requestUris = this._messageHandlerStub.RequestUris;
844+
Assert.Single(requestUris);
845+
Assert.Contains("hl=fr", requestUris[0]!.AbsoluteUri);
846+
847+
// Clean up
848+
((IDisposable)textSearch).Dispose();
849+
}
850+
851+
[Fact]
852+
public async Task DefaultParametersCoexistWithLinqFiltersAsync()
853+
{
854+
// Arrange
855+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
856+
ITextSearch<GoogleWebPage> textSearch = new GoogleTextSearch(
857+
initializer: new() { ApiKey = "ApiKey", HttpClientFactory = this._clientFactory },
858+
searchEngineId: "SearchEngineId",
859+
options: new GoogleTextSearchOptions
860+
{
861+
GeoLocation = "us",
862+
InterfaceLanguage = "en"
863+
});
864+
865+
// Act: LINQ filter for site search + default search params
866+
var searchOptions = new TextSearchOptions<GoogleWebPage>
867+
{
868+
Top = 4,
869+
Skip = 0,
870+
Filter = page => page.DisplayLink == "microsoft.com"
871+
};
872+
KernelSearchResults<string> result = await textSearch.SearchAsync("test query", searchOptions);
873+
874+
// Assert: Both LINQ filter and defaults should appear
875+
var requestUris = this._messageHandlerStub.RequestUris;
876+
Assert.Single(requestUris);
877+
string uri = requestUris[0]!.AbsoluteUri;
878+
Assert.Contains("siteSearch=microsoft.com", uri);
879+
Assert.Contains("gl=us", uri);
880+
Assert.Contains("hl=en", uri);
881+
882+
// Clean up
883+
((IDisposable)textSearch).Dispose();
884+
}
885+
886+
#endregion
887+
736888
/// <inheritdoc/>
737889
public void Dispose()
738890
{

0 commit comments

Comments
 (0)