chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,6 @@
# Suppressing errors for Test projects under dotnet folder
[*.cs]
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>SemanticKernel.Connectors.AzureAISearch.UnitTests</AssemblyName>
<RootNamespace>SemanticKernel.Connectors.AzureAISearch.UnitTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);SKEXP0001</NoWarn>
<NoWarn>$(NoWarn);MEVD9001</NoWarn> <!-- Experimental MEVD connector-facing APIs -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/AssertExtensions.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\VectorData\AzureAISearch\AzureAISearch.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,222 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Azure.Search.Documents.Indexes.Models;
using Microsoft.Extensions.VectorData;
using Microsoft.Extensions.VectorData.ProviderServices;
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
using Xunit;
namespace SemanticKernel.Connectors.AzureAISearch.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AzureAISearchCollectionCreateMapping"/> class.
/// </summary>
public class AzureAISearchCollectionCreateMappingTests
{
[Fact]
public void MapKeyFieldCreatesSearchableField()
{
// Arrange
var keyProperty = new KeyPropertyModel("testkey", typeof(string)) { StorageName = "test_key" };
// Act
var result = AzureAISearchCollectionCreateMapping.MapKeyField(keyProperty);
// Assert
Assert.NotNull(result);
Assert.Equal("test_key", result.Name);
Assert.True(result.IsKey);
Assert.True(result.IsFilterable);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MapFilterableStringDataFieldCreatesSimpleField(bool isFilterable)
{
// Arrange
var dataProperty = new DataPropertyModel("testdata", typeof(string))
{
IsIndexed = isFilterable,
StorageName = "test_data"
};
// Act
var result = AzureAISearchCollectionCreateMapping.MapDataField(dataProperty);
// Assert
Assert.NotNull(result);
Assert.IsType<SimpleField>(result);
Assert.Equal("test_data", result.Name);
Assert.False(result.IsKey);
Assert.Equal(isFilterable, result.IsFilterable);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MapFullTextSearchableStringDataFieldCreatesSearchableField(bool isFilterable)
{
// Arrange
var dataProperty = new DataPropertyModel("testdata", typeof(string))
{
IsIndexed = isFilterable,
IsFullTextIndexed = true,
StorageName = "test_data"
};
// Act
var result = AzureAISearchCollectionCreateMapping.MapDataField(dataProperty);
// Assert
Assert.NotNull(result);
Assert.IsType<SearchableField>(result);
Assert.Equal("test_data", result.Name);
Assert.False(result.IsKey);
Assert.Equal(isFilterable, result.IsFilterable);
}
[Fact]
public void MapFullTextSearchableStringDataFieldThrowsForInvalidType()
{
// Arrange
var dataProperty = new DataPropertyModel("testdata", typeof(int))
{
IsFullTextIndexed = true,
StorageName = "test_data"
};
// Act & Assert
Assert.Throws<InvalidOperationException>(() => AzureAISearchCollectionCreateMapping.MapDataField(dataProperty));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MapDataFieldCreatesSimpleField(bool isFilterable)
{
// Arrange
var dataProperty = new DataPropertyModel("testdata", typeof(int))
{
IsIndexed = isFilterable,
StorageName = "test_data"
};
// Act
var result = AzureAISearchCollectionCreateMapping.MapDataField(dataProperty);
// Assert
Assert.NotNull(result);
Assert.IsType<SimpleField>(result);
Assert.Equal("test_data", result.Name);
Assert.Equal(SearchFieldDataType.Int32, result.Type);
Assert.False(result.IsKey);
Assert.Equal(isFilterable, result.IsFilterable);
}
[Fact]
public void MapVectorFieldCreatesVectorSearchField()
{
// Arrange
var vectorProperty = new VectorPropertyModel("testvector", typeof(ReadOnlyMemory<float>))
{
Dimensions = 10,
IndexKind = IndexKind.Flat,
DistanceFunction = DistanceFunction.DotProductSimilarity,
StorageName = "test_vector"
};
// Act
var (vectorSearchField, algorithmConfiguration, vectorSearchProfile) = AzureAISearchCollectionCreateMapping.MapVectorField(vectorProperty);
// Assert
Assert.NotNull(vectorSearchField);
Assert.NotNull(algorithmConfiguration);
Assert.NotNull(vectorSearchProfile);
Assert.Equal("test_vector", vectorSearchField.Name);
Assert.Equal(vectorProperty.Dimensions, vectorSearchField.VectorSearchDimensions);
Assert.Equal("test_vectorAlgoConfig", algorithmConfiguration.Name);
Assert.IsType<ExhaustiveKnnAlgorithmConfiguration>(algorithmConfiguration);
var flatConfig = algorithmConfiguration as ExhaustiveKnnAlgorithmConfiguration;
Assert.Equal(VectorSearchAlgorithmMetric.DotProduct, flatConfig!.Parameters.Metric);
Assert.Equal("test_vectorProfile", vectorSearchProfile.Name);
Assert.Equal("test_vectorAlgoConfig", vectorSearchProfile.AlgorithmConfigurationName);
}
[Theory]
[InlineData(IndexKind.Hnsw, typeof(HnswAlgorithmConfiguration))]
[InlineData(IndexKind.Flat, typeof(ExhaustiveKnnAlgorithmConfiguration))]
public void MapVectorFieldCreatesExpectedAlgoConfigTypes(string indexKind, Type algoConfigType)
{
// Arrange
var vectorProperty = new VectorPropertyModel("testvector", typeof(ReadOnlyMemory<float>))
{
Dimensions = 10,
IndexKind = indexKind,
DistanceFunction = DistanceFunction.DotProductSimilarity,
StorageName = "test_vector"
};
// Act
var (vectorSearchField, algorithmConfiguration, vectorSearchProfile) = AzureAISearchCollectionCreateMapping.MapVectorField(vectorProperty);
// Assert
Assert.Equal("test_vectorAlgoConfig", algorithmConfiguration.Name);
Assert.Equal(algoConfigType, algorithmConfiguration.GetType());
}
[Fact]
public void MapVectorFieldDefaultsToHsnwAndCosine()
{
// Arrange
var vectorProperty = new VectorPropertyModel("testvector", typeof(ReadOnlyMemory<float>)) { Dimensions = 10 };
// Act
var (vectorSearchField, algorithmConfiguration, vectorSearchProfile) = AzureAISearchCollectionCreateMapping.MapVectorField(vectorProperty);
// Assert
Assert.IsType<HnswAlgorithmConfiguration>(algorithmConfiguration);
var hnswConfig = algorithmConfiguration as HnswAlgorithmConfiguration;
Assert.Equal(VectorSearchAlgorithmMetric.Cosine, hnswConfig!.Parameters.Metric);
}
[Fact]
public void MapVectorFieldThrowsForUnsupportedDistanceFunction()
{
// Arrange
var vectorProperty = new VectorPropertyModel("testvector", typeof(ReadOnlyMemory<float>))
{
Dimensions = 10,
DistanceFunction = DistanceFunction.ManhattanDistance,
};
// Act & Assert
Assert.Throws<NotSupportedException>(() => AzureAISearchCollectionCreateMapping.MapVectorField(vectorProperty));
}
[Theory]
[MemberData(nameof(DataTypeMappingOptions))]
public void GetSDKFieldDataTypeMapsTypesCorrectly(Type propertyType, SearchFieldDataType searchFieldDataType)
{
// Act & Assert
Assert.Equal(searchFieldDataType, AzureAISearchCollectionCreateMapping.GetSDKFieldDataType(propertyType));
}
public static IEnumerable<object[]> DataTypeMappingOptions()
{
yield return new object[] { typeof(string), SearchFieldDataType.String };
yield return new object[] { typeof(bool), SearchFieldDataType.Boolean };
yield return new object[] { typeof(int), SearchFieldDataType.Int32 };
yield return new object[] { typeof(long), SearchFieldDataType.Int64 };
yield return new object[] { typeof(float), SearchFieldDataType.Double };
yield return new object[] { typeof(double), SearchFieldDataType.Double };
yield return new object[] { typeof(DateTimeOffset), SearchFieldDataType.DateTimeOffset };
yield return new object[] { typeof(string[]), SearchFieldDataType.Collection(SearchFieldDataType.String) };
yield return new object[] { typeof(List<string>), SearchFieldDataType.Collection(SearchFieldDataType.String) };
}
}
@@ -0,0 +1,600 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using Azure.Search.Documents.Models;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
using Moq;
using Xunit;
namespace SemanticKernel.Connectors.AzureAISearch.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AzureAISearchCollection{TKey, TRecord}"/> class.
/// </summary>
public class AzureAISearchCollectionTests
{
private const string TestCollectionName = "testcollection";
private const string TestRecordKey1 = "testid1";
private const string TestRecordKey2 = "testid2";
private readonly Mock<SearchIndexClient> _searchIndexClientMock;
private readonly Mock<SearchClient> _searchClientMock;
private readonly CancellationToken _testCancellationToken = new(false);
public AzureAISearchCollectionTests()
{
this._searchClientMock = new Mock<SearchClient>(MockBehavior.Strict);
this._searchIndexClientMock = new Mock<SearchIndexClient>(MockBehavior.Strict);
this._searchIndexClientMock.Setup(x => x.GetSearchClient(TestCollectionName)).Returns(this._searchClientMock.Object);
this._searchIndexClientMock.Setup(x => x.ServiceName).Returns("TestService");
}
[Theory]
[InlineData(TestCollectionName, true)]
[InlineData("nonexistentcollection", false)]
public async Task CollectionExistsReturnsCollectionStateAsync(string collectionName, bool expectedExists)
{
this._searchIndexClientMock.Setup(x => x.GetSearchClient(collectionName)).Returns(this._searchClientMock.Object);
// Arrange.
if (expectedExists)
{
this._searchIndexClientMock
.Setup(x => x.GetIndexAsync(collectionName, this._testCancellationToken))
.Returns(Task.FromResult<Response<SearchIndex>?>(null));
}
else
{
this._searchIndexClientMock
.Setup(x => x.GetIndexAsync(collectionName, this._testCancellationToken))
.ThrowsAsync(new RequestFailedException(404, "Index not found"));
}
using var sut = new AzureAISearchCollection<string, MultiPropsModel>(this._searchIndexClientMock.Object, collectionName);
// Act.
var actual = await sut.CollectionExistsAsync(this._testCancellationToken);
// Assert.
Assert.Equal(expectedExists, actual);
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public async Task EnsureCollectionExistsInvokesSDKAsync(bool useDefinition, bool expectedExists)
{
// Arrange.
if (expectedExists)
{
this._searchIndexClientMock
.Setup(x => x.GetIndexAsync(TestCollectionName, this._testCancellationToken))
.Returns(Task.FromResult<Response<SearchIndex>?>(null));
}
else
{
this._searchIndexClientMock
.Setup(x => x.GetIndexAsync(TestCollectionName, this._testCancellationToken))
.ThrowsAsync(new RequestFailedException(404, "Index not found"));
}
this._searchIndexClientMock
.Setup(x => x.CreateIndexAsync(It.IsAny<SearchIndex>(), this._testCancellationToken))
.ReturnsAsync(Response.FromValue(new SearchIndex(TestCollectionName), Mock.Of<Response>()));
using var sut = this.CreateRecordCollection(useDefinition);
// Act.
await sut.EnsureCollectionExistsAsync();
// Assert.
if (expectedExists)
{
this._searchIndexClientMock
.Verify(
x => x.CreateIndexAsync(
It.IsAny<SearchIndex>(),
this._testCancellationToken),
Times.Never);
}
else
{
this._searchIndexClientMock
.Verify(
x => x.CreateIndexAsync(
It.Is<SearchIndex>(si => si.Fields.Count == 5 && si.Name == TestCollectionName && si.VectorSearch.Profiles.Count == 2 && si.VectorSearch.Algorithms.Count == 2),
this._testCancellationToken),
Times.Once);
}
}
[Fact]
public async Task CanDeleteCollectionAsync()
{
// Arrange.
this._searchIndexClientMock
.Setup(x => x.DeleteIndexAsync(TestCollectionName, this._testCancellationToken))
.Returns(Task.FromResult<Response?>(null));
using var sut = this.CreateRecordCollection(false);
// Act.
await sut.EnsureCollectionDeletedAsync(this._testCancellationToken);
// Assert.
this._searchIndexClientMock.Verify(x => x.DeleteIndexAsync(TestCollectionName, this._testCancellationToken), Times.Once);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanGetRecordWithVectorsAsync(bool useDefinition)
{
// Arrange.
this._searchClientMock.Setup(
x => x.GetDocumentAsync<JsonObject>(
TestRecordKey1,
It.Is<GetDocumentOptions>(x => !x.SelectedFields.Any()),
this._testCancellationToken))
.ReturnsAsync(Response.FromValue(CreateJsonObjectModel(TestRecordKey1, true), Mock.Of<Response>()));
using var sut = this.CreateRecordCollection(useDefinition);
// Act.
var actual = await sut.GetAsync(
TestRecordKey1,
new() { IncludeVectors = true },
this._testCancellationToken);
// Assert.
Assert.NotNull(actual);
Assert.Equal(TestRecordKey1, actual.Key);
Assert.Equal("data 1", actual.Data1);
Assert.Equal("data 2", actual.Data2);
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.Vector1!.Value.ToArray());
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.Vector2!.Value.ToArray());
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public async Task CanGetRecordWithoutVectorsAsync(bool useDefinition, bool useCustomJsonSerializerOptions)
{
// Arrange.
var storageObject = JsonSerializer.SerializeToNode(CreateModel(TestRecordKey1, false))!.AsObject();
string[] expectedSelectFields = useCustomJsonSerializerOptions ? ["key", "storage_data1", "data2"] : ["Key", "storage_data1", "Data2"];
this._searchClientMock.Setup(
x => x.GetDocumentAsync<JsonObject>(
TestRecordKey1,
It.IsAny<GetDocumentOptions>(),
this._testCancellationToken))
.ReturnsAsync(Response.FromValue(CreateJsonObjectModel(TestRecordKey1, true, useCustomJsonSerializerOptions), Mock.Of<Response>()));
using var sut = this.CreateRecordCollection(useDefinition, useCustomJsonSerializerOptions);
// Act.
var actual = await sut.GetAsync(
TestRecordKey1,
new() { IncludeVectors = false },
this._testCancellationToken);
// Assert.
Assert.NotNull(actual);
Assert.Equal(TestRecordKey1, actual.Key);
Assert.Equal("data 1", actual.Data1);
Assert.Equal("data 2", actual.Data2);
this._searchClientMock.Verify(
x => x.GetDocumentAsync<JsonObject>(
TestRecordKey1,
It.Is<GetDocumentOptions>(x => x.SelectedFields.SequenceEqual(expectedSelectFields)),
this._testCancellationToken),
Times.Once);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanGetManyRecordsWithVectorsAsync(bool useDefinition)
{
// Arrange.
this._searchClientMock.Setup(
x => x.GetDocumentAsync<JsonObject>(
It.IsAny<string>(),
It.IsAny<GetDocumentOptions>(),
this._testCancellationToken))
.ReturnsAsync((string id, GetDocumentOptions options, CancellationToken cancellationToken) =>
{
return Response.FromValue(CreateJsonObjectModel(id, true), Mock.Of<Response>());
});
using var sut = this.CreateRecordCollection(useDefinition);
// Act.
var actual = await sut.GetAsync(
[TestRecordKey1, TestRecordKey2],
new() { IncludeVectors = true },
this._testCancellationToken).ToListAsync();
// Assert.
Assert.NotNull(actual);
Assert.Equal(2, actual.Count);
Assert.Equal(TestRecordKey1, actual[0].Key);
Assert.Equal(TestRecordKey2, actual[1].Key);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanDeleteRecordAsync(bool useDefinition)
{
// Arrange.
#pragma warning disable Moq1002 // Moq: No matching constructor
var indexDocumentsResultMock = new Mock<IndexDocumentsResult>(MockBehavior.Strict, new List<IndexingResult>());
#pragma warning restore Moq1002 // Moq: No matching constructor
this._searchClientMock.Setup(
x => x.DeleteDocumentsAsync(
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<IndexDocumentsOptions>(),
this._testCancellationToken))
.ReturnsAsync(Response.FromValue(indexDocumentsResultMock.Object, Mock.Of<Response>()));
using var sut = this.CreateRecordCollection(useDefinition);
// Act.
await sut.DeleteAsync(
TestRecordKey1,
cancellationToken: this._testCancellationToken);
// Assert.
this._searchClientMock.Verify(
x => x.DeleteDocumentsAsync(
"Key",
It.Is<IEnumerable<string>>(x => x.Count() == 1 && x.Contains(TestRecordKey1)),
It.IsAny<IndexDocumentsOptions>(),
this._testCancellationToken),
Times.Once);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanDeleteManyRecordsWithVectorsAsync(bool useDefinition)
{
// Arrange.
#pragma warning disable Moq1002 // Moq: No matching constructor
var indexDocumentsResultMock = new Mock<IndexDocumentsResult>(MockBehavior.Strict, new List<IndexingResult>());
#pragma warning restore Moq1002 // Moq: No matching constructor
this._searchClientMock.Setup(
x => x.DeleteDocumentsAsync(
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<IndexDocumentsOptions>(),
this._testCancellationToken))
.ReturnsAsync(Response.FromValue(indexDocumentsResultMock.Object, Mock.Of<Response>()));
using var sut = this.CreateRecordCollection(useDefinition);
// Act.
await sut.DeleteAsync(
[TestRecordKey1, TestRecordKey2],
cancellationToken: this._testCancellationToken);
// Assert.
this._searchClientMock.Verify(
x => x.DeleteDocumentsAsync(
"Key",
It.Is<IEnumerable<string>>(x => x.Count() == 2 && x.Contains(TestRecordKey1) && x.Contains(TestRecordKey2)),
It.IsAny<IndexDocumentsOptions>(),
this._testCancellationToken),
Times.Once);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanUpsertRecordAsync(bool useDefinition)
{
// Arrange upload result object.
#pragma warning disable Moq1002 // Moq: No matching constructor
var indexingResult = new Mock<IndexingResult>(MockBehavior.Strict, TestRecordKey1, true, 200);
var indexingResults = new List<IndexingResult>
{
indexingResult.Object
};
var indexDocumentsResultMock = new Mock<IndexDocumentsResult>(MockBehavior.Strict, indexingResults);
#pragma warning restore Moq1002 // Moq: No matching constructor
// Arrange upload.
this._searchClientMock.Setup(
x => x.UploadDocumentsAsync(
It.IsAny<IEnumerable<JsonObject>>(),
It.IsAny<IndexDocumentsOptions>(),
this._testCancellationToken))
.ReturnsAsync(Response.FromValue(indexDocumentsResultMock.Object, Mock.Of<Response>()));
// Arrange sut.
using var sut = this.CreateRecordCollection(useDefinition);
var model = CreateModel(TestRecordKey1, true);
// Act.
await sut.UpsertAsync(
model,
cancellationToken: this._testCancellationToken);
// Assert.
this._searchClientMock.Verify(
x => x.UploadDocumentsAsync(
It.Is<IEnumerable<JsonObject>>(x => x.Count() == 1 && x.First()["Key"]!.ToString() == TestRecordKey1),
It.Is<IndexDocumentsOptions>(x => x.ThrowOnAnyError == true),
this._testCancellationToken),
Times.Once);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanUpsertManyRecordsAsync(bool useDefinition)
{
// Arrange upload result object.
#pragma warning disable Moq1002 // Moq: No matching constructor
var indexingResult1 = new Mock<IndexingResult>(MockBehavior.Strict, TestRecordKey1, true, 200);
var indexingResult2 = new Mock<IndexingResult>(MockBehavior.Strict, TestRecordKey2, true, 200);
var indexingResults = new List<IndexingResult>
{
indexingResult1.Object,
indexingResult2.Object
};
var indexDocumentsResultMock = new Mock<IndexDocumentsResult>(MockBehavior.Strict, indexingResults);
#pragma warning restore Moq1002 // Moq: No matching constructor
// Arrange upload.
this._searchClientMock.Setup(
x => x.UploadDocumentsAsync(
It.IsAny<IEnumerable<JsonObject>>(),
It.IsAny<IndexDocumentsOptions>(),
this._testCancellationToken))
.ReturnsAsync(Response.FromValue(indexDocumentsResultMock.Object, Mock.Of<Response>()));
// Arrange sut.
using var sut = this.CreateRecordCollection(useDefinition);
var model1 = CreateModel(TestRecordKey1, true);
var model2 = CreateModel(TestRecordKey2, true);
// Act.
await sut.UpsertAsync(
[model1, model2],
cancellationToken: this._testCancellationToken);
// Assert.
this._searchClientMock.Verify(
x => x.UploadDocumentsAsync(
It.Is<IEnumerable<JsonObject>>(x => x.Count() == 2 && x.First()["Key"]!.ToString() == TestRecordKey1 && x.ElementAt(1)["Key"]!.ToString() == TestRecordKey2),
It.Is<IndexDocumentsOptions>(x => x.ThrowOnAnyError == true),
this._testCancellationToken),
Times.Once);
}
/// <summary>
/// Tests that the collection can be created even if the definition and the type do not match.
/// In this case, the expectation is that a custom mapper will be provided to map between the
/// schema as defined by the definition and the different data model.
/// </summary>
[Fact]
public void CanCreateCollectionWithMismatchedDefinitionAndType()
{
// Arrange.
var definition = new VectorStoreCollectionDefinition()
{
Properties =
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("Data1", typeof(string)),
new VectorStoreVectorProperty("Vector1", typeof(ReadOnlyMemory<float>), 4),
]
};
// Act.
using var sut = new AzureAISearchCollection<string, MultiPropsModel>(
this._searchIndexClientMock.Object,
TestCollectionName,
new() { Definition = definition });
}
[Fact]
public async Task CanSearchWithVectorAndFilterAsync()
{
// Arrange.
#pragma warning disable Moq1002 // Could not find a matching constructor for arguments: SearchResults has an internal parameterless constructor.
var searchResultsMock = Mock.Of<SearchResults<JsonObject>>();
#pragma warning restore Moq1002 // Could not find a matching constructor for arguments: SearchResults has an internal parameterless constructor.
this._searchClientMock
.Setup(x => x.SearchAsync<JsonObject>(null, It.IsAny<SearchOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(searchResultsMock, Mock.Of<Response>()));
using var sut = new AzureAISearchCollection<string, MultiPropsModel>(
this._searchIndexClientMock.Object,
TestCollectionName);
// Act.
var searchResults = await sut.SearchAsync(
new ReadOnlyMemory<float>(new float[4]),
top: 5,
new()
{
Skip = 3,
Filter = r => r.Data1 == "Data1FilterValue",
VectorProperty = record => record.Vector1
},
this._testCancellationToken).ToListAsync();
// Assert.
this._searchClientMock.Verify(
x => x.SearchAsync<JsonObject>(
null,
It.Is<SearchOptions>(x =>
x.Filter == "(storage_data1 eq 'Data1FilterValue')" &&
x.Size == 5 &&
x.Skip == 3 &&
x.VectorSearch.Queries.First().GetType() == typeof(VectorizedQuery) &&
x.VectorSearch.Queries.First().Fields.First() == "storage_vector1"),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task CanSearchWithTextAndFilterAsync()
{
// Arrange.
#pragma warning disable Moq1002 // Could not find a matching constructor for arguments: SearchResults has an internal parameterless constructor.
var searchResultsMock = Mock.Of<SearchResults<JsonObject>>();
#pragma warning restore Moq1002 // Could not find a matching constructor for arguments: SearchResults has an internal parameterless constructor.
this._searchClientMock
.Setup(x => x.SearchAsync<JsonObject>(null, It.IsAny<SearchOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(searchResultsMock, Mock.Of<Response>()));
using var sut = new AzureAISearchCollection<string, MultiPropsModel>(
this._searchIndexClientMock.Object,
TestCollectionName);
// Act.
var searchResults = await sut.SearchAsync(
"search string",
top: 5,
new()
{
Skip = 3,
Filter = r => r.Data1 == "Data1FilterValue",
VectorProperty = record => record.Vector1
},
this._testCancellationToken).ToListAsync();
// Assert.
this._searchClientMock.Verify(
x => x.SearchAsync<JsonObject>(
null,
It.Is<SearchOptions>(x =>
x.Filter == "(storage_data1 eq 'Data1FilterValue')" &&
x.Size == 5 &&
x.Skip == 3 &&
x.VectorSearch.Queries.First().GetType() == typeof(VectorizableTextQuery) &&
x.VectorSearch.Queries.First().Fields.First() == "storage_vector1" &&
((VectorizableTextQuery)x.VectorSearch.Queries.First()).Text == "search string"),
It.IsAny<CancellationToken>()),
Times.Once);
}
private AzureAISearchCollection<string, MultiPropsModel> CreateRecordCollection(bool useDefinition, bool useCustomJsonSerializerOptions = false)
{
return new AzureAISearchCollection<string, MultiPropsModel>(
this._searchIndexClientMock.Object,
TestCollectionName,
new()
{
Definition = useDefinition ? this._multiPropsDefinition : null,
JsonSerializerOptions = useCustomJsonSerializerOptions ? this._customJsonSerializerOptions : null
});
}
private static MultiPropsModel CreateModel(string key, bool withVectors)
{
return new MultiPropsModel
{
Key = key,
Data1 = "data 1",
Data2 = "data 2",
Vector1 = withVectors ? new float[] { 1, 2, 3, 4 } : null,
Vector2 = withVectors ? new float[] { 1, 2, 3, 4 } : null,
NotAnnotated = null,
};
}
private static JsonObject CreateJsonObjectModel(string key, bool withVectors, bool useCustomJsonSerializerOptions = false)
{
if (useCustomJsonSerializerOptions)
{
return new JsonObject
{
["key"] = key,
["storage_data1"] = "data 1",
["data2"] = "data 2",
["storage_vector1"] = withVectors ? new JsonArray { 1, 2, 3, 4 } : null,
["vector2"] = withVectors ? new JsonArray { 1, 2, 3, 4 } : null,
["notAnnotated"] = null,
};
}
return new JsonObject
{
["Key"] = key,
["storage_data1"] = "data 1",
["Data2"] = "data 2",
["storage_vector1"] = withVectors ? new JsonArray { 1, 2, 3, 4 } : null,
["Vector2"] = withVectors ? new JsonArray { 1, 2, 3, 4 } : null,
["NotAnnotated"] = null,
};
}
private readonly JsonSerializerOptions _customJsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
private readonly VectorStoreCollectionDefinition _multiPropsDefinition = new()
{
Properties =
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("Data1", typeof(string)),
new VectorStoreDataProperty("Data2", typeof(string)),
new VectorStoreVectorProperty("Vector1", typeof(ReadOnlyMemory<float>), 4),
new VectorStoreVectorProperty("Vector2", typeof(ReadOnlyMemory<float>), 4)
]
};
public sealed class MultiPropsModel
{
[VectorStoreKey]
public string Key { get; set; } = string.Empty;
[JsonPropertyName("storage_data1")]
[VectorStoreData(IsIndexed = true)]
public string Data1 { get; set; } = string.Empty;
[VectorStoreData]
public string Data2 { get; set; } = string.Empty;
[JsonPropertyName("storage_vector1")]
[VectorStoreVector(4)]
public ReadOnlyMemory<float>? Vector1 { get; set; }
[VectorStoreVector(4)]
public ReadOnlyMemory<float>? Vector2 { get; set; }
public string? NotAnnotated { get; set; }
}
}
@@ -0,0 +1,284 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Microsoft.Extensions.VectorData;
using Microsoft.Extensions.VectorData.ProviderServices;
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
using Xunit;
namespace SemanticKernel.Connectors.AzureAISearch.UnitTests;
/// <summary>
/// Tests for the <see cref="AzureAISearchDynamicMapper"/> class.
/// </summary>
public class AzureAISearchDynamicMapperTests
{
private static readonly CollectionModel s_model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreDataProperty("IntDataProp", typeof(int)),
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
new VectorStoreDataProperty("LongDataProp", typeof(long)),
new VectorStoreDataProperty("NullableLongDataProp", typeof(long?)),
new VectorStoreDataProperty("FloatDataProp", typeof(float)),
new VectorStoreDataProperty("NullableFloatDataProp", typeof(float?)),
new VectorStoreDataProperty("DoubleDataProp", typeof(double)),
new VectorStoreDataProperty("NullableDoubleDataProp", typeof(double?)),
new VectorStoreDataProperty("BoolDataProp", typeof(bool)),
new VectorStoreDataProperty("NullableBoolDataProp", typeof(bool?)),
new VectorStoreDataProperty("DateTimeOffsetDataProp", typeof(DateTimeOffset)),
new VectorStoreDataProperty("NullableDateTimeOffsetDataProp", typeof(DateTimeOffset?)),
new VectorStoreDataProperty("TagListDataProp", typeof(string[])),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10),
]);
private static readonly float[] s_vector1 = [1.0f, 2.0f, 3.0f];
private static readonly float[] s_vector2 = [4.0f, 5.0f, 6.0f];
private static readonly string[] s_taglist = ["tag1", "tag2"];
[Fact]
public void MapFromDataToStorageModelMapsAllSupportedTypes()
{
// Arrange
var sut = new AzureAISearchDynamicMapper(s_model, null);
var dataModel = new Dictionary<string, object?>
{
["Key"] = "key",
["StringDataProp"] = "string",
["IntDataProp"] = 1,
["NullableIntDataProp"] = 2,
["LongDataProp"] = 3L,
["NullableLongDataProp"] = 4L,
["FloatDataProp"] = 5.0f,
["NullableFloatDataProp"] = 6.0f,
["DoubleDataProp"] = 7.0,
["NullableDoubleDataProp"] = 8.0,
["BoolDataProp"] = true,
["NullableBoolDataProp"] = false,
["DateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["NullableDateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["TagListDataProp"] = s_taglist,
["FloatVector"] = new ReadOnlyMemory<float>(s_vector1),
["NullableFloatVector"] = new ReadOnlyMemory<float>(s_vector2)
};
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, 0, null);
// Assert
Assert.Equal("key", (string?)storageModel["Key"]);
Assert.Equal("string", (string?)storageModel["StringDataProp"]);
Assert.Equal(1, (int?)storageModel["IntDataProp"]);
Assert.Equal(2, (int?)storageModel["NullableIntDataProp"]);
Assert.Equal(3L, (long?)storageModel["LongDataProp"]);
Assert.Equal(4L, (long?)storageModel["NullableLongDataProp"]);
Assert.Equal(5.0f, (float?)storageModel["FloatDataProp"]);
Assert.Equal(6.0f, (float?)storageModel["NullableFloatDataProp"]);
Assert.Equal(7.0, (double?)storageModel["DoubleDataProp"]);
Assert.Equal(8.0, (double?)storageModel["NullableDoubleDataProp"]);
Assert.Equal(true, (bool?)storageModel["BoolDataProp"]);
Assert.Equal(false, (bool?)storageModel["NullableBoolDataProp"]);
Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), (DateTimeOffset?)storageModel["DateTimeOffsetDataProp"]);
Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), (DateTimeOffset?)storageModel["NullableDateTimeOffsetDataProp"]);
Assert.Equal(s_taglist, storageModel["TagListDataProp"]!.AsArray().Select(x => (string)x!).ToArray());
Assert.Equal(s_vector1, storageModel["FloatVector"]!.AsArray().Select(x => (float)x!).ToArray());
Assert.Equal(s_vector2, storageModel["NullableFloatVector"]!.AsArray().Select(x => (float)x!).ToArray());
}
[Fact]
public void MapFromDataToStorageModelMapsNullValues()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10),
]);
var dataModel = new Dictionary<string, object?>
{
["Key"] = "key",
["StringDataProp"] = null,
["NullableIntDataProp"] = null,
["NullableFloatVector"] = null
};
var sut = new AzureAISearchDynamicMapper(model, null);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, 0, null);
// Assert
Assert.Null(storageModel["StringDataProp"]);
Assert.Null(storageModel["NullableIntDataProp"]);
Assert.Null(storageModel["NullableFloatVector"]);
}
[Fact]
public void MapFromStorageToDataModelMapsAllSupportedTypes()
{
// Arrange
var sut = new AzureAISearchDynamicMapper(s_model, null);
var storageModel = new JsonObject
{
["Key"] = "key",
["StringDataProp"] = "string",
["IntDataProp"] = 1,
["NullableIntDataProp"] = 2,
["LongDataProp"] = 3L,
["NullableLongDataProp"] = 4L,
["FloatDataProp"] = 5.0f,
["NullableFloatDataProp"] = 6.0f,
["DoubleDataProp"] = 7.0,
["NullableDoubleDataProp"] = 8.0,
["BoolDataProp"] = true,
["NullableBoolDataProp"] = false,
["DateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["NullableDateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["TagListDataProp"] = new JsonArray { "tag1", "tag2" },
["FloatVector"] = new JsonArray { 1.0f, 2.0f, 3.0f },
["NullableFloatVector"] = new JsonArray { 4.0f, 5.0f, 6.0f }
};
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.Equal("string", dataModel["StringDataProp"]);
Assert.Equal(1, dataModel["IntDataProp"]);
Assert.Equal(2, dataModel["NullableIntDataProp"]);
Assert.Equal(3L, dataModel["LongDataProp"]);
Assert.Equal(4L, dataModel["NullableLongDataProp"]);
Assert.Equal(5.0f, dataModel["FloatDataProp"]);
Assert.Equal(6.0f, dataModel["NullableFloatDataProp"]);
Assert.Equal(7.0, dataModel["DoubleDataProp"]);
Assert.Equal(8.0, dataModel["NullableDoubleDataProp"]);
Assert.Equal(true, dataModel["BoolDataProp"]);
Assert.Equal(false, dataModel["NullableBoolDataProp"]);
Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), dataModel["DateTimeOffsetDataProp"]);
Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), dataModel["NullableDateTimeOffsetDataProp"]);
Assert.Equal(s_taglist, dataModel["TagListDataProp"]);
Assert.Equal(s_vector1, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
Assert.Equal(s_vector2, ((ReadOnlyMemory<float>)dataModel["NullableFloatVector"]!)!.ToArray());
}
[Fact]
public void MapFromStorageToDataModelMapsNullValues()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10),
]);
var storageModel = new JsonObject
{
["Key"] = "key",
["StringDataProp"] = null,
["NullableIntDataProp"] = null,
["NullableFloatVector"] = null
};
var sut = new AzureAISearchDynamicMapper(model, null);
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.Null(dataModel["StringDataProp"]);
Assert.Null(dataModel["NullableIntDataProp"]);
Assert.Null(dataModel["NullableFloatVector"]);
}
[Fact]
public void MapFromStorageToDataModelThrowsForMissingKey()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10),
]);
var sut = new AzureAISearchDynamicMapper(model, null);
var storageModel = new JsonObject();
// Act
var exception = Assert.Throws<InvalidOperationException>(() => sut.MapFromStorageToDataModel(storageModel, includeVectors: true));
// Assert
Assert.Equal("The key property 'Key' is missing from the record retrieved from storage.", exception.Message);
}
[Fact]
public void MapFromDataToStorageModelSkipsMissingProperties()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
]);
var dataModel = new Dictionary<string, object?> { ["Key"] = "key" };
var sut = new AzureAISearchDynamicMapper(model, null);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, 0, null);
// Assert
Assert.Equal("key", (string?)storageModel["Key"]);
Assert.False(storageModel.ContainsKey("StringDataProp"));
Assert.False(storageModel.ContainsKey("FloatVector"));
}
[Fact]
public void MapFromStorageToDataModelSkipsMissingProperties()
{
// Arrange
var model = BuildModel(
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
]);
var storageModel = new JsonObject
{
["Key"] = "key"
};
var sut = new AzureAISearchDynamicMapper(model, null);
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.False(dataModel.ContainsKey("StringDataProp"));
Assert.False(dataModel.ContainsKey("FloatVector"));
}
private static CollectionModel BuildModel(List<VectorStoreProperty> properties)
=> new AzureAISearchDynamicModelBuilder()
.BuildDynamic(
new() { Properties = properties },
defaultEmbeddingGenerator: null);
}
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
using Moq;
using Xunit;
namespace SemanticKernel.Connectors.AzureAISearch.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AzureAISearchVectorStore"/> class.
/// </summary>
public class AzureAISearchVectorStoreTests
{
private const string TestCollectionName = "testcollection";
private readonly Mock<SearchIndexClient> _searchIndexClientMock;
private readonly Mock<SearchClient> _searchClientMock;
private readonly CancellationToken _testCancellationToken = new(false);
public AzureAISearchVectorStoreTests()
{
this._searchClientMock = new Mock<SearchClient>(MockBehavior.Strict);
this._searchIndexClientMock = new Mock<SearchIndexClient>(MockBehavior.Strict);
this._searchIndexClientMock.Setup(x => x.GetSearchClient(TestCollectionName)).Returns(this._searchClientMock.Object);
this._searchIndexClientMock.Setup(x => x.ServiceName).Returns("TestService");
}
[Fact]
public void GetCollectionReturnsCollection()
{
// Arrange.
using var sut = new AzureAISearchVectorStore(this._searchIndexClientMock.Object);
// Act.
var actual = sut.GetCollection<string, SinglePropsModel>(TestCollectionName);
// Assert.
Assert.NotNull(actual);
Assert.IsType<AzureAISearchCollection<string, SinglePropsModel>>(actual);
}
[Fact]
public void GetCollectionThrowsForInvalidKeyType()
{
// Arrange.
using var sut = new AzureAISearchVectorStore(this._searchIndexClientMock.Object);
// Act & Assert.
Assert.Throws<NotSupportedException>(() => sut.GetCollection<int, SinglePropsModel>(TestCollectionName));
}
[Fact]
public async Task ListCollectionNamesCallsSDKAsync()
{
// Arrange async enumerator mock.
var iterationCounter = 0;
var asyncEnumeratorMock = new Mock<IAsyncEnumerator<string>>(MockBehavior.Strict);
asyncEnumeratorMock.Setup(x => x.MoveNextAsync()).Returns(() => ValueTask.FromResult(iterationCounter++ <= 4));
asyncEnumeratorMock.Setup(x => x.Current).Returns(() => $"testcollection{iterationCounter}");
// Arrange pageable mock.
var pageableMock = new Mock<AsyncPageable<string>>(MockBehavior.Strict);
pageableMock.Setup(x => x.GetAsyncEnumerator(this._testCancellationToken)).Returns(asyncEnumeratorMock.Object);
// Arrange search index client mock and sut.
this._searchIndexClientMock
.Setup(x => x.GetIndexNamesAsync(this._testCancellationToken))
.Returns(pageableMock.Object);
using var sut = new AzureAISearchVectorStore(this._searchIndexClientMock.Object);
// Act.
var actual = sut.ListCollectionNamesAsync(this._testCancellationToken);
// Assert.
Assert.NotNull(actual);
var actualList = await actual.ToListAsync();
Assert.Equal(5, actualList.Count);
Assert.All(actualList, (value, index) => Assert.Equal($"testcollection{index + 1}", value));
}
public sealed class SinglePropsModel
{
[VectorStoreKey]
public string Key { get; set; } = string.Empty;
[VectorStoreData]
public string Data { get; set; } = string.Empty;
[VectorStoreVector(4)]
public ReadOnlyMemory<float>? Vector { get; set; }
public string? NotAnnotated { get; set; }
}
}