Skip to content

Commit 53995c8

Browse files
.Net: Add unit tests for Text Search AOT enhancements (#10143)
### Motivation and Context Closes #9342 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [ ] 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 😄 --------- Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
1 parent 9a5e2db commit 53995c8

8 files changed

Lines changed: 212 additions & 6 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json.Serialization;
4+
using Microsoft.SemanticKernel.Data;
5+
using SemanticKernel.AotTests.Plugins;
6+
7+
namespace SemanticKernel.AotTests.JsonSerializerContexts;
8+
9+
[JsonSerializable(typeof(CustomResult))]
10+
[JsonSerializable(typeof(int))]
11+
[JsonSerializable(typeof(KernelSearchResults<string>))]
12+
[JsonSerializable(typeof(KernelSearchResults<TextSearchResult>))]
13+
[JsonSerializable(typeof(KernelSearchResults<object>))]
14+
internal sealed partial class CustomResultJsonSerializerContext : JsonSerializerContext
15+
{
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
namespace SemanticKernel.AotTests.Plugins;
4+
5+
internal sealed class CustomResult
6+
{
7+
public string Value { get; set; }
8+
9+
public CustomResult(string value)
10+
{
11+
this.Value = value;
12+
}
13+
}

dotnet/src/SemanticKernel.AotTests/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ private static async Task<int> Main(string[] args)
5959

6060
// Tests for text search
6161
VectorStoreTextSearchTests.GetTextSearchResultsAsync,
62+
VectorStoreTextSearchTests.AddVectorStoreTextSearch,
63+
64+
TextSearchExtensionsTests.CreateWithSearch,
65+
TextSearchExtensionsTests.CreateWithGetTextSearchResults,
66+
TextSearchExtensionsTests.CreateWithGetSearchResults,
6267
];
6368

6469
private static async Task<bool> RunUnitTestsAsync(IEnumerable<Func<Task>> functionsToRun)

dotnet/src/SemanticKernel.AotTests/SemanticKernel.AotTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<PackageReference Include="Microsoft.Extensions.Configuration" />
1919
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
2020
<PackageReference Include="MSTest.TestFramework" />
21+
<PackageReference Include="System.Linq.Async" />
2122
</ItemGroup>
2223

2324
<ItemGroup>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.SemanticKernel.Data;
4+
5+
namespace SemanticKernel.AotTests.UnitTests.Search;
6+
7+
internal sealed class MockTextSearch : ITextSearch
8+
{
9+
private readonly KernelSearchResults<object>? _objectResults;
10+
private readonly KernelSearchResults<TextSearchResult>? _textSearchResults;
11+
private readonly KernelSearchResults<string>? _stringResults;
12+
13+
public MockTextSearch(KernelSearchResults<object>? objectResults)
14+
{
15+
this._objectResults = objectResults;
16+
}
17+
18+
public MockTextSearch(KernelSearchResults<TextSearchResult>? textSearchResults)
19+
{
20+
this._textSearchResults = textSearchResults;
21+
}
22+
23+
public MockTextSearch(KernelSearchResults<string>? stringResults)
24+
{
25+
this._stringResults = stringResults;
26+
}
27+
28+
public Task<KernelSearchResults<object>> GetSearchResultsAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default)
29+
{
30+
return Task.FromResult(this._objectResults!);
31+
}
32+
33+
public Task<KernelSearchResults<TextSearchResult>> GetTextSearchResultsAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default)
34+
{
35+
return Task.FromResult(this._textSearchResults!);
36+
}
37+
38+
public Task<KernelSearchResults<string>> SearchAsync(string query, TextSearchOptions? searchOptions = null, CancellationToken cancellationToken = default)
39+
{
40+
return Task.FromResult(this._stringResults!);
41+
}
42+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json;
4+
using Microsoft.SemanticKernel;
5+
using Microsoft.SemanticKernel.Data;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
using SemanticKernel.AotTests.JsonSerializerContexts;
8+
using SemanticKernel.AotTests.Plugins;
9+
10+
namespace SemanticKernel.AotTests.UnitTests.Search;
11+
12+
internal sealed class TextSearchExtensionsTests
13+
{
14+
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
15+
{
16+
TypeInfoResolverChain = { CustomResultJsonSerializerContext.Default }
17+
};
18+
19+
public static async Task CreateWithSearch()
20+
{
21+
// Arrange
22+
var testData = new List<string> { "test-value" };
23+
KernelSearchResults<string> results = new(testData.ToAsyncEnumerable());
24+
ITextSearch textSearch = new MockTextSearch(results);
25+
26+
// Act
27+
var plugin = textSearch.CreateWithSearch("SearchPlugin", s_jsonSerializerOptions);
28+
29+
// Assert
30+
await AssertSearchFunctionSchemaAndInvocationResult<string>(plugin["Search"], testData[0]);
31+
}
32+
33+
public static async Task CreateWithGetTextSearchResults()
34+
{
35+
// Arrange
36+
var testData = new List<TextSearchResult> { new("test-value") };
37+
KernelSearchResults<TextSearchResult> results = new(testData.ToAsyncEnumerable());
38+
ITextSearch textSearch = new MockTextSearch(results);
39+
40+
// Act
41+
var plugin = textSearch.CreateWithGetTextSearchResults("SearchPlugin", s_jsonSerializerOptions);
42+
43+
// Assert
44+
await AssertSearchFunctionSchemaAndInvocationResult<TextSearchResult>(plugin["GetTextSearchResults"], testData[0]);
45+
}
46+
47+
public static async Task CreateWithGetSearchResults()
48+
{
49+
// Arrange
50+
var testData = new List<CustomResult> { new("test-value") };
51+
KernelSearchResults<object> results = new(testData.ToAsyncEnumerable());
52+
ITextSearch textSearch = new MockTextSearch(results);
53+
54+
// Act
55+
var plugin = textSearch.CreateWithGetSearchResults("SearchPlugin", s_jsonSerializerOptions);
56+
57+
// Assert
58+
await AssertSearchFunctionSchemaAndInvocationResult<object>(plugin["GetSearchResults"], testData[0]);
59+
}
60+
61+
#region assert
62+
internal static async Task AssertSearchFunctionSchemaAndInvocationResult<T>(KernelFunction function, T expectedResult)
63+
{
64+
// Assert input parameter schema
65+
AssertSearchFunctionMetadata<T>(function.Metadata);
66+
67+
// Assert the function result
68+
FunctionResult functionResult = await function.InvokeAsync(new(), new() { ["query"] = "Mock Query" });
69+
70+
var result = functionResult.GetValue<List<T>>()!;
71+
Assert.AreEqual(1, result.Count);
72+
Assert.AreEqual(expectedResult, result[0]);
73+
}
74+
75+
internal static void AssertSearchFunctionMetadata<T>(KernelFunctionMetadata metadata)
76+
{
77+
// Assert input parameter schema
78+
Assert.AreEqual(3, metadata.Parameters.Count);
79+
Assert.AreEqual("{\"description\":\"What to search for\",\"type\":\"string\"}", metadata.Parameters[0].Schema!.ToString());
80+
Assert.AreEqual("{\"description\":\"Number of results (default value: 2)\",\"type\":\"integer\"}", metadata.Parameters[1].Schema!.ToString());
81+
Assert.AreEqual("{\"description\":\"Number of results to skip (default value: 0)\",\"type\":\"integer\"}", metadata.Parameters[2].Schema!.ToString());
82+
83+
// Assert return type schema
84+
var type = typeof(T).Name;
85+
var expectedSchema = type switch
86+
{
87+
"String" => """{"type":"object","properties":{"TotalCount":{"type":["integer","null"],"default":null},"Metadata":{"type":["object","null"],"default":null},"Results":{"type":"array","items":{"type":"string"}}},"required":["Results"]}""",
88+
"TextSearchResult" => """{"type":"object","properties":{"TotalCount":{"type":["integer","null"],"default":null},"Metadata":{"type":["object","null"],"default":null},"Results":{"type":"array","items":{"type":"object","properties":{"Name":{"type":["string","null"]},"Link":{"type":["string","null"]},"Value":{"type":"string"}},"required":["Value"]}}},"required":["Results"]}""",
89+
_ => """{"type":"object","properties":{"TotalCount":{"type":["integer","null"],"default":null},"Metadata":{"type":["object","null"],"default":null},"Results":{"type":"array","items":{"type":"object","properties":{"Name":{"type":["string","null"]},"Link":{"type":["string","null"]},"Value":{"type":"string"}},"required":["Value"]}}},"required":["Results"]}"""
90+
};
91+
Assert.AreEqual(expectedSchema, metadata.ReturnParameter.Schema!.ToString());
92+
}
93+
#endregion
94+
}

