From d6b60197e6cf90d4a20c59b96457e7c3fcaad166 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:08:57 +0100 Subject: [PATCH] .Net: Add filtering support for VectoreStoreTextSearch (#8947) ### Motivation and Context Related to #6725 ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../Data/VectorSearch/VectorSearchFilter.cs | 18 +++++++ .../Data/TextSearch/VectorStoreTextSearch.cs | 2 +- .../Data/VectorStoreTextSearchTests.cs | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchFilter.cs b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchFilter.cs index 030261dd8567..bddc6487fc4c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchFilter.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/VectorSearch/VectorSearchFilter.cs @@ -28,6 +28,24 @@ public sealed class VectorSearchFilter /// public IEnumerable FilterClauses => this._filterClauses; + /// + /// Create an instance of + /// + public VectorSearchFilter() + { + } + + /// + /// Create an instance of with the provided s. + /// The instances to use + /// + internal VectorSearchFilter(IEnumerable filterClauses) + { + Verify.NotNull(filterClauses, nameof(filterClauses)); + + this._filterClauses.AddRange(filterClauses); + } + /// /// Add an equal to clause to the filter options. /// diff --git a/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs b/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs index d720f455eec7..979a7aac3d92 100644 --- a/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs +++ b/dotnet/src/SemanticKernel.Core/Data/TextSearch/VectorStoreTextSearch.cs @@ -117,7 +117,7 @@ private async Task>> ExecuteVectorS searchOptions ??= new TextSearchOptions(); var vectorSearchOptions = new VectorSearchOptions { - Filter = null, + Filter = searchOptions.Filter?.FilterClauses is not null ? new VectorSearchFilter(searchOptions.Filter.FilterClauses) : null, Offset = searchOptions.Offset, Limit = searchOptions.Count, }; diff --git a/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs b/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs index 9f2fd61c6fc7..36f769582811 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Data/VectorStoreTextSearchTests.cs @@ -124,6 +124,47 @@ public async Task CanGetSearchResultsWithVectorizableTextSearchAsync() Assert.Equal(2, results.Count); } + [Fact] + public async Task CanFilterGetSearchResultsWithVectorizedSearchAsync() + { + // Arrange. + var sut = await CreateVectorStoreTextSearchFromVectorizedSearchAsync(); + TextSearchFilter evenFilter = new(); + evenFilter.Equality("Tag", "Even"); + TextSearchFilter oddFilter = new(); + oddFilter.Equality("Tag", "Odd"); + + // Act. + KernelSearchResults evenSearchResults = await sut.GetSearchResultsAsync("What is the Semantic Kernel?", new() + { + Count = 2, + Offset = 0, + Filter = evenFilter + }); + var evenResults = await ToListAsync(evenSearchResults.Results); + KernelSearchResults oddSearchResults = await sut.GetSearchResultsAsync("What is the Semantic Kernel?", new() + { + Count = 2, + Offset = 0, + Filter = oddFilter + }); + var oddResults = await ToListAsync(oddSearchResults.Results); + + Assert.Equal(2, evenResults.Count); + var result1 = evenResults[0] as DataModel; + Assert.Equal("Even", result1?.Tag); + var result2 = evenResults[1] as DataModel; + Assert.Equal("Even", result2?.Tag); + + Assert.Equal(2, oddResults.Count); + result1 = oddResults[0] as DataModel; + Assert.Equal("Odd", result1?.Tag); + result2 = oddResults[1] as DataModel; + Assert.Equal("Odd", result2?.Tag); + } + + #region private + /// /// Create a from a . /// @@ -185,6 +226,7 @@ private static async Task AddRecordsAsync( { Key = Guid.NewGuid(), Text = $"Record {i}", + Tag = i % 2 == 0 ? "Even" : "Odd", Embedding = await embeddingService.GenerateEmbeddingAsync($"Record {i}") }; await recordCollection.UpsertAsync(dataModel); @@ -274,7 +316,12 @@ private sealed class DataModel [VectorStoreRecordData] public required string Text { get; init; } + [VectorStoreRecordData(IsFilterable = true)] + public required string Tag { get; init; } + [VectorStoreRecordVector(1536)] public ReadOnlyMemory Embedding { get; init; } } + + #endregion }