// 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 }