// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Google.Apis.Http;
using Microsoft.SemanticKernel.Data;
using Microsoft.SemanticKernel.Plugins.Web.Google;
namespace Search;
///
/// This example shows how to create and use a .
///
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.
///
[Fact]
public async Task UsingGoogleTextSearchAsync()
{
// Create an ITextSearch instance using Google search
var textSearch = new GoogleTextSearch(
initializer: new() { ApiKey = TestConfiguration.Google.ApiKey, HttpClientFactory = new CustomHttpClientFactory(this.Output) },
searchEngineId: TestConfiguration.Google.SearchEngineId);
var query = "What is the Semantic Kernel?";
// Search and return results as string items
KernelSearchResults stringResults = await textSearch.SearchAsync(query, new TextSearchOptions { Top = 4, Skip = 0 });
Console.WriteLine("——— String Results ———\n");
await foreach (string result in stringResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('—', HorizontalRuleLength));
}
// Search and return results as TextSearchResult items
KernelSearchResults textResults = await textSearch.GetTextSearchResultsAsync(query, new TextSearchOptions { Top = 4, Skip = 4 });
Console.WriteLine("\n——— Text Search Results ———\n");
await foreach (TextSearchResult result in textResults.Results)
{
Console.WriteLine($"Name: {result.Name}");
Console.WriteLine($"Value: {result.Value}");
Console.WriteLine($"Link: {result.Link}");
Console.WriteLine(new string('—', HorizontalRuleLength));
}
// Search and return results as Google.Apis.CustomSearchAPI.v1.Data.Result items
KernelSearchResults fullResults = await textSearch.GetSearchResultsAsync(query, new TextSearchOptions { Top = 4, Skip = 8 });
Console.WriteLine("\n——— Google Web Page Results ———\n");
await foreach (Google.Apis.CustomSearchAPI.v1.Data.Result result in fullResults.Results)
{
Console.WriteLine($"Title: {result.Title}");
Console.WriteLine($"Snippet: {result.Snippet}");
Console.WriteLine($"Link: {result.Link}");
Console.WriteLine($"DisplayLink: {result.DisplayLink}");
Console.WriteLine($"Kind: {result.Kind}");
Console.WriteLine(new string('—', HorizontalRuleLength));
}
}
///
/// Show how to create a with a custom mapper and use it to perform a text search.
///
[Fact]
public async Task UsingGoogleTextSearchWithACustomMapperAsync()
{
// Create an ITextSearch instance using Google search
var textSearch = new GoogleTextSearch(
searchEngineId: TestConfiguration.Google.SearchEngineId,
apiKey: TestConfiguration.Google.ApiKey,
options: new() { StringMapper = new TestTextSearchStringMapper() });
var query = "What is the Semantic Kernel?";
// Search with TextSearchResult textResult type
KernelSearchResults stringResults = await textSearch.SearchAsync(query, new TextSearchOptions { Top = 2, Skip = 0 });
Console.WriteLine("--- Serialized JSON Results ---");
await foreach (string result in stringResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('-', HorizontalRuleLength));
}
}
///
/// Show how to create a with a custom mapper and use it to perform a text search.
///
[Fact]
public async Task UsingGoogleTextSearchWithASiteSearchFilterAsync()
{
// Create an ITextSearch instance using Google search
var textSearch = new GoogleTextSearch(
initializer: new() { ApiKey = TestConfiguration.Google.ApiKey, HttpClientFactory = new CustomHttpClientFactory(this.Output) },
searchEngineId: TestConfiguration.Google.SearchEngineId);
var query = "What is the Semantic Kernel?";
// Search with TextSearchResult textResult type
TextSearchOptions searchOptions = new() { Top = 4, Skip = 0, Filter = new TextSearchFilter().Equality("siteSearch", "devblogs.microsoft.com") };
KernelSearchResults textResults = await textSearch.GetTextSearchResultsAsync(query, searchOptions);
Console.WriteLine("--- Microsoft Developer Blogs Results ---");
await foreach (TextSearchResult result in textResults.Results)
{
Console.WriteLine(result.Link);
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.
///
[Fact]
public async Task UsingGoogleTextSearchWithEnhancedLinqFilteringAsync()
{
// Create an ITextSearch instance using Google search
var textSearch = new GoogleTextSearch(
initializer: new() { ApiKey = TestConfiguration.Google.ApiKey, HttpClientFactory = new CustomHttpClientFactory(this.Output) },
searchEngineId: TestConfiguration.Google.SearchEngineId);
var query = "Semantic Kernel AI";
// Example 1: Simple equality filtering
Console.WriteLine("——— Example 1: Equality Filter (DisplayLink) ———\n");
var equalityOptions = new TextSearchOptions
{
Top = 2,
Skip = 0,
Filter = page => page.DisplayLink == "microsoft.com"
};
var equalityResults = await textSearch.SearchAsync(query, equalityOptions);
await foreach (string result in equalityResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('—', HorizontalRuleLength));
}
// Example 2: Contains filtering
Console.WriteLine("\n——— Example 2: Contains Filter (Title) ———\n");
var containsOptions = new TextSearchOptions
{
Top = 2,
Skip = 0,
Filter = page => page.Title != null && page.Title.Contains("AI")
};
var containsResults = await textSearch.SearchAsync(query, containsOptions);
await foreach (string result in containsResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('—', HorizontalRuleLength));
}
// Example 3: NOT Contains filtering (exclusion)
Console.WriteLine("\n——— Example 3: NOT Contains Filter (Exclude 'deprecated') ———\n");
var notContainsOptions = new TextSearchOptions
{
Top = 2,
Skip = 0,
Filter = page => page.Title != null && !page.Title.Contains("deprecated")
};
var notContainsResults = await textSearch.SearchAsync(query, notContainsOptions);
await foreach (string result in notContainsResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('—', HorizontalRuleLength));
}
// Example 4: FileFormat filtering
Console.WriteLine("\n——— Example 4: FileFormat Filter (PDF files) ———\n");
var fileFormatOptions = new TextSearchOptions
{
Top = 2,
Skip = 0,
Filter = page => page.FileFormat == "pdf"
};
var fileFormatResults = await textSearch.SearchAsync(query, fileFormatOptions);
await foreach (string result in fileFormatResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('—', HorizontalRuleLength));
}
// Example 5: Compound AND filtering (multiple conditions)
Console.WriteLine("\n——— Example 5: Compound AND Filter (Title + Site) ———\n");
var compoundOptions = new TextSearchOptions
{
Top = 2,
Skip = 0,
Filter = page => page.Title != null && page.Title.Contains("Semantic") &&
page.DisplayLink != null && page.DisplayLink.Contains("microsoft")
};
var compoundResults = await textSearch.SearchAsync(query, compoundOptions);
await foreach (string result in compoundResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('—', HorizontalRuleLength));
}
// Example 6: Complex compound filtering (equality + contains + exclusion)
Console.WriteLine("\n——— Example 6: Complex Compound Filter (FileFormat + Contains + NOT Contains) ———\n");
var complexOptions = new TextSearchOptions
{
Top = 2,
Skip = 0,
Filter = page => page.FileFormat == "pdf" &&
page.Title != null && page.Title.Contains("AI") &&
page.Snippet != null && !page.Snippet.Contains("deprecated")
};
var complexResults = await textSearch.SearchAsync(query, complexOptions);
await foreach (string result in complexResults.Results)
{
Console.WriteLine(result);
Console.WriteLine(new string('—', HorizontalRuleLength));
}
}
#region private
private const int HorizontalRuleLength = 80;
///
/// Test mapper which converts an arbitrary search result to a string using JSON serialization.
///
private sealed class TestTextSearchStringMapper : ITextSearchStringMapper
{
///
public string MapFromResultToString(object result)
{
return JsonSerializer.Serialize(result);
}
}
///
/// Implementation of which logs HTTP responses.
///
private sealed class LoggingConfigurableMessageHandler(HttpMessageHandler innerHandler, ITestOutputHelper output) : ConfigurableMessageHandler(innerHandler)
{
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true };
private readonly ITestOutputHelper _output = output;
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Log the request details
if (request.Content is not null)
{
var content = await request.Content.ReadAsStringAsync(cancellationToken);
this._output.WriteLine("=== REQUEST ===");
try
{
string formattedContent = JsonSerializer.Serialize(JsonElement.Parse(content), s_jsonSerializerOptions);
this._output.WriteLine(formattedContent);
}
catch (JsonException)
{
this._output.WriteLine(content);
}
this._output.WriteLine(string.Empty);
}
// Call the next handler in the pipeline
var response = await base.SendAsync(request, cancellationToken);
if (response.Content is not null)
{
// Log the response details
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
this._output.WriteLine("=== RESPONSE ===");
this._output.WriteLine(responseContent);
this._output.WriteLine(string.Empty);
}
return response;
}
}
///
/// Implementation of which uses the .
///
private sealed class CustomHttpClientFactory(ITestOutputHelper output) : Google.Apis.Http.IHttpClientFactory
{
private readonly ITestOutputHelper _output = output;
public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args)
{
ConfigurableMessageHandler messageHandler = new LoggingConfigurableMessageHandler(new HttpClientHandler(), this._output);
var configurableHttpClient = new ConfigurableHttpClient(messageHandler);
return configurableHttpClient;
}
}
#endregion
}