Skip to content
80 changes: 80 additions & 0 deletions dotnet/samples/Concepts/Search/Bing_TextSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,86 @@ public async Task UsingBingTextSearchWithASiteFilterAsync()
}
}

/// <summary>
/// Show how to use enhanced LINQ filtering with BingTextSearch for type-safe searches.
/// </summary>
[Fact]
public async Task UsingBingTextSearchWithLinqFilteringAsync()
{
// Create a logging handler to output HTTP requests and responses
LoggingHandler handler = new(new HttpClientHandler(), this.Output);
using HttpClient httpClient = new(handler);

// Create an ITextSearch<BingWebPage> instance for type-safe LINQ filtering
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: TestConfiguration.Bing.ApiKey, options: new() { HttpClient = httpClient });

var query = "Semantic Kernel AI";

// Example 1: Filter by language (English only)
Console.WriteLine("——— Example 1: Language Filter (English) ———\n");
var languageOptions = new TextSearchOptions<BingWebPage>
{
Top = 2,
Filter = page => page.Language == "en"
};
var languageResults = await textSearch.SearchAsync(query, languageOptions);
await foreach (string result in languageResults.Results)
{
Console.WriteLine(result);
WriteHorizontalRule();
}

// Example 2: Filter by family-friendly content
Console.WriteLine("\n——— Example 2: Family Friendly Filter ———\n");
var familyFriendlyOptions = new TextSearchOptions<BingWebPage>
{
Top = 2,
Filter = page => page.IsFamilyFriendly == true
};
var familyFriendlyResults = await textSearch.SearchAsync(query, familyFriendlyOptions);
await foreach (string result in familyFriendlyResults.Results)
{
Console.WriteLine(result);
WriteHorizontalRule();
}

// Example 3: Compound AND filtering (language + family-friendly)
Console.WriteLine("\n——— Example 3: Compound Filter (English + Family Friendly) ———\n");
var compoundOptions = new TextSearchOptions<BingWebPage>
{
Top = 2,
Filter = page => page.Language == "en" && page.IsFamilyFriendly == true
};
var compoundResults = await textSearch.GetSearchResultsAsync(query, compoundOptions);
await foreach (BingWebPage page in compoundResults.Results)
{
Console.WriteLine($"Name: {page.Name}");
Console.WriteLine($"Snippet: {page.Snippet}");
Console.WriteLine($"Language: {page.Language}");
Console.WriteLine($"Family Friendly: {page.IsFamilyFriendly}");
WriteHorizontalRule();
}

// Example 4: Complex compound filtering with nullable checks
Console.WriteLine("\n——— Example 4: Complex Compound Filter (Language + Site + Family Friendly) ———\n");
var complexOptions = new TextSearchOptions<BingWebPage>
{
Top = 2,
Filter = page => page.Language == "en" &&
page.IsFamilyFriendly == true &&
page.DisplayUrl != null && page.DisplayUrl.Contains("microsoft")
};
var complexResults = await textSearch.GetSearchResultsAsync(query, complexOptions);
await foreach (BingWebPage page in complexResults.Results)
{
Console.WriteLine($"Name: {page.Name}");
Console.WriteLine($"Display URL: {page.DisplayUrl}");
Console.WriteLine($"Language: {page.Language}");
Console.WriteLine($"Family Friendly: {page.IsFamilyFriendly}");
WriteHorizontalRule();
}
}

#region private
/// <summary>
/// Test mapper which converts an arbitrary search result to a string using JSON serialization.
Expand Down
115 changes: 111 additions & 4 deletions dotnet/samples/Concepts/Search/Google_TextSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public async Task UsingGoogleTextSearchAsync()
var query = "What is the Semantic Kernel?";