dotnet/src/SemanticKernel.AotTests/UnitTests/Search/VectorStoreTextSearchTests.cs

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

3+
using Microsoft.Extensions.DependencyInjection;
34
using Microsoft.Extensions.VectorData;
5+
using Microsoft.SemanticKernel;
46
using Microsoft.SemanticKernel.Data;
57
using Microsoft.VisualStudio.TestTools.UnitTesting;
68

@@ -35,6 +37,39 @@ public static async Task GetTextSearchResultsAsync()
3537
Assert.AreEqual("test-link", results[0].Link);
3638
}
3739

40+
public static async Task AddVectorStoreTextSearch()
41+
{
42+
// Arrange
43+
var testData = new List<VectorSearchResult<DataModel>>
44+
{
45+
new(new DataModel { Key = "test-name", Text = "test-result", Link = "test-link" }, 0.5)
46+
};
47+
var vectorizableTextSearch = new MockVectorizableTextSearch<DataModel>(testData);
48+
var serviceCollection = new ServiceCollection();
49+
serviceCollection.AddSingleton<IVectorizableTextSearch<DataModel>>(vectorizableTextSearch);
50+
51+
// Act
52+
serviceCollection.AddVectorStoreTextSearch<DataModel>();
53+
var textSearch = serviceCollection.BuildServiceProvider().GetService<VectorStoreTextSearch<DataModel>>();
54+
Assert.IsNotNull(textSearch);
55+
56+
// Assert
57+
KernelSearchResults<TextSearchResult> searchResults = await textSearch.GetTextSearchResultsAsync("query");
58+
59+
List<TextSearchResult> results = [];
60+
61+
await foreach (TextSearchResult result in searchResults.Results)
62+
{
63+
results.Add(result);
64+
}
65+
66+
// Assert
67+
Assert.AreEqual(1, results.Count);
68+
Assert.AreEqual("test-name", results[0].Name);
69+
Assert.AreEqual("test-result", results[0].Value);
70+
Assert.AreEqual("test-link", results[0].Link);
71+
}
72+
3873
private sealed class DataModel
3974
{
4075
[TextSearchResultName]

dotnet/src/SemanticKernel.Core/Data/TextSearch/TextSearchExtensions.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,9 @@ private static KernelFunctionFromMethodOptions DefaultGetSearchResultsMethodOpti
519519
private static IEnumerable<KernelParameterMetadata> CreateDefaultKernelParameterMetadata(JsonSerializerOptions jsonSerializerOptions)
520520
{
521521
return [
522-
new KernelParameterMetadata("query", jsonSerializerOptions) { Description = "What to search for", IsRequired = true },
523-
new KernelParameterMetadata("count", jsonSerializerOptions) { Description = "Number of results", IsRequired = false, DefaultValue = 2 },
524-
new KernelParameterMetadata("skip", jsonSerializerOptions) { Description = "Number of results to skip", IsRequired = false, DefaultValue = 0 },
522+
new KernelParameterMetadata("query", jsonSerializerOptions) { Description = "What to search for", ParameterType = typeof(string), IsRequired = true },
523+
new KernelParameterMetadata("count", jsonSerializerOptions) { Description = "Number of results", ParameterType = typeof(int), IsRequired = false, DefaultValue = 2 },
524+
new KernelParameterMetadata("skip", jsonSerializerOptions) { Description = "Number of results to skip", ParameterType = typeof(int), IsRequired = false, DefaultValue = 0 },
525525
];
526526
}
527527

@@ -530,9 +530,9 @@ private static IEnumerable<KernelParameterMetadata> CreateDefaultKernelParameter
530530
private static IEnumerable<KernelParameterMetadata> GetDefaultKernelParameterMetadata()
531531
{
532532
return s_kernelParameterMetadata ??= [
533-
new KernelParameterMetadata("query") { Description = "What to search for", IsRequired = true },
534-
new KernelParameterMetadata("count") { Description = "Number of results", IsRequired = false, DefaultValue = 2 },
535-
new KernelParameterMetadata("skip") { Description = "Number of results to skip", IsRequired = false, DefaultValue = 0 },
533+
new KernelParameterMetadata("query") { Description = "What to search for", ParameterType = typeof(string), IsRequired = true },
534+
new KernelParameterMetadata("count") { Description = "Number of results", ParameterType = typeof(int), IsRequired = false, DefaultValue = 2 },
535+
new KernelParameterMetadata("skip") { Description = "Number of results to skip", ParameterType = typeof(int), IsRequired = false, DefaultValue = 0 },
536536
];
537537
}
538538

0 commit comments

Comments
 (0)