// Search and return results as string items
KernelSearchResults<string> stringResults = await textSearch.SearchAsync(query, new() { Top = 4, Skip = 0 });
KernelSearchResults<string> stringResults = await textSearch.SearchAsync(query, new TextSearchOptions { Top = 4, Skip = 0 });
Console.WriteLine("——— String Results ———\n");
await foreach (string result in stringResults.Results)
{
Expand All @@ -35,7 +35,7 @@ public async Task UsingGoogleTextSearchAsync()
}

// Search and return results as TextSearchResult items
KernelSearchResults<TextSearchResult> textResults = await textSearch.GetTextSearchResultsAsync(query, new() { Top = 4, Skip = 4 });
KernelSearchResults<TextSearchResult> 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)
{
Expand All @@ -46,7 +46,7 @@ public async Task UsingGoogleTextSearchAsync()
}

// Search and return results as Google.Apis.CustomSearchAPI.v1.Data.Result items
KernelSearchResults<object> fullResults = await textSearch.GetSearchResultsAsync(query, new() { Top = 4, Skip = 8 });
KernelSearchResults<object> 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)
{
Expand Down Expand Up @@ -74,7 +74,7 @@ public async Task UsingGoogleTextSearchWithACustomMapperAsync()
var query = "What is the Semantic Kernel?";

// Search with TextSearchResult textResult type
KernelSearchResults<string> stringResults = await textSearch.SearchAsync(query, new() { Top = 2, Skip = 0 });
KernelSearchResults<string> stringResults = await textSearch.SearchAsync(query, new TextSearchOptions { Top = 2, Skip = 0 });
Console.WriteLine("--- Serialized JSON Results ---");
await foreach (string result in stringResults.Results)
{
Expand Down Expand Up @@ -107,6 +107,113 @@ public async Task UsingGoogleTextSearchWithASiteSearchFilterAsync()
}
}

/// <summary>
/// Show how to use enhanced LINQ filtering with GoogleTextSearch including Contains, NOT, FileType, and compound AND expressions.
/// </summary>
[Fact]
public async Task UsingGoogleTextSearchWithEnhancedLinqFilteringAsync()
{
// Create an ITextSearch<GoogleWebPage> 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<GoogleWebPage>
{
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<GoogleWebPage>
{
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<GoogleWebPage>
{
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<GoogleWebPage>
{
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<GoogleWebPage>
{
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<GoogleWebPage>
{
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;

Expand Down
80 changes: 80 additions & 0 deletions dotnet/samples/Concepts/Search/Tavily_TextSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,86 @@ public async Task UsingTavilyTextSearchWithAnIncludeDomainFilterAsync()
}
}

/// <summary>
/// Show how to use enhanced LINQ filtering with TavilyTextSearch for type-safe searches with Title.Contains() support.
/// </summary>
[Fact]
public async Task UsingTavilyTextSearchWithLinqFilteringAsync()
{
// Create a logging handler to output HTTP requests and responses
LoggingHandler handler = new(new HttpClientHandler(), this.Output);
using HttpClient httpClient = new(handler);

// Create an ITextSearch<TavilyWebPage> instance for type-safe LINQ filtering
ITextSearch<TavilyWebPage> textSearch = new TavilyTextSearch(apiKey: TestConfiguration.Tavily.ApiKey, options: new() { HttpClient = httpClient });

var query = "Semantic Kernel AI";

// Example 1: Filter results by title content using Contains
Console.WriteLine("——— Example 1: Title Contains Filter ———\n");
var titleContainsOptions = new TextSearchOptions<TavilyWebPage>
{
Top = 2,
Filter = page => page.Title != null && page.Title.Contains("Kernel")
};
var titleResults = await textSearch.SearchAsync(query, titleContainsOptions);
await foreach (string result in titleResults.Results)
{
Console.WriteLine(result);
WriteHorizontalRule();
}

// Example 2: Compound AND filtering (title contains + NOT contains)
Console.WriteLine("\n——— Example 2: Compound Filter (Title Contains + Exclusion) ———\n");
var compoundOptions = new TextSearchOptions<TavilyWebPage>
{
Top = 2,
Filter = page => page.Title != null && page.Title.Contains("AI") &&
page.Content != null && !page.Content.Contains("deprecated")
};
var compoundResults = await textSearch.SearchAsync(query, compoundOptions);
await foreach (string result in compoundResults.Results)
{
Console.WriteLine(result);
WriteHorizontalRule();
}

// Example 3: Get full results with LINQ filtering
Console.WriteLine("\n——— Example 3: Full Results with Title Filter ———\n");
var fullResultsOptions = new TextSearchOptions<TavilyWebPage>
{
Top = 2,
Filter = page => page.Title != null && page.Title.Contains("Semantic")
};
var fullResults = await textSearch.GetSearchResultsAsync(query, fullResultsOptions);
await foreach (TavilyWebPage page in fullResults.Results)
{
Console.WriteLine($"Title: {page.Title}");
Console.WriteLine($"Content: {page.Content}");
Console.WriteLine($"URL: {page.Url}");
Console.WriteLine($"Score: {page.Score}");
WriteHorizontalRule();
}

// Example 4: Complex compound filtering with multiple conditions
Console.WriteLine("\n——— Example 4: Complex Compound Filter (Title + Content + URL) ———\n");
var complexOptions = new TextSearchOptions<TavilyWebPage>
{
Top = 2,
Filter = page => page.Title != null && page.Title.Contains("Kernel") &&
page.Content != null && page.Content.Contains("AI") &&
page.Url != null && page.Url.ToString().Contains("microsoft")
};
var complexResults = await textSearch.GetSearchResultsAsync(query, complexOptions);
await foreach (TavilyWebPage page in complexResults.Results)
{
Console.WriteLine($"Title: {page.Title}");
Console.WriteLine($"URL: {page.Url}");
Console.WriteLine($"Score: {page.Score}");
WriteHorizontalRule();
}
}

#region private
/// <summary>
/// Test mapper which converts an arbitrary search result to a string using JSON serialization.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,24 @@ namespace GettingStartedWithTextSearch;
/// </summary>
public class InMemoryVectorStoreFixture : IAsyncLifetime
{
/// <summary>
/// Gets the embedding generator used for creating vector embeddings.
/// </summary>
public IEmbeddingGenerator<string, Embedding<float>> EmbeddingGenerator { get; private set; }

/// <summary>
/// Gets the in-memory vector store instance.
/// </summary>
public InMemoryVectorStore InMemoryVectorStore { get; private set; }

/// <summary>
/// Gets the vector store record collection for data models.
/// </summary>
public VectorStoreCollection<Guid, DataModel> VectorStoreRecordCollection { get; private set; }

/// <summary>
/// Gets the name of the collection used for storing records.
/// </summary>
public string CollectionName => "records";

/// <summary>
Expand Down Expand Up @@ -138,21 +150,36 @@ private async Task<VectorStoreCollection<TKey, TRecord>> CreateCollectionFromLis
/// </remarks>
public sealed class DataModel
{
/// <summary>
/// Gets or sets the unique identifier for this record.
/// </summary>
[VectorStoreKey]
[TextSearchResultName]
public Guid Key { get; init; }

/// <summary>
/// Gets or sets the text content of this record.
/// </summary>
[VectorStoreData]
[TextSearchResultValue]
public string Text { get; init; }

/// <summary>
/// Gets or sets the link associated with this record.
/// </summary>
[VectorStoreData]
[TextSearchResultLink]
public string Link { get; init; }

/// <summary>
/// Gets or sets the tag for categorizing this record.
/// </summary>
[VectorStoreData(IsIndexed = true)]
public required string Tag { get; init; }

/// <summary>
/// Gets the embedding representation of the text content.
/// </summary>
[VectorStoreVector(1536)]
public string Embedding => Text;
}
Expand Down
Loading
Loading