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,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;net472</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<RootNamespace>AzureAI.ConformanceTests</RootNamespace>
<UserSecretsId>b7762d10-e29b-4bb1-8b74-b6d69a667dd4</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Azure.Identity"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="Microsoft.Extensions.Configuration"/>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json"/>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets"/>
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="Humanizer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\VectorData\AzureAISearch\AzureAISearch.csproj"/>
<ProjectReference Include="..\VectorData.ConformanceTests\VectorData.ConformanceTests.csproj"/>
</ItemGroup>
<ItemGroup>
<None Update="testsettings.json" Condition="Exists('testsettings.json')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="testsettings.development.json" Condition="Exists('testsettings.development.json')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchAllSupportedTypesTests(AzureAISearchFixture fixture) : IClassFixture<AzureAISearchFixture>
{
[ConditionalFact]
public async Task AllTypesBatchGetAsync()
{
var collection = fixture.TestStore.DefaultVectorStore.GetCollection<string, AzureAISearchAllTypes>("all-types", AzureAISearchAllTypes.GetRecordDefinition());
await collection.EnsureCollectionExistsAsync();
List<AzureAISearchAllTypes> records =
[
new()
{
Id = "all-types-1",
BoolProperty = true,
NullableBoolProperty = false,
StringProperty = "string prop 1",
NullableStringProperty = "nullable prop 1",
IntProperty = 1,
NullableIntProperty = 10,
LongProperty = 100L,
NullableLongProperty = 1000L,
FloatProperty = 10.5f,
NullableFloatProperty = 100.5f,
DoubleProperty = 23.75d,
NullableDoubleProperty = 233.75d,
DateTimeOffsetProperty = DateTimeOffset.UtcNow,
NullableDateTimeOffsetProperty = DateTimeOffset.UtcNow,
StringArray = ["one", "two"],
StringList = ["eleven", "twelve"],
BoolArray = [true, false],
BoolList = [true, false],
IntArray = [1, 2],
IntList = [11, 12],
LongArray = [100L, 200L],
LongList = [1100L, 1200L],
FloatArray = [1.5f, 2.5f],
FloatList = [11.5f, 12.5f],
DoubleArray = [1.5d, 2.5d],
DoubleList = [11.5d, 12.5d],
DateTimeOffsetArray = [DateTimeOffset.UtcNow, DateTimeOffset.UtcNow],
DateTimeOffsetList = [DateTimeOffset.UtcNow, DateTimeOffset.UtcNow],
Embedding = new ReadOnlyMemory<float>([1.5f, 2.5f, 3.5f, 4.5f, 5.5f, 6.5f, 7.5f, 8.5f])
},
new()
{
Id = "all-types-2",
BoolProperty = false,
NullableBoolProperty = null,
StringProperty = "string prop 2",
NullableStringProperty = null,
IntProperty = 2,
NullableIntProperty = null,
LongProperty = 200L,
NullableLongProperty = null,
FloatProperty = 20.5f,
NullableFloatProperty = null,
DoubleProperty = 43.75,
NullableDoubleProperty = null,
Embedding = ReadOnlyMemory<float>.Empty,
// From https://learn.microsoft.com/en-us/rest/api/searchservice/supported-data-types:
// "All of the above types are nullable, except for collections of primitive and complex types, for example, Collection(Edm.String)"
// So for collections, we can't use nulls.
StringArray = [],
StringList = [],
BoolArray = [],
BoolList = [],
IntArray = [],
IntList = [],
LongArray = [],
LongList = [],
FloatArray = [],
FloatList = [],
DoubleArray = [],
DoubleList = [],
DateTimeOffsetArray = [],
DateTimeOffsetList = [],
}
];
try
{
await collection.UpsertAsync(records);
var allTypes = await collection.GetAsync(records.Select(r => r.Id), new RecordRetrievalOptions { IncludeVectors = true }).ToListAsync();
var allTypes1 = allTypes.Single(x => x.Id == records[0].Id);
var allTypes2 = allTypes.Single(x => x.Id == records[1].Id);
records[0].AssertEqual(allTypes1);
records[1].AssertEqual(allTypes2);
}
finally
{
await collection.EnsureCollectionDeletedAsync();
}
}
}
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchCollectionManagementTests(AzureAISearchFixture fixture)
: CollectionManagementTests<string>(fixture), IClassFixture<AzureAISearchFixture>;
@@ -0,0 +1,118 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure;
using Azure.Search.Documents.Indexes;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
using VectorData.ConformanceTests;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchDependencyInjectionTests
: DependencyInjectionTests<AzureAISearchVectorStore, AzureAISearchCollection<string, DependencyInjectionTests<string>.Record>, string, DependencyInjectionTests<string>.Record>
{
private static readonly Uri s_endpoint = new("https://localhost");
private static readonly AzureKeyCredential s_keyCredential = new("fakeKey");
protected override void PopulateConfiguration(ConfigurationManager configuration, object? serviceKey = null)
=> configuration.AddInMemoryCollection(
[
new(CreateConfigKey("AzureAI", serviceKey, "Endpoint"), "https://localhost"),
new(CreateConfigKey("AzureAI", serviceKey, "Key"), "fakeKey"),
]);
private static Uri EndpointProvider(IServiceProvider sp, object? serviceKey = null)
=> new(sp.GetRequiredService<IConfiguration>().GetRequiredSection(CreateConfigKey("AzureAI", serviceKey, "Endpoint")).Value!);
private static AzureKeyCredential KeyProvider(IServiceProvider sp, object? serviceKey = null)
=> new(sp.GetRequiredService<IConfiguration>().GetRequiredSection(CreateConfigKey("AzureAI", serviceKey, "Key")).Value!);
public override IEnumerable<Func<IServiceCollection, object?, string, ServiceLifetime, IServiceCollection>> CollectionDelegates
{
get
{
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services
.AddAzureAISearchCollection<Record>(name,
sp => new SearchIndexClient(EndpointProvider(sp), KeyProvider(sp)), lifetime: lifetime)
: services
.AddKeyedAzureAISearchCollection<Record>(serviceKey, name,
sp => new SearchIndexClient(EndpointProvider(sp, serviceKey), KeyProvider(sp, serviceKey)), lifetime: lifetime);
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services
.AddSingleton<SearchIndexClient>(sp => new SearchIndexClient(s_endpoint, s_keyCredential))
.AddAzureAISearchCollection<Record>(name, lifetime: lifetime)
: services
.AddSingleton<SearchIndexClient>(sp => new SearchIndexClient(s_endpoint, s_keyCredential))
.AddKeyedAzureAISearchCollection<Record>(serviceKey, name, lifetime: lifetime);
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services.AddAzureAISearchCollection<Record>(
name, s_endpoint, s_keyCredential, lifetime: lifetime)
: services.AddKeyedAzureAISearchCollection<Record>(
serviceKey, name, s_endpoint, s_keyCredential, lifetime: lifetime);
}
}
public override IEnumerable<Func<IServiceCollection, object?, ServiceLifetime, IServiceCollection>> StoreDelegates
{
get
{
yield return (services, serviceKey, lifetime) => serviceKey is null
? services
.AddAzureAISearchVectorStore(sp => new SearchIndexClient(EndpointProvider(sp), KeyProvider(sp)), lifetime: lifetime)
: services
.AddKeyedAzureAISearchVectorStore(serviceKey, sp => new SearchIndexClient(EndpointProvider(sp, serviceKey), KeyProvider(sp, serviceKey)), lifetime: lifetime);
yield return (services, serviceKey, lifetime) => serviceKey is null
? services.AddAzureAISearchVectorStore(
s_endpoint, s_keyCredential, lifetime: lifetime)
: services.AddKeyedAzureAISearchVectorStore(
serviceKey, s_endpoint, s_keyCredential, lifetime: lifetime);
yield return (services, serviceKey, lifetime) => serviceKey is null
? services
.AddSingleton<SearchIndexClient>(sp => new SearchIndexClient(s_endpoint, s_keyCredential))
.AddAzureAISearchVectorStore(lifetime: lifetime)
: services
.AddSingleton<SearchIndexClient>(sp => new SearchIndexClient(s_endpoint, s_keyCredential))
.AddKeyedAzureAISearchVectorStore(serviceKey, lifetime: lifetime);
}
}
[Fact]
public void EndpointCantBeNull()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddAzureAISearchCollection<Record>(
name: "notNull", endpoint: null!, s_keyCredential));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedAzureAISearchCollection<Record>(
serviceKey: "notNull", name: "notNull", endpoint: null!, s_keyCredential));
}
[Fact]
public void KeyCredentialCantBeNull()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddAzureAISearchCollection<Record>(
name: "notNull", s_endpoint, keyCredential: null!));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedAzureAISearchCollection<Record>(
serviceKey: "notNull", name: "notNull", s_endpoint, keyCredential: null!));
}
[Fact]
public void TokenCredentialCantBeNull()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddAzureAISearchCollection<Record>(
name: "notNull", s_endpoint, tokenCredential: null!));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedAzureAISearchCollection<Record>(
serviceKey: "notNull", name: "notNull", s_endpoint, tokenCredential: null!));
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchDistanceFunctionTests(AzureAISearchDistanceFunctionTests.Fixture fixture)
: DistanceFunctionTests<string>(fixture), IClassFixture<AzureAISearchDistanceFunctionTests.Fixture>
{
public override Task CosineDistance() => Assert.ThrowsAsync<NotSupportedException>(base.CosineDistance);
public override Task NegativeDotProductSimilarity() => Assert.ThrowsAsync<NotSupportedException>(base.NegativeDotProductSimilarity);
public override Task EuclideanSquaredDistance() => Assert.ThrowsAsync<NotSupportedException>(base.EuclideanSquaredDistance);
public override Task HammingDistance() => Assert.ThrowsAsync<NotSupportedException>(base.HammingDistance);
public override Task ManhattanDistance() => Assert.ThrowsAsync<NotSupportedException>(base.ManhattanDistance);
public new class Fixture() : DistanceFunctionTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
// AzureAISearch does not return the expected standard mathematical result for each distance function
public override bool AssertScores => false;
}
}
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchEmbeddingGenerationTests(AzureAISearchEmbeddingGenerationTests.StringVectorFixture stringVectorFixture, AzureAISearchEmbeddingGenerationTests.RomOfFloatVectorFixture romOfFloatVectorFixture)
: EmbeddingGenerationTests<string>(stringVectorFixture, romOfFloatVectorFixture), IClassFixture<AzureAISearchEmbeddingGenerationTests.StringVectorFixture>, IClassFixture<AzureAISearchEmbeddingGenerationTests.RomOfFloatVectorFixture>
{
// SearchAsync without a generator delegates to the service for AzureAISearch
public override Task SearchAsync_string_without_generator_throws()
=> Task.CompletedTask;
public new class StringVectorFixture : EmbeddingGenerationTests<string>.StringVectorFixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> AzureAISearchTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
services => services
.AddSingleton(AzureAISearchTestStore.Instance.Client)
.AddAzureAISearchVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
services => services
.AddSingleton(AzureAISearchTestStore.Instance.Client)
.AddAzureAISearchCollection<RecordWithAttributes>(this.CollectionName)
];
}
public new class RomOfFloatVectorFixture : EmbeddingGenerationTests<string>.RomOfFloatVectorFixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> AzureAISearchTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
services => services
.AddSingleton(AzureAISearchTestStore.Instance.Client)
.AddAzureAISearchVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
services => services
.AddSingleton(AzureAISearchTestStore.Instance.Client)
.AddAzureAISearchCollection<RecordWithAttributes>(this.CollectionName)
];
}
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchFilterTests(AzureAISearchFilterTests.Fixture fixture)
: FilterTests<string>(fixture), IClassFixture<AzureAISearchFilterTests.Fixture>
{
// Azure AI Search only supports search.in() over strings
public override Task Contains_over_inline_int_array()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Contains_over_inline_int_array());
public new class Fixture : FilterTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchHybridSearchTests(
AzureAISearchHybridSearchTests.VectorAndStringFixture vectorAndStringFixture,
AzureAISearchHybridSearchTests.MultiTextFixture multiTextFixture)
: HybridSearchTests<string>(vectorAndStringFixture, multiTextFixture),
IClassFixture<AzureAISearchHybridSearchTests.VectorAndStringFixture>,
IClassFixture<AzureAISearchHybridSearchTests.MultiTextFixture>
{
public new class VectorAndStringFixture : HybridSearchTests<string>.VectorAndStringFixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
public new class MultiTextFixture : HybridSearchTests<string>.MultiTextFixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchIndexKindTests(AzureAISearchIndexKindTests.Fixture fixture)
: IndexKindTests<string>(fixture), IClassFixture<AzureAISearchIndexKindTests.Fixture>
{
[ConditionalFact]
public virtual Task Hnsw()
=> this.Test(IndexKind.Hnsw);
public new class Fixture() : IndexKindTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests;
namespace AzureAISearch.ConformanceTests;
public class AzureAISearchTestSuiteImplementationTests : TestSuiteImplementationTests;
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests.ModelTests;
public class AzureAISearchBasicModelTests(AzureAISearchBasicModelTests.Fixture fixture)
: BasicModelTests<string>(fixture), IClassFixture<AzureAISearchBasicModelTests.Fixture>
{
public new class Fixture : BasicModelTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests.ModelTests;
public class AzureAISearchDynamicModelTests(AzureAISearchDynamicModelTests.Fixture fixture)
: DynamicModelTests<string>(fixture), IClassFixture<AzureAISearchDynamicModelTests.Fixture>
{
public new class Fixture : DynamicModelTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests.ModelTests;
public class AzureAISearchMultiVectorModelTests(AzureAISearchMultiVectorModelTests.Fixture fixture)
: MultiVectorModelTests<string>(fixture), IClassFixture<AzureAISearchMultiVectorModelTests.Fixture>
{
public new class Fixture : MultiVectorModelTests<string>.Fixture
{
public override string CollectionName => "multi-vector-" + AzureAISearchTestEnvironment.TestIndexPostfix;
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests.ModelTests;
public class AzureAISearchNoDataModelTests(AzureAISearchNoDataModelTests.Fixture fixture)
: NoDataModelTests<string>(fixture), IClassFixture<AzureAISearchNoDataModelTests.Fixture>
{
public new class Fixture : NoDataModelTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace AzureAISearch.ConformanceTests.ModelTests;
public class AzureAISearchNoVectorModelTests(AzureAISearchNoVectorModelTests.Fixture fixture)
: NoVectorModelTests<string>(fixture), IClassFixture<AzureAISearchNoVectorModelTests.Fixture>
{
public new class Fixture : NoVectorModelTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,3 @@
// Copyright (c) Microsoft. All rights reserved.
[assembly: AzureAISearch.ConformanceTests.Support.AzureAISearchUrlRequiredAttribute]
@@ -0,0 +1,168 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.VectorData;
using Xunit;
namespace AzureAISearch.ConformanceTests.Support;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public class AzureAISearchAllTypes
{
[VectorStoreKey]
public string Id { get; set; }
[VectorStoreData]
public bool BoolProperty { get; set; }
[VectorStoreData]
public bool? NullableBoolProperty { get; set; }
[VectorStoreData]
public string StringProperty { get; set; }
[VectorStoreData]
public string? NullableStringProperty { get; set; }
[VectorStoreData]
public int IntProperty { get; set; }
[VectorStoreData]
public int? NullableIntProperty { get; set; }
[VectorStoreData]
public long LongProperty { get; set; }
[VectorStoreData]
public long? NullableLongProperty { get; set; }
[VectorStoreData]
public float FloatProperty { get; set; }
[VectorStoreData]
public float? NullableFloatProperty { get; set; }
[VectorStoreData]
public double DoubleProperty { get; set; }
[VectorStoreData]
public double? NullableDoubleProperty { get; set; }
[VectorStoreData]
public DateTimeOffset DateTimeOffsetProperty { get; set; }
[VectorStoreData]
public DateTimeOffset? NullableDateTimeOffsetProperty { get; set; }
[VectorStoreData]
public string[] StringArray { get; set; }
[VectorStoreData]
public List<string> StringList { get; set; }
[VectorStoreData]
public bool[] BoolArray { get; set; }
[VectorStoreData]
public List<bool> BoolList { get; set; }
[VectorStoreData]
public int[] IntArray { get; set; }
[VectorStoreData]
public List<int> IntList { get; set; }
[VectorStoreData]
public long[] LongArray { get; set; }
[VectorStoreData]
public List<long> LongList { get; set; }
[VectorStoreData]
public float[] FloatArray { get; set; }
[VectorStoreData]
public List<float> FloatList { get; set; }
[VectorStoreData]
public double[] DoubleArray { get; set; }
[VectorStoreData]
public List<double> DoubleList { get; set; }
[VectorStoreData]
public DateTimeOffset[] DateTimeOffsetArray { get; set; }
[VectorStoreData]
public List<DateTimeOffset> DateTimeOffsetList { get; set; }
[VectorStoreVector(Dimensions: 8, DistanceFunction = DistanceFunction.DotProductSimilarity)]
public ReadOnlyMemory<float>? Embedding { get; set; }
internal void AssertEqual(AzureAISearchAllTypes other)
{
Assert.Equal(this.Id, other.Id);
Assert.Equal(this.BoolProperty, other.BoolProperty);
Assert.Equal(this.NullableBoolProperty, other.NullableBoolProperty);
Assert.Equal(this.StringProperty, other.StringProperty);
Assert.Equal(this.NullableStringProperty, other.NullableStringProperty);
Assert.Equal(this.IntProperty, other.IntProperty);
Assert.Equal(this.NullableIntProperty, other.NullableIntProperty);
Assert.Equal(this.LongProperty, other.LongProperty);
Assert.Equal(this.NullableLongProperty, other.NullableLongProperty);
Assert.Equal(this.FloatProperty, other.FloatProperty);
Assert.Equal(this.NullableFloatProperty, other.NullableFloatProperty);
Assert.Equal(this.DoubleProperty, other.DoubleProperty);
Assert.Equal(this.NullableDoubleProperty, other.NullableDoubleProperty);
AssertEqual(this.DateTimeOffsetProperty, other.DateTimeOffsetProperty);
Assert.Equal(this.NullableDateTimeOffsetProperty.HasValue, other.NullableDateTimeOffsetProperty.HasValue);
if (this.NullableDateTimeOffsetProperty.HasValue && other.NullableDateTimeOffsetProperty.HasValue)
{
AssertEqual(this.NullableDateTimeOffsetProperty.Value, other.NullableDateTimeOffsetProperty.Value);
}
Assert.Equal(this.StringArray, other.StringArray);
Assert.Equal(this.StringList, other.StringList);
Assert.Equal(this.BoolArray, other.BoolArray);
Assert.Equal(this.BoolList, other.BoolList);
Assert.Equal(this.IntArray, other.IntArray);
Assert.Equal(this.IntList, other.IntList);
Assert.Equal(this.LongArray, other.LongArray);
Assert.Equal(this.LongList, other.LongList);
Assert.Equal(this.FloatArray, other.FloatArray);
Assert.Equal(this.FloatList, other.FloatList);
Assert.Equal(this.DoubleArray, other.DoubleArray);
Assert.Equal(this.DoubleList, other.DoubleList);
Assert.Equal(this.DateTimeOffsetArray.Length, other.DateTimeOffsetArray.Length);
for (int i = 0; i < this.DateTimeOffsetArray.Length; i++)
{
AssertEqual(this.DateTimeOffsetArray[i], other.DateTimeOffsetArray[i]);
}
Assert.Equal(this.DateTimeOffsetList.Count, other.DateTimeOffsetList.Count);
for (int i = 0; i < this.DateTimeOffsetList.Count; i++)
{
AssertEqual(this.DateTimeOffsetList[i], other.DateTimeOffsetList[i]);
}
Assert.Equal(this.Embedding!.Value.ToArray(), other.Embedding!.Value.ToArray());
static void AssertEqual(DateTimeOffset expected, DateTimeOffset actual)
{
Assert.Equal(expected, actual, TimeSpan.FromSeconds(0.01));
}
}
internal static VectorStoreCollectionDefinition GetRecordDefinition()
=> new()
{
Properties =
[
new VectorStoreKeyProperty(nameof(AzureAISearchAllTypes.Id), typeof(string)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.BoolProperty), typeof(bool)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.NullableBoolProperty), typeof(bool?)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.StringProperty), typeof(string)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.NullableStringProperty), typeof(string)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.IntProperty), typeof(int)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.NullableIntProperty), typeof(int?)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.LongProperty), typeof(long)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.NullableLongProperty), typeof(long?)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.FloatProperty), typeof(float)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.NullableFloatProperty), typeof(float?)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.DoubleProperty), typeof(double)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.NullableDoubleProperty), typeof(double?)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.DateTimeOffsetProperty), typeof(DateTimeOffset)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.NullableDateTimeOffsetProperty), typeof(DateTimeOffset?)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.StringArray), typeof(string[])),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.StringList), typeof(List<string>)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.BoolArray), typeof(bool[])),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.BoolList), typeof(List<bool>)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.IntArray), typeof(int[])),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.IntList), typeof(List<int>)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.LongArray), typeof(long[])),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.LongList), typeof(List<long>)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.FloatArray), typeof(float[])),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.FloatList), typeof(List<float>)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.DoubleArray), typeof(double[])),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.DoubleList), typeof(List<double>)),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.DateTimeOffsetArray), typeof(DateTimeOffset[])),
new VectorStoreDataProperty(nameof(AzureAISearchAllTypes.DateTimeOffsetList), typeof(List<DateTimeOffset>)),
new VectorStoreVectorProperty(nameof(AzureAISearchAllTypes.Embedding), typeof(ReadOnlyMemory<float>?), 8) { DistanceFunction = Microsoft.Extensions.VectorData.DistanceFunction.DotProductSimilarity }
]
};
}
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests.Support;
namespace AzureAISearch.ConformanceTests.Support;
public class AzureAISearchFixture : VectorStoreFixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
namespace AzureAISearch.ConformanceTests.Support;
#pragma warning disable CA1810 // Initialize all static fields when those fields are declared
internal static class AzureAISearchTestEnvironment
{
#pragma warning disable CA1308 // Normalize strings to uppercase
public static readonly string TestIndexPostfix = '-' + new Regex("[^a-zA-Z0-9]").Replace(Environment.MachineName.ToLowerInvariant(), "");
#pragma warning restore CA1308 // Normalize strings to uppercase
public static readonly string? ServiceUrl, ApiKey;
public static bool IsConnectionInfoDefined => ServiceUrl is not null;
static AzureAISearchTestEnvironment()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(path: "testsettings.json", optional: true)
.AddJsonFile(path: "testsettings.development.json", optional: true)
.AddEnvironmentVariables()
.AddUserSecrets<AzureAISearchUrlRequiredAttribute>()
.Build();
var azureAISearchSection = configuration.GetSection("AzureAISearch");
ServiceUrl = azureAISearchSection?["ServiceUrl"];
ApiKey = azureAISearchSection?["ApiKey"];
}
}
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq.Expressions;
using Azure;
using Azure.Identity;
using Azure.Search.Documents.Indexes;
using Humanizer;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
using VectorData.ConformanceTests.Support;
namespace AzureAISearch.ConformanceTests.Support;
internal sealed class AzureAISearchTestStore : TestStore
{
public static AzureAISearchTestStore Instance { get; } = new();
private SearchIndexClient? _client;
public SearchIndexClient Client
=> this._client ?? throw new InvalidOperationException("Call InitializeAsync() first");
public AzureAISearchVectorStore GetVectorStore(AzureAISearchVectorStoreOptions options)
=> new(this.Client, options);
private AzureAISearchTestStore()
{
}
protected override Task StartAsync()
{
(string? serviceUrl, string? apiKey) = (AzureAISearchTestEnvironment.ServiceUrl, AzureAISearchTestEnvironment.ApiKey);
if (string.IsNullOrWhiteSpace(serviceUrl))
{
throw new InvalidOperationException("Service URL is not configured, set AzureAISearch:ServiceUrl (and AzureAISearch:ApiKey if you want)");
}
this._client = string.IsNullOrWhiteSpace(apiKey)
? new SearchIndexClient(new Uri(serviceUrl), new DefaultAzureCredential())
: new SearchIndexClient(new Uri(serviceUrl), new AzureKeyCredential(apiKey));
this.DefaultVectorStore = new AzureAISearchVectorStore(this._client);
return Task.CompletedTask;
}
// Azure AI search only supports lowercase letters, digits or dashes.
// Also, add a suffix containing machine name to allow multiple developers to work against the same cloud instance.
public override string AdjustCollectionName(string baseName)
=> baseName.Kebaberize() + AzureAISearchTestEnvironment.TestIndexPostfix;
public override async Task WaitForDataAsync<TKey, TRecord>(
VectorStoreCollection<TKey, TRecord> collection,
int recordCount,
Expression<Func<TRecord, bool>>? filter = null,
Expression<Func<TRecord, object?>>? vectorProperty = null,
int? vectorSize = null,
object? dummyVector = null)
{
await base.WaitForDataAsync(collection, recordCount, filter, vectorProperty, vectorSize, dummyVector);
// There seems to be some asynchronicity/race condition specific to Azure AI Search which isn't taken care
// of by the generic retry loop in the base implementation.
// TODO: Investigate this and remove
await Task.Delay(TimeSpan.FromMilliseconds(1000));
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests.Xunit;
namespace AzureAISearch.ConformanceTests.Support;
/// <summary>
/// Checks whether the sqlite_vec extension is properly installed, and skips the test(s) otherwise.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class AzureAISearchUrlRequiredAttribute : Attribute, ITestCondition
{
public ValueTask<bool> IsMetAsync() => new(AzureAISearchTestEnvironment.IsConnectionInfoDefined);
public string Skip { get; set; } = "Service URL is not configured, set AzureAISearch:ServiceUrl (and AzureAISearch:ApiKey if you don't use managed identity).";
public string SkipReason
=> this.Skip;
}
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace AzureAISearch.ConformanceTests.TypeTests;
public class AzureAISearchDataTypeTests(AzureAISearchDataTypeTests.Fixture fixture)
: DataTypeTests<string, AzureAISearchDataTypeTests.Fixture.AzureAISearchRecord>(fixture),
IClassFixture<AzureAISearchDataTypeTests.Fixture>
{
[ConditionalFact(Skip = "Issues around empty collection initialization")]
public override Task String_array() => Task.CompletedTask;
protected override object? GenerateEmptyProperty(VectorStoreProperty property)
=> property.Type switch
{
null => throw new InvalidOperationException($"Property '{property.Name}' has no type defined."),
// In Azure AI Search, array fields must be non-null (at least for now)
var t when t.IsArray => Array.CreateInstance(t.GetElementType()!, 0),
_ => base.GenerateEmptyProperty(property)
};
public new class Fixture : DataTypeTests<string, Fixture.AzureAISearchRecord>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
public override IList<VectorStoreDataProperty> GetDataProperties()
=> base.GetDataProperties().Where(p =>
p.Type != typeof(byte)
&& p.Type != typeof(short)
&& p.Type != typeof(decimal)
&& p.Type != typeof(Guid)
#if NET
&& p.Type != typeof(TimeOnly)
#endif
).ToList();
public override Type[] UnsupportedDefaultTypes { get; } =
[
typeof(byte),
typeof(short),
typeof(decimal),
typeof(Guid),
#if NET
typeof(TimeOnly)
#endif
];
public class AzureAISearchRecord : RecordBase
{
public int Int { get; set; }
public long Long { get; set; }
public float Float { get; set; }
public double Double { get; set; }
public string? String { get; set; }
public bool Bool { get; set; }
public DateTime DateTime { get; set; }
public DateTimeOffset DateTimeOffset { get; set; }
#if NET
public DateOnly DateOnly { get; set; }
#endif
public string[] StringArray { get; set; } = null!;
public int? NullableInt { get; set; }
}
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using Xunit;
#pragma warning disable CA2000 // Dispose objects before losing scope
namespace AzureAISearch.ConformanceTests.TypeTests;
public class AzureAISearchEmbeddingTypeTests(AzureAISearchEmbeddingTypeTests.Fixture fixture)
: EmbeddingTypeTests<string>(fixture), IClassFixture<AzureAISearchEmbeddingTypeTests.Fixture>
{
public new class Fixture : EmbeddingTypeTests<string>.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using AzureAISearch.ConformanceTests.Support;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace AzureAISearch.ConformanceTests.TypeTests;
public class AzureAISearchKeyTypeTests(AzureAISearchKeyTypeTests.Fixture fixture)
: KeyTypeTests(fixture), IClassFixture<AzureAISearchKeyTypeTests.Fixture>
{
[ConditionalFact]
public virtual Task String() => this.Test<string>("foo", "bar");
public new class Fixture : KeyTypeTests.Fixture
{
public override TestStore TestStore => AzureAISearchTestStore.Instance;
}
}
@@ -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; }
}
}
@@ -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.Chroma.UnitTests</AssemblyName>
<RootNamespace>SemanticKernel.Connectors.Chroma.UnitTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0050</NoWarn>
</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)" />
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/HttpMessageHandlerStub.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/**/*.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\VectorData\Chroma\Chroma.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,335 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Chroma;
using Microsoft.SemanticKernel.Memory;
using Moq;
using Xunit;
namespace SemanticKernel.Connectors.UnitTests.Chroma;
/// <summary>
/// Unit tests for <see cref="ChromaMemoryStore"/> class.
/// </summary>
[Experimental("SKEXP0020")]
public sealed class ChromaMemoryStoreTests : IDisposable
{
private const string CollectionId = "fake-collection-id";
private const string CollectionName = "fake-collection-name";
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<IChromaClient> _chromaClientMock;
public ChromaMemoryStoreTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = this.GetHttpClientStub();
this._chromaClientMock = new Mock<IChromaClient>();
this._chromaClientMock
.Setup(client => client.GetCollectionAsync(CollectionName, CancellationToken.None))
.ReturnsAsync(new ChromaCollectionModel { Id = CollectionId, Name = CollectionName });
}
[Fact]
public async Task ItUsesProvidedEndpointFromConstructorAsync()
{
// Arrange
const string Endpoint = "https://fake-random-test-host/fake-path/";
var store = new ChromaMemoryStore(this._httpClient, Endpoint);
// Act
await store.GetAsync("fake-collection", "fake-key");
// Assert
Assert.StartsWith(Endpoint, this._messageHandlerStub.RequestUri?.AbsoluteUri, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ItUsesBaseAddressFromHttpClientAsync()
{
// Arrange
const string BaseAddress = "https://fake-random-test-host/fake-path/";
using var httpClient = this.GetHttpClientStub();
httpClient.BaseAddress = new Uri(BaseAddress);
var store = new ChromaMemoryStore(httpClient);
// Act
await store.GetAsync("fake-collection", "fake-key");
// Assert
Assert.StartsWith(BaseAddress, this._messageHandlerStub.RequestUri?.AbsoluteUri, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ItCanCreateCollectionAsync()
{
// Arrange
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
await store.CreateCollectionAsync(CollectionName);
// Assert
this._chromaClientMock.Verify(client => client.CreateCollectionAsync(CollectionName, CancellationToken.None), Times.Once());
}
[Fact]
public async Task ItCanDeleteCollectionAsync()
{
// Arrange
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
await store.DeleteCollectionAsync(CollectionName);
// Assert
this._chromaClientMock.Verify(client => client.DeleteCollectionAsync(CollectionName, CancellationToken.None), Times.Once());
}
[Fact]
public async Task ItThrowsExceptionOnNonExistentCollectionDeletionAsync()
{
// Arrange
const string CollectionName = "non-existent-collection";
const string CollectionDoesNotExistErrorMessage = $"Collection {CollectionName} does not exist";
const string ExpectedExceptionMessage = $"Cannot delete non-existent collection {CollectionName}";
this._chromaClientMock
.Setup(client => client.DeleteCollectionAsync(CollectionName, CancellationToken.None))
.Throws(new HttpOperationException { ResponseContent = CollectionDoesNotExistErrorMessage });
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var exception = await Record.ExceptionAsync(() => store.DeleteCollectionAsync(CollectionName));
// Assert
Assert.IsType<KernelException>(exception);
Assert.Equal(ExpectedExceptionMessage, exception.Message);
}
[Fact]
public async Task ItReturnsTrueWhenCollectionExistsAsync()
{
// Arrange
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var doesCollectionExist = await store.DoesCollectionExistAsync(CollectionName);
// Assert
Assert.True(doesCollectionExist);
}
[Fact]
public async Task ItReturnsFalseWhenCollectionDoesNotExistAsync()
{
// Arrange
const string CollectionName = "non-existent-collection";
const string CollectionDoesNotExistErrorMessage = $"Collection {CollectionName} does not exist";
this._chromaClientMock
.Setup(client => client.GetCollectionAsync(CollectionName, CancellationToken.None))
.Throws(new HttpOperationException { ResponseContent = CollectionDoesNotExistErrorMessage });
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var doesCollectionExist = await store.DoesCollectionExistAsync(CollectionName);
// Assert
Assert.False(doesCollectionExist);
}
[Fact]
public async Task ItCanGetMemoryRecordFromCollectionAsync()
{
// Arrange
var expectedMemoryRecord = this.GetRandomMemoryRecord();
var embeddingsModel = this.GetEmbeddingsModelFromMemoryRecord(expectedMemoryRecord);
this._chromaClientMock
.Setup(client => client.GetEmbeddingsAsync(CollectionId, new[] { expectedMemoryRecord.Key }, It.IsAny<string[]>(), CancellationToken.None))
.ReturnsAsync(embeddingsModel);
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var actualMemoryRecord = await store.GetAsync(CollectionName, expectedMemoryRecord.Key, withEmbedding: true);
// Assert
Assert.NotNull(actualMemoryRecord);
this.AssertMemoryRecordEqual(expectedMemoryRecord, actualMemoryRecord);
}
[Fact]
public async Task ItReturnsNullWhenMemoryRecordDoesNotExistAsync()
{
// Arrange
const string MemoryRecordKey = "fake-record-key";
this._chromaClientMock
.Setup(client => client.GetEmbeddingsAsync(CollectionId, new[] { MemoryRecordKey }, It.IsAny<string[]>(), CancellationToken.None))
.ReturnsAsync(new ChromaEmbeddingsModel());
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var actualMemoryRecord = await store.GetAsync(CollectionName, MemoryRecordKey, withEmbedding: true);
// Assert
Assert.Null(actualMemoryRecord);
}
[Fact]
public async Task ItThrowsExceptionOnGettingMemoryRecordFromNonExistingCollectionAsync()
{
// Arrange
const string CollectionName = "non-existent-collection";
const string MemoryRecordKey = "fake-record-key";
const string CollectionDoesNotExistErrorMessage = $"Collection {CollectionName} does not exist";
this._chromaClientMock
.Setup(client => client.GetCollectionAsync(CollectionName, CancellationToken.None))
.Throws(new KernelException(CollectionDoesNotExistErrorMessage));
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var exception = await Record.ExceptionAsync(() => store.GetAsync(CollectionName, MemoryRecordKey, withEmbedding: true));
// Assert
Assert.IsType<KernelException>(exception);
Assert.Equal(CollectionDoesNotExistErrorMessage, exception.Message);
}
[Fact]
public async Task ItCanGetMemoryRecordBatchFromCollectionAsync()
{
// Arrange
var memoryRecord1 = this.GetRandomMemoryRecord();
var memoryRecord2 = this.GetRandomMemoryRecord();
var memoryRecord3 = this.GetRandomMemoryRecord();
MemoryRecord[] expectedMemoryRecords = [memoryRecord1, memoryRecord2, memoryRecord3];
var memoryRecordKeys = expectedMemoryRecords.Select(l => l.Key).ToArray();
var embeddingsModel = this.GetEmbeddingsModelFromMemoryRecords(expectedMemoryRecords);
this._chromaClientMock
.Setup(client => client.GetEmbeddingsAsync(CollectionId, memoryRecordKeys, It.IsAny<string[]>(), CancellationToken.None))
.ReturnsAsync(embeddingsModel);
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var actualMemoryRecords = await store.GetBatchAsync(CollectionName, memoryRecordKeys, withEmbeddings: true).ToListAsync();
// Assert
Assert.Equal(expectedMemoryRecords.Length, actualMemoryRecords.Count);
for (var i = 0; i < expectedMemoryRecords.Length; i++)
{
this.AssertMemoryRecordEqual(expectedMemoryRecords[i], actualMemoryRecords[i]);
}
}
[Fact]
public async Task ItCanReturnCollectionsAsync()
{
// Arrange
var expectedCollections = new List<string> { "fake-collection-1", "fake-collection-2", "fake-collection-3" };
this._chromaClientMock
.Setup(client => client.ListCollectionsAsync(CancellationToken.None))
.Returns(expectedCollections.ToAsyncEnumerable());
var store = new ChromaMemoryStore(this._chromaClientMock.Object);
// Act
var actualCollections = await store.GetCollectionsAsync().ToListAsync();
// Assert
Assert.Equal(expectedCollections.Count, actualCollections.Count);
for (var i = 0; i < expectedCollections.Count; i++)
{
Assert.Equal(expectedCollections[i], actualCollections[i]);
}
}
public void Dispose()
{
this._httpClient.Dispose();
this._messageHandlerStub.Dispose();
}
#region private ================================================================================
private void AssertMemoryRecordEqual(MemoryRecord expectedRecord, MemoryRecord actualRecord)
{
Assert.Equal(expectedRecord.Key, actualRecord.Key);
Assert.True(expectedRecord.Embedding.Span.SequenceEqual(actualRecord.Embedding.Span));
Assert.Equal(expectedRecord.Metadata.Id, actualRecord.Metadata.Id);
Assert.Equal(expectedRecord.Metadata.Text, actualRecord.Metadata.Text);
Assert.Equal(expectedRecord.Metadata.Description, actualRecord.Metadata.Description);
Assert.Equal(expectedRecord.Metadata.AdditionalMetadata, actualRecord.Metadata.AdditionalMetadata);
Assert.Equal(expectedRecord.Metadata.IsReference, actualRecord.Metadata.IsReference);
Assert.Equal(expectedRecord.Metadata.ExternalSourceName, actualRecord.Metadata.ExternalSourceName);
}
private HttpClient GetHttpClientStub()
{
return new HttpClient(this._messageHandlerStub, false);
}
private MemoryRecord GetRandomMemoryRecord(ReadOnlyMemory<float>? embedding = null)
{
var id = Guid.NewGuid().ToString();
var memoryEmbedding = embedding ?? new[] { 1f, 3f, 5f };
return MemoryRecord.LocalRecord(
id: id,
text: "text-" + Guid.NewGuid().ToString(),
description: "description-" + Guid.NewGuid().ToString(),
embedding: memoryEmbedding,
additionalMetadata: "metadata-" + Guid.NewGuid().ToString(),
key: id);
}
private Dictionary<string, object> GetEmbeddingMetadataFromMemoryRecord(MemoryRecord memoryRecord)
{
var serialized = JsonSerializer.Serialize(memoryRecord.Metadata);
return JsonSerializer.Deserialize<Dictionary<string, object>>(serialized)!;
}
private ChromaEmbeddingsModel GetEmbeddingsModelFromMemoryRecords(MemoryRecord[] memoryRecords)
{
var embeddingsModel = new ChromaEmbeddingsModel();
embeddingsModel.Ids.AddRange(memoryRecords.Select(l => l.Key));
embeddingsModel.Embeddings.AddRange(memoryRecords.Select(l => l.Embedding.ToArray()));
embeddingsModel.Metadatas.AddRange(memoryRecords.Select(this.GetEmbeddingMetadataFromMemoryRecord));
return embeddingsModel;
}
private ChromaEmbeddingsModel GetEmbeddingsModelFromMemoryRecord(MemoryRecord memoryRecord)
{
return this.GetEmbeddingsModelFromMemoryRecords([memoryRecord]);
}
#endregion
}
@@ -0,0 +1,145 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.CosmosMongoDB;
using MongoDB.Bson.Serialization.Attributes;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosMongoDB.ConformanceTests;
[CosmosConnectionStringRequired]
public sealed class CosmosMongoBsonMappingTests(CosmosMongoBsonMappingTests.Fixture fixture)
: IClassFixture<CosmosMongoBsonMappingTests.Fixture>
{
[ConditionalFact]
public async Task Upsert_with_bson_model_works()
{
var store = (CosmosMongoTestStore)fixture.TestStore;
var collectionName = fixture.TestStore.AdjustCollectionName("BsonModel");
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty(nameof(BsonTestModel.Id), typeof(string)),
new VectorStoreDataProperty(nameof(BsonTestModel.HotelName), typeof(string))
]
};
var model = new BsonTestModel { Id = "key", HotelName = "Test Name" };
using var collection = new CosmosMongoCollection<string, BsonTestModel>(
store.Database,
collectionName,
new() { Definition = definition });
await collection.EnsureCollectionExistsAsync();
try
{
await collection.UpsertAsync(model);
var getResult = await collection.GetAsync(model.Id!);
Assert.NotNull(getResult);
Assert.Equal("key", getResult!.Id);
Assert.Equal("Test Name", getResult.HotelName);
}
finally
{
await collection.EnsureCollectionDeletedAsync();
}
}
[ConditionalFact]
public async Task Upsert_with_bson_vector_store_model_works()
{
var store = (CosmosMongoTestStore)fixture.TestStore;
var collectionName = fixture.TestStore.AdjustCollectionName("BsonVectorStoreModel");
var model = new BsonVectorStoreTestModel { HotelId = "key", HotelName = "Test Name" };
using var collection = new CosmosMongoCollection<string, BsonVectorStoreTestModel>(store.Database, collectionName);
await collection.EnsureCollectionExistsAsync();
try
{
await collection.UpsertAsync(model);
var getResult = await collection.GetAsync(model.HotelId!);
Assert.NotNull(getResult);
Assert.Equal("key", getResult!.HotelId);
Assert.Equal("Test Name", getResult.HotelName);
}
finally
{
await collection.EnsureCollectionDeletedAsync();
}
}
[ConditionalFact]
public async Task Upsert_with_bson_vector_store_with_name_model_works()
{
var store = (CosmosMongoTestStore)fixture.TestStore;
var collectionName = fixture.TestStore.AdjustCollectionName("BsonVectorStoreWithNameModel");
var model = new BsonVectorStoreWithNameTestModel { Id = "key", HotelName = "Test Name" };
using var collection = new CosmosMongoCollection<string, BsonVectorStoreWithNameTestModel>(store.Database, collectionName);
await collection.EnsureCollectionExistsAsync();
try
{
await collection.UpsertAsync(model);
var getResult = await collection.GetAsync(model.Id!);
Assert.NotNull(getResult);
Assert.Equal("key", getResult!.Id);
Assert.Equal("Test Name", getResult.HotelName);
}
finally
{
await collection.EnsureCollectionDeletedAsync();
}
}
public sealed class Fixture : VectorStoreFixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
private sealed class BsonTestModel
{
[BsonId]
public string? Id { get; set; }
[BsonElement("hotel_name")]
public string? HotelName { get; set; }
}
private sealed class BsonVectorStoreTestModel
{
[BsonId]
[VectorStoreKey]
public string? HotelId { get; set; }
[BsonElement("hotel_name")]
[VectorStoreData]
public string? HotelName { get; set; }
}
private sealed class BsonVectorStoreWithNameTestModel
{
[BsonId]
[VectorStoreKey]
public string? Id { get; set; }
[BsonElement("bson_hotel_name")]
[VectorStoreData(StorageName = "storage_hotel_name")]
public string? HotelName { get; set; }
}
}
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests;
using Xunit;
namespace CosmosMongoDB.ConformanceTests;
public class CosmosMongoCollectionManagementTests(CosmosMongoFixture fixture)
: CollectionManagementTests<string>(fixture), IClassFixture<CosmosMongoFixture>
{
}
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;net472</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<RootNamespace>CosmosMongoDB.ConformanceTests</RootNamespace>
<UserSecretsId>b7762d10-e29b-4bb1-8b74-b6d69a667dd4</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\VectorData\CosmosMongoDB\CosmosMongoDB.csproj" />
<ProjectReference Include="..\VectorData.ConformanceTests\VectorData.ConformanceTests.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="testsettings.json" Condition="Exists('testsettings.json')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="testsettings.development.json" Condition="Exists('testsettings.development.json')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel.Connectors.CosmosMongoDB;
using MongoDB.Driver;
using VectorData.ConformanceTests;
using Xunit;
namespace CosmosMongoDB.ConformanceTests;
public class CosmosMongoDependencyInjectionTests
: DependencyInjectionTests<CosmosMongoVectorStore, CosmosMongoCollection<string, DependencyInjectionTests<string>.Record>, string, DependencyInjectionTests<string>.Record>
{
protected const string ConnectionString = "mongodb://localhost:27017";
protected const string DatabaseName = "dbName";
protected override void PopulateConfiguration(ConfigurationManager configuration, object? serviceKey = null)
=> configuration.AddInMemoryCollection(
[
new(CreateConfigKey("CosmosMongo", serviceKey, "ConnectionString"), ConnectionString),
new(CreateConfigKey("CosmosMongo", serviceKey, "DatabaseName"), DatabaseName),
]);
private static string ConnectionStringProvider(IServiceProvider sp)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection("CosmosMongo:ConnectionString").Value!;
private static string ConnectionStringProvider(IServiceProvider sp, object serviceKey)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection(CreateConfigKey("CosmosMongo", serviceKey, "ConnectionString")).Value!;
private static string DatabaseNameProvider(IServiceProvider sp)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection("CosmosMongo:DatabaseName").Value!;
private static string DatabaseNameProvider(IServiceProvider sp, object serviceKey)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection(CreateConfigKey("CosmosMongo", serviceKey, "DatabaseName")).Value!;
public override IEnumerable<Func<IServiceCollection, object?, string, ServiceLifetime, IServiceCollection>> CollectionDelegates
{
get
{
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services
.AddSingleton<MongoClient>(sp => new MongoClient(MongoClientSettings.FromConnectionString(ConnectionString)))
.AddSingleton<IMongoDatabase>(sp => sp.GetRequiredService<MongoClient>().GetDatabase(DatabaseName))
.AddCosmosMongoCollection<Record>(name, lifetime: lifetime)
: services
.AddSingleton<MongoClient>(sp => new MongoClient(MongoClientSettings.FromConnectionString(ConnectionString)))
.AddSingleton<IMongoDatabase>(sp => sp.GetRequiredService<MongoClient>().GetDatabase(DatabaseName))
.AddKeyedCosmosMongoCollection<Record>(serviceKey, name, lifetime: lifetime);
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services.AddCosmosMongoCollection<Record>(
name, ConnectionString, DatabaseName, lifetime: lifetime)
: services.AddKeyedCosmosMongoCollection<Record>(
serviceKey, name, ConnectionString, DatabaseName, lifetime: lifetime);
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services.AddCosmosMongoCollection<Record>(
name, ConnectionStringProvider, DatabaseNameProvider, lifetime: lifetime)
: services.AddKeyedCosmosMongoCollection<Record>(
serviceKey, name, sp => ConnectionStringProvider(sp, serviceKey), sp => DatabaseNameProvider(sp, serviceKey), lifetime: lifetime);
}
}
public override IEnumerable<Func<IServiceCollection, object?, ServiceLifetime, IServiceCollection>> StoreDelegates
{
get
{
yield return (services, serviceKey, lifetime) => serviceKey is null
? services.AddCosmosMongoVectorStore(
ConnectionString, DatabaseName, lifetime: lifetime)
: services.AddKeyedCosmosMongoVectorStore(
serviceKey, ConnectionString, DatabaseName, lifetime: lifetime);
yield return (services, serviceKey, lifetime) => serviceKey is null
? services
.AddSingleton<MongoClient>(sp => new MongoClient(MongoClientSettings.FromConnectionString(ConnectionString)))
.AddSingleton<IMongoDatabase>(sp => sp.GetRequiredService<MongoClient>().GetDatabase(DatabaseName))
.AddCosmosMongoVectorStore(lifetime: lifetime)
: services
.AddSingleton<MongoClient>(sp => new MongoClient(MongoClientSettings.FromConnectionString(ConnectionString)))
.AddSingleton<IMongoDatabase>(sp => sp.GetRequiredService<MongoClient>().GetDatabase(DatabaseName))
.AddKeyedCosmosMongoVectorStore(serviceKey, lifetime: lifetime);
}
}
[Fact]
public void ConnectionStringProviderCantBeNull()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosMongoCollection<Record>(
name: "notNull", connectionStringProvider: null!, databaseNameProvider: DatabaseNameProvider));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosMongoCollection<Record>(
serviceKey: "notNull", name: "notNull", connectionStringProvider: null!, databaseNameProvider: DatabaseNameProvider));
}
[Fact]
public void DatabaseNameProviderCantBeNull()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosMongoCollection<Record>(
name: "notNull", connectionStringProvider: ConnectionStringProvider, databaseNameProvider: null!));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosMongoCollection<Record>(
serviceKey: "notNull", name: "notNull", connectionStringProvider: ConnectionStringProvider, databaseNameProvider: null!));
}
[Fact]
public void ConnectionStringCantBeNullOrEmpty()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosMongoVectorStore(connectionString: null!, DatabaseName));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosMongoVectorStore(serviceKey: "notNull", connectionString: null!, DatabaseName));
Assert.Throws<ArgumentNullException>(() => services.AddCosmosMongoCollection<Record>(
name: "notNull", connectionString: null!, DatabaseName));
Assert.Throws<ArgumentException>(() => services.AddCosmosMongoCollection<Record>(
name: "notNull", connectionString: "", DatabaseName));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosMongoCollection<Record>(
serviceKey: "notNull", name: "notNull", connectionString: null!, DatabaseName));
Assert.Throws<ArgumentException>(() => services.AddKeyedCosmosMongoCollection<Record>(
serviceKey: "notNull", name: "notNull", connectionString: "", DatabaseName));
}
[Fact]
public void DatabaseNameCantBeNullOrEmpty()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosMongoVectorStore(ConnectionString, databaseName: null!));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosMongoVectorStore(serviceKey: "notNull", ConnectionString, databaseName: null!));
Assert.Throws<ArgumentNullException>(() => services.AddCosmosMongoCollection<Record>(
name: "notNull", ConnectionString, databaseName: null!));
Assert.Throws<ArgumentException>(() => services.AddCosmosMongoCollection<Record>(
name: "notNull", ConnectionString, databaseName: ""));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosMongoCollection<Record>(
serviceKey: "notNull", name: "notNull", ConnectionString, databaseName: null!));
Assert.Throws<ArgumentException>(() => services.AddKeyedCosmosMongoCollection<Record>(
serviceKey: "notNull", name: "notNull", ConnectionString, databaseName: ""));
}
}
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosMongoDB.ConformanceTests;
public class CosmosMongoDistanceFunctionTests(CosmosMongoDistanceFunctionTests.Fixture fixture)
: DistanceFunctionTests<int>(fixture), IClassFixture<CosmosMongoDistanceFunctionTests.Fixture>
{
public override Task CosineSimilarity() => Assert.ThrowsAsync<NotSupportedException>(base.CosineSimilarity);
public override Task EuclideanSquaredDistance() => Assert.ThrowsAsync<NotSupportedException>(base.EuclideanSquaredDistance);
public override Task NegativeDotProductSimilarity() => Assert.ThrowsAsync<NotSupportedException>(base.NegativeDotProductSimilarity);
public override Task HammingDistance() => Assert.ThrowsAsync<NotSupportedException>(base.HammingDistance);
public override Task ManhattanDistance() => Assert.ThrowsAsync<NotSupportedException>(base.ManhattanDistance);
public new class Fixture() : DistanceFunctionTests<int>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
public override bool AssertScores { get; } = false;
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosMongoDB.ConformanceTests;
public class CosmosMongoEmbeddingGenerationTests(CosmosMongoEmbeddingGenerationTests.StringVectorFixture stringVectorFixture, CosmosMongoEmbeddingGenerationTests.RomOfFloatVectorFixture romOfFloatVectorFixture)
: EmbeddingGenerationTests<string>(stringVectorFixture, romOfFloatVectorFixture), IClassFixture<CosmosMongoEmbeddingGenerationTests.StringVectorFixture>, IClassFixture<CosmosMongoEmbeddingGenerationTests.RomOfFloatVectorFixture>
{
public new class StringVectorFixture : EmbeddingGenerationTests<string>.StringVectorFixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> CosmosMongoTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosMongoTestStore.Instance.Database)
.AddCosmosMongoVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosMongoTestStore.Instance.Database)
.AddCosmosMongoCollection<RecordWithAttributes>(this.CollectionName)
];
}
public new class RomOfFloatVectorFixture : EmbeddingGenerationTests<string>.RomOfFloatVectorFixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> CosmosMongoTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosMongoTestStore.Instance.Database)
.AddCosmosMongoVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosMongoTestStore.Instance.Database)
.AddCosmosMongoCollection<RecordWithAttributes>(this.CollectionName)
];
}
}
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosMongoDB.ConformanceTests;
public class CosmosMongoFilterTests(CosmosMongoFilterTests.Fixture fixture)
: FilterTests<string>(fixture), IClassFixture<CosmosMongoFilterTests.Fixture>
{
// Specialized MongoDB syntax for NOT over Contains ($nin)
[ConditionalFact]
public virtual Task Not_over_Contains()
=> this.TestFilterAsync(
r => !new[] { 8, 10 }.Contains(r.Int),
r => !new[] { 8, 10 }.Contains((int)r["Int"]!));
#region Null checking
// MongoDB currently doesn't support null checking ({ "Foo" : null }) in vector search pre-filters
public override Task Equal_with_null_reference_type()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Equal_with_null_reference_type());
public override Task Equal_with_null_captured()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Equal_with_null_captured());
public override Task NotEqual_with_null_reference_type()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.NotEqual_with_null_reference_type());
public override Task NotEqual_with_null_captured()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.NotEqual_with_null_captured());
public override Task Equal_int_property_with_null_nullable_int()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Equal_int_property_with_null_nullable_int());
#endregion
#region Not
// MongoDB currently doesn't support NOT in vector search pre-filters
// (https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#atlas-vector-search-pre-filter)
public override Task Not_over_And()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Not_over_And());
public override Task Not_over_Or()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Not_over_Or());
#endregion
public override Task Contains_over_field_string_array()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Contains_over_field_string_array());
public override Task Contains_over_field_string_List()
=> Assert.ThrowsAsync<NotSupportedException>(() => base.Contains_over_field_string_List());
public new class Fixture : FilterTests<string>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
protected override string IndexKind => Microsoft.Extensions.VectorData.IndexKind.IvfFlat;
protected override string DistanceFunction => Microsoft.Extensions.VectorData.DistanceFunction.CosineDistance;
}
}
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosMongoDB.ConformanceTests;
public class CosmosMongoIndexKindTests(CosmosMongoIndexKindTests.Fixture fixture)
: IndexKindTests<int>(fixture), IClassFixture<CosmosMongoIndexKindTests.Fixture>
{
// Note: Cosmos Mongo support HNSW, but only in a specific tier.
// [ConditionalFact]
// public virtual Task Hnsw()
// => this.Test(IndexKind.Hnsw);
[ConditionalFact]
public virtual Task IvfFlat()
=> this.Test(IndexKind.IvfFlat);
// Cosmos Mongo does not support index-less searching
public override Task Flat() => Assert.ThrowsAsync<NotSupportedException>(base.Flat);
public new class Fixture() : IndexKindTests<int>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.ModelTests;
namespace CosmosMongoDB.ConformanceTests;
public class CosmosMongoTestSuiteImplementationTests : TestSuiteImplementationTests
{
protected override ICollection<Type> IgnoredTestBases { get; } =
[
typeof(DynamicModelTests<>),
// Hybrid search not supported
typeof(HybridSearchTests<>),
];
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosMongoDB.ConformanceTests.ModelTests;
public class CosmosMongoBasicModelTests(CosmosMongoBasicModelTests.Fixture fixture)
: BasicModelTests<string>(fixture), IClassFixture<CosmosMongoBasicModelTests.Fixture>
{
public new class Fixture : BasicModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosMongoDB.ConformanceTests.ModelTests;
public class CosmosMongoMultiVectorModelTests(CosmosMongoMultiVectorModelTests.Fixture fixture)
: MultiVectorModelTests<string>(fixture), IClassFixture<CosmosMongoMultiVectorModelTests.Fixture>
{
public new class Fixture : MultiVectorModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosMongoDB.ConformanceTests.ModelTests;
public class CosmosMongoNoDataModelTests(CosmosMongoNoDataModelTests.Fixture fixture)
: NoDataModelTests<string>(fixture), IClassFixture<CosmosMongoNoDataModelTests.Fixture>
{
public new class Fixture : NoDataModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosMongoDB.ConformanceTests.ModelTests;
public class CosmosMongoNoVectorModelTests(CosmosMongoNoVectorModelTests.Fixture fixture)
: NoVectorModelTests<string>(fixture), IClassFixture<CosmosMongoNoVectorModelTests.Fixture>
{
public new class Fixture : NoVectorModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
}
@@ -0,0 +1,3 @@
// Copyright (c) Microsoft. All rights reserved.
[assembly: CosmosMongoDB.ConformanceTests.Support.CosmosConnectionStringRequired]
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests.Xunit;
namespace CosmosMongoDB.ConformanceTests.Support;
/// <summary>
/// Checks whether the sqlite_vec extension is properly installed, and skips the test(s) otherwise.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class CosmosConnectionStringRequiredAttribute : Attribute, ITestCondition
{
public ValueTask<bool> IsMetAsync() => new(CosmosMongoTestEnvironment.IsConnectionStringDefined);
public string Skip { get; set; } = "The Cosmos connection string hasn't been configured (CosmosMongo:ConnectionString).";
public string SkipReason
=> this.Skip;
}
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests.Support;
namespace CosmosMongoDB.ConformanceTests.Support;
public class CosmosMongoFixture : VectorStoreFixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.Configuration;
namespace CosmosMongoDB.ConformanceTests.Support;
#pragma warning disable CA1810 // Initialize all static fields when those fields are declared
public static class CosmosMongoTestEnvironment
{
public static readonly string? ConnectionString;
public static bool IsConnectionStringDefined => ConnectionString is not null;
static CosmosMongoTestEnvironment()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(path: "testsettings.json", optional: true)
.AddJsonFile(path: "testsettings.development.json", optional: true)
.AddEnvironmentVariables()
.AddUserSecrets<CosmosConnectionStringRequiredAttribute>()
.Build();
ConnectionString = configuration["CosmosMongo:ConnectionString"];
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel.Connectors.CosmosMongoDB;
using MongoDB.Driver;
using VectorData.ConformanceTests.Support;
namespace CosmosMongoDB.ConformanceTests.Support;
#pragma warning disable CA1001
public sealed class CosmosMongoTestStore : TestStore
#pragma warning restore CA1001
{
public static CosmosMongoTestStore Instance { get; } = new();
private MongoClient? _client;
private IMongoDatabase? _database;
public MongoClient Client => this._client ?? throw new InvalidOperationException("Not initialized");
public IMongoDatabase Database => this._database ?? throw new InvalidOperationException("Not initialized");
public override string DefaultIndexKind => Microsoft.Extensions.VectorData.IndexKind.IvfFlat;
public override string DefaultDistanceFunction => Microsoft.Extensions.VectorData.DistanceFunction.CosineDistance;
public CosmosMongoVectorStore GetVectorStore(CosmosMongoVectorStoreOptions options)
=> new(this.Database, options);
private CosmosMongoTestStore()
{
}
protected override Task StartAsync()
{
if (string.IsNullOrWhiteSpace(CosmosMongoTestEnvironment.ConnectionString))
{
throw new InvalidOperationException("Connection string is not configured, set the CosmosMongo:ConnectionString environment variable");
}
this._client = new MongoClient(CosmosMongoTestEnvironment.ConnectionString);
this._database = this._client.GetDatabase("VectorSearchTests");
this.DefaultVectorStore = new CosmosMongoVectorStore(this._database);
return Task.CompletedTask;
}
}
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using Xunit;
namespace CosmosMongoDB.ConformanceTests.TypeTests;
public class CosmosMongoDataTypeTests(CosmosMongoDataTypeTests.Fixture fixture)
: DataTypeTests<string, DataTypeTests<string>.DefaultRecord>(fixture), IClassFixture<CosmosMongoDataTypeTests.Fixture>
{
public override Task Decimal()
=> this.Test<decimal>(
"Decimal", 8.5m, 9.5m,
isFilterable: false); // TODO: Filtering doesn't fail but the data doesn't seem to appear...
public override Task DateTime()
=> this.Test<DateTime>(
"DateTime",
new DateTime(2020, 1, 1, 12, 30, 45, DateTimeKind.Utc),
new DateTime(2021, 2, 3, 13, 40, 55, DateTimeKind.Utc),
instantiationExpression: () => new DateTime(2020, 1, 1, 12, 30, 45, DateTimeKind.Utc));
// MongoDB stores DateTimeOffset as UTC BsonDateTime; the offset is lost on round-trip.
public override Task DateTimeOffset()
=> this.Test<DateTimeOffset>(
"DateTimeOffset",
new DateTimeOffset(2020, 1, 1, 12, 30, 45, TimeSpan.Zero),
new DateTimeOffset(2021, 2, 3, 13, 40, 55, TimeSpan.Zero),
instantiationExpression: () => new DateTimeOffset(2020, 1, 1, 12, 30, 45, TimeSpan.Zero));
public new class Fixture : DataTypeTests<string, DataTypeTests<string>.DefaultRecord>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
// MongoDB does not support null checks in vector search pre-filters
public override bool IsNullFilteringSupported => false;
public override Type[] UnsupportedDefaultTypes { get; } =
[
typeof(byte),
typeof(short),
typeof(Guid),
#if NET
typeof(TimeOnly)
#endif
];
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using Xunit;
#pragma warning disable CA2000 // Dispose objects before losing scope
namespace CosmosMongoDB.ConformanceTests.TypeTests;
public class CosmosMongoEmbeddingTypeTests(CosmosMongoEmbeddingTypeTests.Fixture fixture)
: EmbeddingTypeTests<string>(fixture), IClassFixture<CosmosMongoEmbeddingTypeTests.Fixture>
{
public new class Fixture : EmbeddingTypeTests<string>.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
}
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosMongoDB.ConformanceTests.Support;
using MongoDB.Bson;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosMongoDB.ConformanceTests.TypeTests;
public class CosmosMongoKeyTypeTests(CosmosMongoKeyTypeTests.Fixture fixture)
: KeyTypeTests(fixture), IClassFixture<CosmosMongoKeyTypeTests.Fixture>
{
[ConditionalFact]
public virtual Task ObjectId() => this.Test<ObjectId>(new("652f8c3e8f9b2c1a4d3e6a7b"), supportsAutoGeneration: true);
[ConditionalFact]
public virtual Task String() => this.Test<string>("foo", "bar");
[ConditionalFact]
public virtual Task Int() => this.Test<int>(8);
[ConditionalFact]
public virtual Task Long() => this.Test<long>(8L);
public new class Fixture : KeyTypeTests.Fixture
{
public override TestStore TestStore => CosmosMongoTestStore.Instance;
}
}
@@ -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,713 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.CosmosMongoDB;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Moq;
using Xunit;
using MEVD = Microsoft.Extensions.VectorData;
namespace SemanticKernel.Connectors.CosmosMongoDB.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosMongoCollection{TKey, TRecord}"/> class.
/// </summary>
public sealed class CosmosMongoCollectionTests
{
private readonly Mock<IMongoDatabase> _mockMongoDatabase = new();
private readonly Mock<IMongoCollection<BsonDocument>> _mockMongoCollection = new();
public CosmosMongoCollectionTests()
{
this._mockMongoDatabase
.Setup(l => l.GetCollection<BsonDocument>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
.Returns(this._mockMongoCollection.Object);
}
[Fact]
public void ConstructorForModelWithoutKeyThrowsException()
{
// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => new CosmosMongoCollection<string, object>(this._mockMongoDatabase.Object, "collection"));
Assert.Contains("No key property found", exception.Message);
}
[Fact]
public void ConstructorWithDeclarativeModelInitializesCollection()
{
// Act & Assert
using var collection = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithImperativeModelInitializesCollection()
{
// Arrange
var definition = new VectorStoreCollectionDefinition
{
Properties = [new VectorStoreKeyProperty("Id", typeof(string))]
};
// Act
using var collection = new CosmosMongoCollection<string, TestModel>(
this._mockMongoDatabase.Object,
"collection",
new() { Definition = definition });
// Assert
Assert.NotNull(collection);
}
[Theory]
[MemberData(nameof(CollectionExistsData))]
public async Task CollectionExistsReturnsValidResultAsync(List<string> collections, string collectionName, bool expectedResult)
{
// Arrange
var mockCursor = new Mock<IAsyncCursor<string>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns(collections);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
collectionName);
// Act
var actualResult = await sut.CollectionExistsAsync();
// Assert
Assert.Equal(expectedResult, actualResult);
}
[Fact]
public async Task EnsureCollectionExistsInvokesValidMethodsAsync()
{
// Arrange
const string CollectionName = "collection";
var mockCursor = new Mock<IAsyncCursor<string>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([]);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
var mockIndexCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockIndexCursor
.SetupSequence(l => l.MoveNext(It.IsAny<CancellationToken>()))
.Returns(true)
.Returns(false);
mockIndexCursor
.Setup(l => l.Current)
.Returns([]);
var mockMongoIndexManager = new Mock<IMongoIndexManager<BsonDocument>>();
mockMongoIndexManager
.Setup(l => l.ListAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockIndexCursor.Object);
this._mockMongoCollection
.Setup(l => l.Indexes)
.Returns(mockMongoIndexManager.Object);
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionExistsAsync();
// Assert
this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync(
CollectionName,
It.IsAny<CreateCollectionOptions>(),
It.IsAny<CancellationToken>()), Times.Exactly(1));
this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync(
It.IsAny<ListCollectionNamesOptions>(),
It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task DeleteInvokesValidMethodsAsync()
{
// Arrange
const string RecordKey = "key";
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], RecordKey);
// Act
await sut.DeleteAsync(RecordKey);
// Assert
this._mockMongoCollection.Verify(l => l.DeleteOneAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DeleteBatchInvokesValidMethodsAsync()
{
// Arrange
List<string> recordKeys = ["key1", "key2"];
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.In(document => document["_id"].AsString, recordKeys);
// Act
await sut.DeleteAsync(recordKeys);
// Assert
this._mockMongoCollection.Verify(l => l.DeleteManyAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DeleteCollectionInvokesValidMethodsAsync()
{
// Arrange
const string CollectionName = "collection";
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionDeletedAsync();
// Assert
this._mockMongoDatabase.Verify(l => l.DropCollectionAsync(
It.Is<string>(name => name == CollectionName),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
var document = new BsonDocument { ["_id"] = RecordKey, ["HotelName"] = "Test Name" };
var mockCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([document]);
this._mockMongoCollection
.Setup(l => l.FindAsync(
It.IsAny<FilterDefinition<BsonDocument>>(),
It.IsAny<FindOptions<BsonDocument>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
var result = await sut.GetAsync(RecordKey);
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.HotelId);
Assert.Equal("Test Name", result.HotelName);
}
[Fact]
public async Task GetBatchReturnsValidRecordAsync()
{
// Arrange
var document1 = new BsonDocument { ["_id"] = "key1", ["HotelName"] = "Test Name 1" };
var document2 = new BsonDocument { ["_id"] = "key2", ["HotelName"] = "Test Name 2" };
var document3 = new BsonDocument { ["_id"] = "key3", ["HotelName"] = "Test Name 3" };
var mockCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockCursor
.SetupSequence(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true)
.ReturnsAsync(false);
mockCursor
.Setup(l => l.Current)
.Returns([document1, document2, document3]);
this._mockMongoCollection
.Setup(l => l.FindAsync(
It.IsAny<FilterDefinition<BsonDocument>>(),
It.IsAny<FindOptions<BsonDocument>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
var results = await sut.GetAsync(["key1", "key2", "key3"]).ToListAsync();
// Assert
Assert.NotNull(results[0]);
Assert.Equal("key1", results[0].HotelId);
Assert.Equal("Test Name 1", results[0].HotelName);
Assert.NotNull(results[1]);
Assert.Equal("key2", results[1].HotelId);
Assert.Equal("Test Name 2", results[1].HotelName);
Assert.NotNull(results[2]);
Assert.Equal("key3", results[2].HotelId);
Assert.Equal("Test Name 3", results[2].HotelName);
}
[Fact]
public async Task CanUpsertRecordAsync()
{
// Arrange
var hotel = new CosmosMongoHotelModel("key") { HotelName = "Test Name" };
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], "key");
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
await sut.UpsertAsync(hotel);
// Assert
this._mockMongoCollection.Verify(l => l.ReplaceOneAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.Is<BsonDocument>(document =>
document["_id"] == "key" &&
document["HotelName"] == "Test Name"),
It.IsAny<ReplaceOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task UpsertBatchReturnsRecordKeysAsync()
{
// Arrange
var hotel1 = new CosmosMongoHotelModel("key1") { HotelName = "Test Name 1" };
var hotel2 = new CosmosMongoHotelModel("key2") { HotelName = "Test Name 2" };
var hotel3 = new CosmosMongoHotelModel("key3") { HotelName = "Test Name 3" };
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
await sut.UpsertAsync([hotel1, hotel2, hotel3]);
}
[Fact]
public async Task UpsertWithModelWorksCorrectlyAsync()
{
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty("Id", typeof(string)),
new VectorStoreDataProperty("HotelName", typeof(string))
]
};
await this.TestUpsertWithModelAsync<TestModel>(
dataModel: new TestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "HotelName",
definition: definition);
}
[Fact]
public async Task UpsertWithVectorStoreModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync<VectorStoreTestModel>(
dataModel: new VectorStoreTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "HotelName");
}
[Fact]
public async Task UpsertWithBsonModelWorksCorrectlyAsync()
{
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty("Id", typeof(string)),
new VectorStoreDataProperty("HotelName", typeof(string))
]
};
await this.TestUpsertWithModelAsync<BsonTestModel>(
dataModel: new BsonTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "hotel_name",
definition: definition);
}
[Fact]
public async Task UpsertWithBsonVectorStoreModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync<BsonVectorStoreTestModel>(
dataModel: new BsonVectorStoreTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "hotel_name");
}
[Fact]
public async Task UpsertWithBsonVectorStoreWithNameModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync<BsonVectorStoreWithNameTestModel>(
dataModel: new BsonVectorStoreWithNameTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "bson_hotel_name");
}
[Theory]
[MemberData(nameof(SearchVectorTypeData))]
public async Task SearchThrowsExceptionWithInvalidVectorTypeAsync(object vector, bool exceptionExpected)
{
// Arrange
this.MockCollectionForSearch();
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act & Assert
if (exceptionExpected)
{
await Assert.ThrowsAsync<NotSupportedException>(async () => await sut.SearchAsync(vector, top: 3).ToListAsync());
}
else
{
Assert.NotNull(await sut.SearchAsync(vector, top: 3).FirstOrDefaultAsync());
}
}
[Theory]
[InlineData("TestEmbedding1", "TestEmbedding1", 3, 3)]
[InlineData("TestEmbedding2", "test_embedding_2", 4, 4)]
public async Task SearchUsesValidQueryAsync(
string? vectorPropertyName,
string expectedVectorPropertyName,
int actualTop,
int expectedTop)
{
// Arrange
var vector = new ReadOnlyMemory<float>([1f, 2f, 3f]);
var expectedSearch = new BsonDocument
{
{ "$search",
new BsonDocument
{
{ "cosmosSearch",
new BsonDocument
{
{ "vector", BsonArray.Create(vector.ToArray()) },
{ "path", expectedVectorPropertyName },
{ "k", expectedTop },
}
},
{ "returnStoredSource", true }
}
}
};
var expectedProjection = new BsonDocument
{
{ "$project",
new BsonDocument
{
{ "similarityScore", new BsonDocument { { "$meta", "searchScore" } } },
{ "document", "$$ROOT" }
}
}
};
this.MockCollectionForSearch();
using var sut = new CosmosMongoCollection<string, VectorSearchModel>(
this._mockMongoDatabase.Object,
"collection");
Expression<Func<VectorSearchModel, object?>>? vectorSelector = vectorPropertyName switch
{
"TestEmbedding1" => record => record.TestEmbedding1,
"TestEmbedding2" => record => record.TestEmbedding2,
_ => null
};
// Act
var actual = await sut.SearchAsync(vector, top: actualTop, new()
{
VectorProperty = vectorSelector,
}).FirstOrDefaultAsync();
// Assert
Assert.NotNull(actual);
this._mockMongoCollection.Verify(l => l.AggregateAsync(
It.Is<PipelineDefinition<BsonDocument, BsonDocument>>(pipeline =>
this.ComparePipeline(pipeline, expectedSearch, expectedProjection)),
It.IsAny<AggregateOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SearchThrowsExceptionWithNonExistentVectorPropertyNameAsync()
{
// Arrange
this.MockCollectionForSearch();
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
var options = new MEVD.VectorSearchOptions<CosmosMongoHotelModel> { VectorProperty = r => "non-existent-property" };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3, options).FirstOrDefaultAsync());
}
[Fact]
public async Task SearchReturnsRecordWithScoreAsync()
{
// Arrange
this.MockCollectionForSearch();
using var sut = new CosmosMongoCollection<string, CosmosMongoHotelModel>(
this._mockMongoDatabase.Object,
"collection");
// Act
var result = await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3).FirstOrDefaultAsync();
// Assert
Assert.NotNull(result);
Assert.Equal("key", result.Record.HotelId);
Assert.Equal("Test Name", result.Record.HotelName);
Assert.Equal(0.99f, result.Score);
}
public static TheoryData<List<string>, string, bool> CollectionExistsData => new()
{
{ ["collection-2"], "collection-2", true },
{ [], "non-existent-collection", false }
};
public static TheoryData<List<string>, int> EnsureCollectionExistsData => new()
{
{ ["collection"], 0 },
{ [], 1 }
};
public static TheoryData<object, bool> SearchVectorTypeData => new()
{
{ new ReadOnlyMemory<float>([1f, 2f, 3f]), false },
{ new ReadOnlyMemory<float>?(new([1f, 2f, 3f])), false },
{ new List<float>([1f, 2f, 3f]), true },
};
#region private
private bool ComparePipeline(
PipelineDefinition<BsonDocument, BsonDocument> actualPipeline,
BsonDocument expectedSearch,
BsonDocument expectedProjection)
{
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var documents = actualPipeline.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry)).Documents;
return
documents[0].ToJson() == expectedSearch.ToJson() &&
documents[1].ToJson() == expectedProjection.ToJson();
}
private void MockCollectionForSearch()
{
var document = new BsonDocument { ["_id"] = "key", ["HotelName"] = "Test Name" };
var searchResult = new BsonDocument { ["document"] = document, ["similarityScore"] = 0.99f };
var mockCursor = new Mock<IAsyncCursor<BsonDocument>>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([searchResult]);
this._mockMongoCollection
.Setup(l => l.AggregateAsync(
It.IsAny<PipelineDefinition<BsonDocument, BsonDocument>>(),
It.IsAny<AggregateOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
}
private async Task TestUpsertWithModelAsync<TDataModel>(
TDataModel dataModel,
string expectedPropertyName,
VectorStoreCollectionDefinition? definition = null)
where TDataModel : class
{
// Arrange
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], "key");
CosmosMongoCollectionOptions? options = definition != null ?
new() { Definition = definition } :
null;
using var sut = new CosmosMongoCollection<string, TDataModel>(
this._mockMongoDatabase.Object,
"collection",
options);
// Act
await sut.UpsertAsync(dataModel);
// Assert
this._mockMongoCollection.Verify(l => l.ReplaceOneAsync(
It.Is<FilterDefinition<BsonDocument>>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.Is<BsonDocument>(document =>
document["_id"] == "key" &&
document.Contains(expectedPropertyName) &&
document[expectedPropertyName] == "Test Name"),
It.IsAny<ReplaceOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
private static bool CompareFilterDefinitions(
FilterDefinition<BsonDocument> actual,
FilterDefinition<BsonDocument> expected,
IBsonSerializer<BsonDocument> documentSerializer,
IBsonSerializerRegistry serializerRegistry)
{
return actual.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry)) ==
expected.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry));
}
#pragma warning disable CA1812
private sealed class TestModel
{
public string? Id { get; set; }
public string? HotelName { get; set; }
}
private sealed class VectorStoreTestModel
{
[VectorStoreKey]
public string? Id { get; set; }
[VectorStoreData(StorageName = "hotel_name")]
public string? HotelName { get; set; }
}
private sealed class BsonTestModel
{
[BsonId]
public string? Id { get; set; }
[BsonElement("hotel_name")]
public string? HotelName { get; set; }
}
private sealed class BsonVectorStoreTestModel
{
[BsonId]
[VectorStoreKey]
public string? Id { get; set; }
[BsonElement("hotel_name")]
[VectorStoreData]
public string? HotelName { get; set; }
}
private sealed class BsonVectorStoreWithNameTestModel
{
[BsonId]
[VectorStoreKey]
public string? Id { get; set; }
[BsonElement("bson_hotel_name")]
[VectorStoreData(StorageName = "storage_hotel_name")]
public string? HotelName { get; set; }
}
private sealed class VectorSearchModel
{
[BsonId]
[VectorStoreKey]
public string? Id { get; set; }
[VectorStoreData]
public string? HotelName { get; set; }
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.IvfFlat, StorageName = "test_embedding_1")]
public ReadOnlyMemory<float> TestEmbedding1 { get; set; }
[BsonElement("test_embedding_2")]
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.IvfFlat)]
public ReadOnlyMemory<float> TestEmbedding2 { get; set; }
}
#pragma warning restore CA1812
#endregion
}
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>SemanticKernel.Connectors.CosmosMongoDB.UnitTests</AssemblyName>
<RootNamespace>SemanticKernel.Connectors.CosmosMongoDB.UnitTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);SKEXP0001</NoWarn>
</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>
<ProjectReference Include="..\..\..\src\VectorData\CosmosMongoDB\CosmosMongoDB.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.VectorData;
using MongoDB.Bson.Serialization.Attributes;
namespace SemanticKernel.Connectors.CosmosMongoDB.UnitTests;
public class CosmosMongoHotelModel(string hotelId)
{
/// <summary>The key of the record.</summary>
[VectorStoreKey]
public string HotelId { get; init; } = hotelId;
/// <summary>A string metadata field.</summary>
[VectorStoreData(IsIndexed = true)]
public string? HotelName { get; set; }
/// <summary>An int metadata field.</summary>
[VectorStoreData]
public int HotelCode { get; set; }
/// <summary>A float metadata field.</summary>
[VectorStoreData]
public float? HotelRating { get; set; }
/// <summary>A bool metadata field.</summary>
[BsonElement("parking_is_included")]
[VectorStoreData]
public bool ParkingIncluded { get; set; }
/// <summary>An array metadata field.</summary>
[VectorStoreData]
public List<string> Tags { get; set; } = [];
/// <summary>A data field.</summary>
[VectorStoreData]
public string? Description { get; set; }
/// <summary>A vector field.</summary>
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.IvfFlat)]
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
}
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Connectors.CosmosMongoDB;
using MongoDB.Driver;
using Moq;
using Xunit;
namespace SemanticKernel.Connectors.CosmosMongoDB.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosMongoVectorStore"/> class.
/// </summary>
public sealed class CosmosMongoVectorStoreTests
{
private readonly Mock<IMongoDatabase> _mockMongoDatabase = new();
[Fact]
public void GetCollectionWithNotSupportedKeyThrowsException()
{
// Arrange
using var sut = new CosmosMongoVectorStore(this._mockMongoDatabase.Object);
// Act & Assert
Assert.Throws<NotSupportedException>(() => sut.GetCollection<byte[], CosmosMongoHotelModel>("collection"));
}
[Fact]
public void GetCollectionWithoutFactoryReturnsDefaultCollection()
{
// Arrange
using var sut = new CosmosMongoVectorStore(this._mockMongoDatabase.Object);
// Act
var collection = sut.GetCollection<string, CosmosMongoHotelModel>("collection");
// Assert
Assert.NotNull(collection);
}
[Fact]
public async Task ListCollectionNamesReturnsCollectionNamesAsync()
{
// Arrange
var expectedCollectionNames = new List<string> { "collection-1", "collection-2", "collection-3" };
var mockCursor = new Mock<IAsyncCursor<string>>();
mockCursor
.SetupSequence(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true)
.ReturnsAsync(false);
mockCursor
.Setup(l => l.Current)
.Returns(expectedCollectionNames);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockCursor.Object);
using var sut = new CosmosMongoVectorStore(this._mockMongoDatabase.Object);
// Act
var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync();
// Assert
Assert.Equal(expectedCollectionNames, actualCollectionNames);
}
}
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;net472</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<RootNamespace>CosmosNoSql.ConformanceTests</RootNamespace>
<UserSecretsId>b7762d10-e29b-4bb1-8b74-b6d69a667dd4</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\VectorData\CosmosNoSql\CosmosNoSql.csproj"/>
<ProjectReference Include="..\VectorData.ConformanceTests\VectorData.ConformanceTests.csproj"/>
</ItemGroup>
<ItemGroup>
<None Update="testsettings.json" Condition="Exists('testsettings.json')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="testsettings.development.json" Condition="Exists('testsettings.development.json')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests;
using Xunit;
namespace CosmosNoSql.ConformanceTests;
public class CosmosNoSqlCollectionManagementTests(CosmosNoSqlFixture fixture)
: CollectionManagementTests<string>(fixture), IClassFixture<CosmosNoSqlFixture>
{
}
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosNoSql.ConformanceTests;
[CosmosConnectionStringRequired]
public sealed class CosmosNoSqlCollectionOptionsTests(CosmosNoSqlCollectionOptionsTests.Fixture fixture)
: IClassFixture<CosmosNoSqlCollectionOptionsTests.Fixture>
{
[ConditionalFact]
public async Task Collection_supports_partition_key_composite_key()
{
var store = (CosmosNoSqlTestStore)fixture.TestStore;
var collectionName = fixture.TestStore.AdjustCollectionName("PartitionKeyCompositeKey");
// The key type for operations is CosmosNoSqlKey (containing both document ID and partition key).
using var collection = new CosmosNoSqlCollection<CosmosNoSqlKey, PartitionedHotel>(
store.Database,
collectionName,
new() { PartitionKeyProperties = [nameof(PartitionedHotel.HotelName)] });
await collection.EnsureCollectionExistsAsync();
try
{
var record = new PartitionedHotel
{
HotelId = "hotel-1",
HotelName = "Hotel A",
Description = "Partitioned",
Embedding = new([1f, 2f, 3f])
};
await collection.UpsertAsync(record);
var key = new CosmosNoSqlKey(record.HotelId, record.HotelName);
var fetched = await collection.GetAsync(key, new() { IncludeVectors = true });
Assert.NotNull(fetched);
await collection.DeleteAsync(key);
Assert.Null(await collection.GetAsync(key));
}
finally
{
await collection.EnsureCollectionDeletedAsync();
}
}
[ConditionalTheory]
[InlineData(IndexingMode.Consistent)]
[InlineData(IndexingMode.Lazy)]
[InlineData(IndexingMode.None)]
public async Task Collection_supports_indexing_mode(IndexingMode indexingMode)
{
var store = (CosmosNoSqlTestStore)fixture.TestStore;
var collectionName = fixture.TestStore.AdjustCollectionName($"IndexingMode_{indexingMode}");
using var collection = new CosmosNoSqlCollection<string, IndexingModeHotel>(
store.Database,
collectionName,
new() { IndexingMode = indexingMode, Automatic = indexingMode != IndexingMode.None });
await collection.EnsureCollectionExistsAsync();
try
{
var record = new IndexingModeHotel
{
HotelId = "hotel-2",
HotelName = "Hotel B",
Embedding = new([1f, 0f, 0f])
};
await collection.UpsertAsync(record);
var fetched = await collection.GetAsync(record.HotelId);
Assert.NotNull(fetched);
await collection.DeleteAsync(record.HotelId);
Assert.Null(await collection.GetAsync(record.HotelId));
}
finally
{
await collection.EnsureCollectionDeletedAsync();
}
}
public sealed class Fixture : VectorStoreFixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
private sealed class PartitionedHotel
{
[VectorStoreKey]
public string HotelId { get; set; } = string.Empty;
[VectorStoreData]
public string HotelName { get; set; } = string.Empty;
[VectorStoreData]
public string? Description { get; set; }
[VectorStoreVector(Dimensions: 3)]
public ReadOnlyMemory<float> Embedding { get; set; }
}
private sealed class IndexingModeHotel
{
[VectorStoreKey]
public string HotelId { get; set; } = string.Empty;
[VectorStoreData]
public string HotelName { get; set; } = string.Empty;
[VectorStoreVector(Dimensions: 3)]
public ReadOnlyMemory<float> Embedding { get; set; }
}
}
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using VectorData.ConformanceTests;
using Xunit;
namespace CosmosNoSql.ConformanceTests.DependencyInjection;
public class CosmosNoSqlDependencyInjectionTests
: DependencyInjectionTests<CosmosNoSqlVectorStore, CosmosNoSqlCollection<string, DependencyInjectionTests<string>.Record>, string, DependencyInjectionTests<string>.Record>
{
protected const string ConnectionString = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=mock;";
protected const string DatabaseName = "dbName";
private static readonly CosmosClientOptions s_clientOptions = new()
{
UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default
};
protected override void PopulateConfiguration(ConfigurationManager configuration, object? serviceKey = null)
=> configuration.AddInMemoryCollection(
[
new(CreateConfigKey("CosmosNoSql", serviceKey, "ConnectionString"), ConnectionString),
new(CreateConfigKey("CosmosNoSql", serviceKey, "DatabaseName"), DatabaseName),
]);
private static string ConnectionStringProvider(IServiceProvider sp)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection("CosmosNoSql:ConnectionString").Value!;
private static string ConnectionStringProvider(IServiceProvider sp, object serviceKey)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection(CreateConfigKey("CosmosNoSql", serviceKey, "ConnectionString")).Value!;
private static string DatabaseNameProvider(IServiceProvider sp)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection("CosmosNoSql:DatabaseName").Value!;
private static string DatabaseNameProvider(IServiceProvider sp, object serviceKey)
=> sp.GetRequiredService<IConfiguration>().GetRequiredSection(CreateConfigKey("CosmosNoSql", serviceKey, "DatabaseName")).Value!;
public override IEnumerable<Func<IServiceCollection, object?, string, ServiceLifetime, IServiceCollection>> CollectionDelegates
{
get
{
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services
.AddSingleton<CosmosClient>(sp => new CosmosClient(ConnectionString, s_clientOptions))
.AddSingleton<Database>(sp => sp.GetRequiredService<CosmosClient>().GetDatabase(DatabaseName))
.AddCosmosNoSqlCollection<string, Record>(name, lifetime: lifetime)
: services
.AddSingleton<CosmosClient>(sp => new CosmosClient(ConnectionString, s_clientOptions))
.AddSingleton<Database>(sp => sp.GetRequiredService<CosmosClient>().GetDatabase(DatabaseName))
.AddKeyedCosmosNoSqlCollection<string, Record>(serviceKey, name, lifetime: lifetime);
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services.AddCosmosNoSqlCollection<string, Record>(
name, ConnectionString, DatabaseName, lifetime: lifetime)
: services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey, name, ConnectionString, DatabaseName, lifetime: lifetime);
yield return (services, serviceKey, name, lifetime) => serviceKey is null
? services.AddCosmosNoSqlCollection<string, Record>(
name, ConnectionStringProvider, DatabaseNameProvider, lifetime: lifetime)
: services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey, name, sp => ConnectionStringProvider(sp, serviceKey), sp => DatabaseNameProvider(sp, serviceKey), lifetime: lifetime);
}
}
public override IEnumerable<Func<IServiceCollection, object?, ServiceLifetime, IServiceCollection>> StoreDelegates
{
get
{
yield return (services, serviceKey, lifetime) => serviceKey is null
? services.AddCosmosNoSqlVectorStore(
ConnectionString, DatabaseName, lifetime: lifetime)
: services.AddKeyedCosmosNoSqlVectorStore(
serviceKey, ConnectionString, DatabaseName, lifetime: lifetime);
yield return (services, serviceKey, lifetime) => serviceKey is null
? services
.AddSingleton<CosmosClient>(sp => new CosmosClient(ConnectionString, s_clientOptions))
.AddSingleton<Database>(sp => sp.GetRequiredService<CosmosClient>().GetDatabase(DatabaseName))
.AddCosmosNoSqlVectorStore(lifetime: lifetime)
: services
.AddSingleton<CosmosClient>(sp => new CosmosClient(ConnectionString, s_clientOptions))
.AddSingleton<Database>(sp => sp.GetRequiredService<CosmosClient>().GetDatabase(DatabaseName))
.AddKeyedCosmosNoSqlVectorStore(serviceKey, lifetime: lifetime);
}
}
[Fact]
public void ConnectionStringProviderCantBeNull()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosNoSqlCollection<string, Record>(
name: "notNull", connectionStringProvider: null!, databaseNameProvider: DatabaseNameProvider));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey: "notNull", name: "notNull", connectionStringProvider: null!, databaseNameProvider: DatabaseNameProvider));
}
[Fact]
public void DatabaseNameProviderCantBeNull()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosNoSqlCollection<string, Record>(
name: "notNull", connectionStringProvider: ConnectionStringProvider, databaseNameProvider: null!));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey: "notNull", name: "notNull", connectionStringProvider: ConnectionStringProvider, databaseNameProvider: null!));
}
[Fact]
public void ConnectionStringCantBeNullOrEmpty()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosNoSqlVectorStore(connectionString: null!, DatabaseName));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosNoSqlVectorStore(serviceKey: "notNull", connectionString: null!, DatabaseName));
Assert.Throws<ArgumentNullException>(() => services.AddCosmosNoSqlCollection<string, Record>(
name: "notNull", connectionString: null!, DatabaseName));
Assert.Throws<ArgumentException>(() => services.AddCosmosNoSqlCollection<string, Record>(
name: "notNull", connectionString: "", DatabaseName));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey: "notNull", name: "notNull", connectionString: null!, DatabaseName));
Assert.Throws<ArgumentException>(() => services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey: "notNull", name: "notNull", connectionString: "", DatabaseName));
}
[Fact]
public void DatabaseNameCantBeNullOrEmpty()
{
IServiceCollection services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddCosmosNoSqlVectorStore(ConnectionString, databaseName: null!));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosNoSqlVectorStore(serviceKey: "notNull", ConnectionString, databaseName: null!));
Assert.Throws<ArgumentNullException>(() => services.AddCosmosNoSqlCollection<string, Record>(
name: "notNull", ConnectionString, databaseName: null!));
Assert.Throws<ArgumentException>(() => services.AddCosmosNoSqlCollection<string, Record>(
name: "notNull", ConnectionString, databaseName: ""));
Assert.Throws<ArgumentNullException>(() => services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey: "notNull", name: "notNull", ConnectionString, databaseName: null!));
Assert.Throws<ArgumentException>(() => services.AddKeyedCosmosNoSqlCollection<string, Record>(
serviceKey: "notNull", name: "notNull", ConnectionString, databaseName: ""));
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests;
public class CosmosNoSqlDistanceFunctionTests(CosmosNoSqlDistanceFunctionTests.Fixture fixture)
: DistanceFunctionTests<string>(fixture), IClassFixture<CosmosNoSqlDistanceFunctionTests.Fixture>
{
public override Task CosineDistance() => Assert.ThrowsAsync<NotSupportedException>(base.CosineDistance);
public override Task EuclideanSquaredDistance() => Assert.ThrowsAsync<NotSupportedException>(base.EuclideanSquaredDistance);
public override Task NegativeDotProductSimilarity() => Assert.ThrowsAsync<NotSupportedException>(base.NegativeDotProductSimilarity);
public override Task HammingDistance() => Assert.ThrowsAsync<NotSupportedException>(base.HammingDistance);
public override Task ManhattanDistance() => Assert.ThrowsAsync<NotSupportedException>(base.ManhattanDistance);
public new class Fixture() : DistanceFunctionTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests;
public class CosmosNoSqlEmbeddingGenerationTests(CosmosNoSqlEmbeddingGenerationTests.StringVectorFixture stringVectorFixture, CosmosNoSqlEmbeddingGenerationTests.RomOfFloatVectorFixture romOfFloatVectorFixture)
: EmbeddingGenerationTests<string>(stringVectorFixture, romOfFloatVectorFixture), IClassFixture<CosmosNoSqlEmbeddingGenerationTests.StringVectorFixture>, IClassFixture<CosmosNoSqlEmbeddingGenerationTests.RomOfFloatVectorFixture>
{
public new class StringVectorFixture : EmbeddingGenerationTests<string>.StringVectorFixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> CosmosNoSqlTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosNoSqlTestStore.Instance.Database)
.AddCosmosNoSqlVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosNoSqlTestStore.Instance.Database)
.AddCosmosNoSqlCollection<string, RecordWithAttributes>(this.CollectionName)
];
}
public new class RomOfFloatVectorFixture : EmbeddingGenerationTests<string>.RomOfFloatVectorFixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> CosmosNoSqlTestStore.Instance.GetVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosNoSqlTestStore.Instance.Database)
.AddCosmosNoSqlVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
services => services
.AddSingleton(CosmosNoSqlTestStore.Instance.Database)
.AddCosmosNoSqlCollection<string, RecordWithAttributes>(this.CollectionName)
];
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests;
public class CosmosNoSqlFilterTests(CosmosNoSqlFilterTests.Fixture fixture)
: FilterTests<string>(fixture), IClassFixture<CosmosNoSqlFilterTests.Fixture>
{
public new class Fixture : FilterTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests;
public class CosmosNoSqlHybridSearchTests(
CosmosNoSqlHybridSearchTests.VectorAndStringFixture vectorAndStringFixture,
CosmosNoSqlHybridSearchTests.MultiTextFixture multiTextFixture)
: HybridSearchTests<string>(vectorAndStringFixture, multiTextFixture),
IClassFixture<CosmosNoSqlHybridSearchTests.VectorAndStringFixture>,
IClassFixture<CosmosNoSqlHybridSearchTests.MultiTextFixture>
{
public new class VectorAndStringFixture : HybridSearchTests<string>.VectorAndStringFixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
public new class MultiTextFixture : HybridSearchTests<string>.MultiTextFixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosNoSql.ConformanceTests;
public class CosmosNoSqlIndexKindTests(CosmosNoSqlIndexKindTests.Fixture fixture)
: IndexKindTests<string>(fixture), IClassFixture<CosmosNoSqlIndexKindTests.Fixture>
{
[ConditionalFact(Skip = "DiskANN is supported by Cosmos NoSQL, but is not supported on the emulator and needs to be explicitly enabled")]
public virtual Task DiskANN()
=> this.Test(IndexKind.DiskAnn);
public new class Fixture() : IndexKindTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests;
namespace CosmosNoSql.ConformanceTests;
public class CosmosNoSqlTestSuiteImplementationTests : TestSuiteImplementationTests;
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests.ModelTests;
public class CosmosNoSqlBasicModelTests(CosmosNoSqlBasicModelTests.Fixture fixture)
: BasicModelTests<string>(fixture), IClassFixture<CosmosNoSqlBasicModelTests.Fixture>
{
public override async Task GetAsync_with_filter_and_multiple_OrderBys()
{
// CosmosException: The order by query does not have a corresponding composite index that it can be served from.
var exception = await Assert.ThrowsAsync<VectorStoreException>(base.GetAsync_with_filter_and_multiple_OrderBys);
Assert.IsType<CosmosException>(exception.InnerException);
}
public new class Fixture : BasicModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests.ModelTests;
public class CosmosNoSqlDynamicModelTests(CosmosNoSqlDynamicModelTests.Fixture fixture)
: DynamicModelTests<string>(fixture), IClassFixture<CosmosNoSqlDynamicModelTests.Fixture>
{
public override async Task GetAsync_with_filter_and_multiple_OrderBys()
{
// CosmosException: The order by query does not have a corresponding composite index that it can be served from.
var exception = await Assert.ThrowsAsync<VectorStoreException>(base.GetAsync_with_filter_and_multiple_OrderBys);
Assert.IsType<CosmosException>(exception.InnerException);
}
public new class Fixture : DynamicModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests.ModelTests;
public class CosmosNoSqlMultiVectorModelTests(CosmosNoSqlMultiVectorModelTests.Fixture fixture)
: MultiVectorModelTests<string>(fixture), IClassFixture<CosmosNoSqlMultiVectorModelTests.Fixture>
{
public new class Fixture : MultiVectorModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests.ModelTests;
public class CosmosNoSqlNoDataModelTests(CosmosNoSqlNoDataModelTests.Fixture fixture)
: NoDataModelTests<string>(fixture), IClassFixture<CosmosNoSqlNoDataModelTests.Fixture>
{
public new class Fixture : NoDataModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace CosmosNoSql.ConformanceTests.ModelTests;
public class CosmosNoSqlNoVectorModelTests(CosmosNoSqlNoVectorModelTests.Fixture fixture)
: NoVectorModelTests<string>(fixture), IClassFixture<CosmosNoSqlNoVectorModelTests.Fixture>
{
public new class Fixture : NoVectorModelTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,3 @@
// Copyright (c) Microsoft. All rights reserved.
[assembly: CosmosNoSql.ConformanceTests.Support.CosmosConnectionStringRequired]
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests.Xunit;
namespace CosmosNoSql.ConformanceTests.Support;
/// <summary>
/// Checks whether the sqlite_vec extension is properly installed, and skips the test(s) otherwise.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class CosmosConnectionStringRequiredAttribute : Attribute, ITestCondition
{
public ValueTask<bool> IsMetAsync() => new(CosmosNoSqlTestEnvironment.IsConnectionStringDefined);
public string Skip { get; set; } = "The Cosmos connection string hasn't been configured (CosmosNoSql:ConnectionString).";
public string SkipReason
=> this.Skip;
}
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests.Support;
namespace CosmosNoSql.ConformanceTests.Support;
public class CosmosNoSqlFixture : VectorStoreFixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.Configuration;
namespace CosmosNoSql.ConformanceTests.Support;
#pragma warning disable CA1810 // Initialize all static fields when those fields are declared
internal static class CosmosNoSqlTestEnvironment
{
public static readonly string? ConnectionString;
public static bool IsConnectionStringDefined => ConnectionString is not null;
static CosmosNoSqlTestEnvironment()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(path: "testsettings.json", optional: true)
.AddJsonFile(path: "testsettings.development.json", optional: true)
.AddEnvironmentVariables()
.AddUserSecrets<CosmosConnectionStringRequiredAttribute>()
.Build();
ConnectionString = configuration["CosmosNoSql:ConnectionString"];
}
}
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
#if NET472
using System.Net.Http;
#endif
using System.Text.Json;
using Microsoft.Azure.Cosmos;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using VectorData.ConformanceTests.Support;
namespace CosmosNoSql.ConformanceTests.Support;
#pragma warning disable CA1001 // Type owns disposable fields (_connection) but is not disposable
#pragma warning disable CA2000 // Dispose objects before losing scope
internal sealed class CosmosNoSqlTestStore : TestStore
{
public static CosmosNoSqlTestStore Instance { get; } = new();
private CosmosClient? _client;
private Database? _database;
public override string DefaultIndexKind => Microsoft.Extensions.VectorData.IndexKind.Flat;
public CosmosClient Client
=> this._client ?? throw new InvalidOperationException("Call InitializeAsync() first");
public Database Database
=> this._database ?? throw new InvalidOperationException("Call InitializeAsync() first");
public CosmosNoSqlVectorStore GetVectorStore(CosmosNoSqlVectorStoreOptions options)
=> new(this.Database, options);
private CosmosNoSqlTestStore()
{
}
#pragma warning disable CA5400 // HttpClient may be created without enabling CheckCertificateRevocationList
protected override async Task StartAsync()
{
var connectionString = CosmosNoSqlTestEnvironment.ConnectionString;
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException("Connection string is not configured, set the CosmosNoSql:ConnectionString environment variable");
}
var options = new CosmosClientOptions
{
UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default,
ConnectionMode = ConnectionMode.Gateway,
HttpClientFactory = () => new HttpClient(new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator })
};
this._client = new CosmosClient(connectionString, options);
this._database = this._client.GetDatabase("VectorDataIntegrationTests");
await this._client.CreateDatabaseIfNotExistsAsync("VectorDataIntegrationTests");
this.DefaultVectorStore = new CosmosNoSqlVectorStore(this._database);
}
#pragma warning restore CA5400
protected override Task StopAsync()
{
this._client?.Dispose();
return base.StopAsync();
}
}
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosNoSql.ConformanceTests.TypeTests;
public class CosmosNoSqlDataTypeTests(CosmosNoSqlDataTypeTests.Fixture fixture)
: DataTypeTests<string, DataTypeTests<string>.DefaultRecord>(fixture), IClassFixture<CosmosNoSqlDataTypeTests.Fixture>
{
// Cosmos doesn't support DateTimeOffset with non-zero offset, so we convert it to UTC.
// See https://github.com/dotnet/efcore/issues/35310
[ConditionalFact(Skip = "Need to convert DateTimeOffset to UTC before sending to Cosmos")]
public override Task DateTimeOffset()
=> this.Test<DateTimeOffset>(
"DateTimeOffset",
new DateTimeOffset(2020, 1, 1, 12, 30, 45, TimeSpan.FromHours(2)),
new DateTimeOffset(2021, 2, 3, 13, 40, 55, TimeSpan.FromHours(3)),
instantiationExpression: () => new DateTimeOffset(2020, 1, 1, 10, 30, 45, TimeSpan.FromHours(0)));
public new class Fixture : DataTypeTests<string, DataTypeTests<string>.DefaultRecord>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
public override Type[] UnsupportedDefaultTypes { get; } =
[
typeof(byte),
typeof(short),
typeof(decimal),
typeof(Guid),
#if NET
typeof(TimeOnly)
#endif
];
}
}
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using Microsoft.Extensions.AI;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using VectorData.ConformanceTests.Xunit;
using Xunit;
#pragma warning disable CA2000 // Dispose objects before losing scope
namespace CosmosNoSql.ConformanceTests.TypeTests;
public class CosmosNoSqlEmbeddingTypeTests(CosmosNoSqlEmbeddingTypeTests.Fixture fixture)
: EmbeddingTypeTests<string>(fixture), IClassFixture<CosmosNoSqlEmbeddingTypeTests.Fixture>
{
[ConditionalFact]
public virtual Task ReadOnlyMemory_of_byte()
=> this.Test<ReadOnlyMemory<byte>>(
new ReadOnlyMemory<byte>([1, 2, 3]),
new ReadOnlyMemoryEmbeddingGenerator<byte>([1, 2, 3]),
vectorEqualityAsserter: (e, a) => Assert.Equal(e.Span.ToArray(), a.Span.ToArray()));
[ConditionalFact]
public virtual Task Embedding_of_byte()
=> this.Test<Embedding<byte>>(
new Embedding<byte>(new ReadOnlyMemory<byte>([1, 2, 3])),
new ReadOnlyMemoryEmbeddingGenerator<byte>([1, 2, 3]),
vectorEqualityAsserter: (e, a) => Assert.Equal(e.Vector.Span.ToArray(), a.Vector.Span.ToArray()));
[ConditionalFact]
public virtual Task Array_of_byte()
=> this.Test<byte[]>(
[1, 2, 3],
new ReadOnlyMemoryEmbeddingGenerator<byte>([1, 2, 3]));
[ConditionalFact]
public virtual Task ReadOnlyMemory_of_sbyte()
=> this.Test<ReadOnlyMemory<sbyte>>(
new ReadOnlyMemory<sbyte>([1, 2, 3]),
new ReadOnlyMemoryEmbeddingGenerator<sbyte>([1, 2, 3]),
vectorEqualityAsserter: (e, a) => Assert.Equal(e.Span.ToArray(), a.Span.ToArray()));
[ConditionalFact]
public virtual Task Embedding_of_sbyte()
=> this.Test<Embedding<sbyte>>(
new Embedding<sbyte>(new ReadOnlyMemory<sbyte>([1, 2, 3])),
new ReadOnlyMemoryEmbeddingGenerator<sbyte>([1, 2, 3]),
vectorEqualityAsserter: (e, a) => Assert.Equal(e.Vector.Span.ToArray(), a.Vector.Span.ToArray()));
[ConditionalFact]
public virtual Task Array_of_sbyte()
=> this.Test<sbyte[]>(
[1, 2, 3],
new ReadOnlyMemoryEmbeddingGenerator<sbyte>([1, 2, 3]));
public new class Fixture : EmbeddingTypeTests<string>.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using CosmosNoSql.ConformanceTests.Support;
using VectorData.ConformanceTests.Support;
using VectorData.ConformanceTests.TypeTests;
using VectorData.ConformanceTests.Xunit;
using Xunit;
namespace CosmosNoSql.ConformanceTests.TypeTests;
public class CosmosNoSqlKeyTypeTests(CosmosNoSqlKeyTypeTests.Fixture fixture)
: KeyTypeTests(fixture), IClassFixture<CosmosNoSqlKeyTypeTests.Fixture>
{
[ConditionalFact]
public virtual Task String() => this.Test<string>("foo", "bar");
public new class Fixture : KeyTypeTests.Fixture
{
public override TestStore TestStore => CosmosNoSqlTestStore.Instance;
}
}
@@ -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,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>SemanticKernel.Connectors.CosmosNoSql.UnitTests</AssemblyName>
<RootNamespace>SemanticKernel.Connectors.CosmosNoSql.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>
<ProjectReference Include="..\..\..\src\VectorData\CosmosNoSql\CosmosNoSql.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.VectorData;
using Microsoft.Extensions.VectorData.ProviderServices;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlCollectionQueryBuilder"/> class.
/// </summary>
public sealed class CosmosNoSqlCollectionQueryBuilderTests
{
private const string ScorePropertyName = "TestScore";
private readonly CollectionModel _model = new CosmosNoSqlModelBuilder().BuildDynamic(
new()
{
Properties =
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreVectorProperty("TestProperty1", typeof(ReadOnlyMemory<float>), 10) { StorageName = "test_property_1" },
new VectorStoreDataProperty("TestProperty2", typeof(string)) { StorageName = "test_property_2" },
new VectorStoreDataProperty("TestProperty3", typeof(string)) { StorageName = "test_property_3" }
]
},
defaultEmbeddingGenerator: null);
[Fact]
public void BuildSearchQueryWithoutFilterDoesNotContainWhereClause()
{
// Arrange
var vector = new ReadOnlyMemory<float>([1f, 2f, 3f]);
var vectorPropertyName = "test_property_1";
// Act
var queryDefinition = CosmosNoSqlCollectionQueryBuilder.BuildSearchQuery<DummyType>(
vector,
keywords: null,
this._model,
vectorPropertyName,
distanceFunction: null,
textPropertyName: null,
ScorePropertyName,
filter: null,
scoreThreshold: null,
10,
5,
includeVectors: true);
var queryText = queryDefinition.QueryText;
var queryParameters = queryDefinition.GetQueryParameters();
// Assert
Assert.DoesNotContain("WHERE", queryText);
Assert.Contains("OFFSET 5 LIMIT 10", queryText);
Assert.Equal("@vector", queryParameters[0].Name);
Assert.Equal(vector, queryParameters[0].Value);
}
#pragma warning disable CA1812 // An internal class that is apparently never instantiated. If so, remove the code from the assembly.
private sealed class DummyType;
#pragma warning restore CA1812
}
@@ -0,0 +1,803 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Moq;
using Xunit;
using DistanceFunction = Microsoft.Extensions.VectorData.DistanceFunction;
using IndexKind = Microsoft.Extensions.VectorData.IndexKind;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosNoSqlCollection{TKey, TRecord}"/> class.
/// </summary>
public sealed class CosmosNoSqlCollectionTests
{
private readonly Mock<Database> _mockDatabase = new();
private readonly Mock<Container> _mockContainer = new();
public CosmosNoSqlCollectionTests()
{
this._mockDatabase.Setup(l => l.GetContainer(It.IsAny<string>())).Returns(this._mockContainer.Object);
var mockClient = new Mock<CosmosClient>();
mockClient.Setup(l => l.ClientOptions).Returns(new CosmosClientOptions() { UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default });
this._mockDatabase
.Setup(l => l.Client)
.Returns(mockClient.Object);
}
[Fact]
public void ConstructorForModelWithoutKeyThrowsException()
{
// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => new CosmosNoSqlCollection<string, object>(this._mockDatabase.Object, "collection"));
Assert.Contains("No key property found", exception.Message);
}
[Fact]
public void ConstructorWithoutSystemTextJsonSerializerOptionsThrowsArgumentException()
{
// Arrange
var mockDatabase = new Mock<Database>();
var mockClient = new Mock<CosmosClient>();
mockDatabase.Setup(l => l.Client).Returns(mockClient.Object);
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(mockDatabase.Object, "collection"));
Assert.Contains(nameof(CosmosClientOptions.UseSystemTextJsonSerializerWithOptions), exception.Message);
}
[Fact]
public void ConstructorWithDeclarativeModelInitializesCollection()
{
// Act & Assert
using var collection = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithImperativeModelInitializesCollection()
{
// Arrange
var definition = new VectorStoreCollectionDefinition
{
Properties = [new VectorStoreKeyProperty("Id", typeof(string))]
};
// Act
using var collection = new CosmosNoSqlCollection<string, TestModel>(
this._mockDatabase.Object,
"collection",
new() { Definition = definition });
// Assert
Assert.NotNull(collection);
}
[Theory]
[MemberData(nameof(CollectionExistsData))]
public async Task CollectionExistsReturnsValidResultAsync(List<string> collections, string collectionName, bool expectedResult)
{
// Arrange
var mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns(collections);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
collectionName);
// Act
var actualResult = await sut.CollectionExistsAsync();
// Assert
Assert.Equal(expectedResult, actualResult);
}
[Theory]
[InlineData(IndexingMode.Consistent)]
[InlineData(IndexingMode.Lazy)]
[InlineData(IndexingMode.None)]
public async Task EnsureCollectionExistsUsesValidContainerPropertiesAsync(IndexingMode indexingMode)
{
// Arrange
const string CollectionName = "collection";
var mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns([]);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, TestIndexingModel>(
this._mockDatabase.Object,
CollectionName,
new()
{
IndexingMode = indexingMode,
Automatic = indexingMode != IndexingMode.None
});
var expectedVectorEmbeddingPolicy = new VectorEmbeddingPolicy(
[
new Embedding
{
DataType = VectorDataType.Float32,
Dimensions = 2,
DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.Cosine,
Path = "/DescriptionEmbedding2"
},
new Embedding
{
DataType = VectorDataType.Uint8,
Dimensions = 3,
DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.DotProduct,
Path = "/DescriptionEmbedding3"
},
new Embedding
{
DataType = VectorDataType.Int8,
Dimensions = 4,
DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.Euclidean,
Path = "/DescriptionEmbedding4"
},
]);
var expectedIndexingPolicy = new IndexingPolicy
{
VectorIndexes =
[
new VectorIndexPath { Type = VectorIndexType.Flat, Path = "/DescriptionEmbedding2" },
new VectorIndexPath { Type = VectorIndexType.QuantizedFlat, Path = "/DescriptionEmbedding3" },
new VectorIndexPath { Type = VectorIndexType.DiskANN, Path = "/DescriptionEmbedding4" },
],
IndexingMode = indexingMode,
Automatic = indexingMode != IndexingMode.None
};
if (indexingMode != IndexingMode.None)
{
expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/IndexableData1/?" });
expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/IndexableData2/?" });
expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/" });
expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding2/*" });
expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding3/*" });
expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding4/*" });
}
var expectedContainerProperties = new ContainerProperties(CollectionName, "/id")
{
VectorEmbeddingPolicy = expectedVectorEmbeddingPolicy,
IndexingPolicy = expectedIndexingPolicy
};
// Act
await sut.EnsureCollectionExistsAsync();
// Assert
this._mockDatabase.Verify(l => l.CreateContainerAsync(
It.Is<ContainerProperties>(properties => this.VerifyContainerProperties(expectedContainerProperties, properties)),
It.IsAny<int?>(),
It.IsAny<RequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Theory]
[MemberData(nameof(EnsureCollectionExistsData))]
public async Task EnsureCollectionExistsInvokesValidMethodsAsync(List<string> collections, int actualCollectionCreations)
{
// Arrange
const string CollectionName = "collection";
var mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns(collections);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionExistsAsync();
// Assert
this._mockDatabase.Verify(l => l.CreateContainerAsync(
It.IsAny<ContainerProperties>(),
It.IsAny<int?>(),
It.IsAny<RequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Exactly(actualCollectionCreations));
}
[Theory]
[InlineData("recordKey", false)]
[InlineData("partitionKey", true)]
public async Task DeleteInvokesValidMethodsAsync(
string expectedPartitionKey,
bool useExplicitPartitionKey)
{
// Arrange
const string RecordKey = "recordKey";
const string PartitionKey = "partitionKey";
// Act
if (useExplicitPartitionKey)
{
using var sut = new CosmosNoSqlCollection<CosmosNoSqlKey, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
await ((VectorStoreCollection<CosmosNoSqlKey, CosmosNoSqlHotel>)sut).DeleteAsync(
new CosmosNoSqlKey(RecordKey, PartitionKey));
}
else
{
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
await ((VectorStoreCollection<string, CosmosNoSqlHotel>)sut).DeleteAsync(
RecordKey);
}
// Assert
this._mockContainer.Verify(l => l.DeleteItemAsync<JsonObject>(
RecordKey,
new PartitionKey(expectedPartitionKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task DeleteBatchInvokesValidMethodsAsync()
{
// Arrange
List<string> recordKeys = ["key1", "key2"];
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.DeleteAsync(recordKeys);
// Assert
foreach (var key in recordKeys)
{
this._mockContainer.Verify(l => l.DeleteItemAsync<JsonObject>(
key,
new PartitionKey(key),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
}
[Fact]
public async Task DeleteCollectionInvokesValidMethodsAsync()
{
// Arrange
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.EnsureCollectionDeletedAsync();
// Assert
this._mockContainer.Verify(l => l.DeleteContainerAsync(
It.IsAny<ContainerRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task GetReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
var jsonObject = new JsonObject { ["id"] = RecordKey, ["HotelName"] = "Test Name" };
var mockItemResponse = new Mock<ItemResponse<JsonObject>>();
mockItemResponse
.Setup(l => l.Resource)
.Returns(jsonObject);
this._mockContainer
.Setup(l => l.ReadItemAsync<JsonObject>(
RecordKey,
new PartitionKey(RecordKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockItemResponse.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
var result = await sut.GetAsync(RecordKey);
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.HotelId);
Assert.Equal("Test Name", result.HotelName);
}
[Fact]
public async Task GetBatchReturnsValidRecordAsync()
{
// Arrange
var jsonObject1 = new JsonObject { ["id"] = "key1", ["HotelName"] = "Test Name 1" };
var jsonObject2 = new JsonObject { ["id"] = "key2", ["HotelName"] = "Test Name 2" };
var jsonObject3 = new JsonObject { ["id"] = "key3", ["HotelName"] = "Test Name 3" };
var mockFeedResponse = new Mock<FeedResponse<JsonObject>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns([jsonObject1, jsonObject2, jsonObject3]);
this._mockContainer
.Setup(l => l.ReadManyItemsAsync<JsonObject>(
It.IsAny<IReadOnlyList<(string id, PartitionKey partitionKey)>>(),
It.IsAny<ReadManyRequestOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
var results = await sut.GetAsync(["key1", "key2", "key3"]).ToListAsync();
// Assert
Assert.NotNull(results[0]);
Assert.Equal("key1", results[0].HotelId);
Assert.Equal("Test Name 1", results[0].HotelName);
Assert.NotNull(results[1]);
Assert.Equal("key2", results[1].HotelId);
Assert.Equal("Test Name 2", results[1].HotelName);
Assert.NotNull(results[2]);
Assert.Equal("key3", results[2].HotelId);
Assert.Equal("Test Name 3", results[2].HotelName);
}
[Fact]
public async Task CanUpsertRecordAsync()
{
// Arrange
var hotel = new CosmosNoSqlHotel("key") { HotelName = "Test Name" };
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.UpsertAsync(hotel);
// Assert
this._mockContainer.Verify(l => l.UpsertItemAsync<JsonNode>(
It.Is<JsonNode>(node =>
node["id"]!.ToString() == "key" &&
node["HotelName"]!.ToString() == "Test Name"),
new PartitionKey("key"),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task CanUpsertManyRecordsAsync()
{
// Arrange
var hotel1 = new CosmosNoSqlHotel("key1") { HotelName = "Test Name 1" };
var hotel2 = new CosmosNoSqlHotel("key2") { HotelName = "Test Name 2" };
var hotel3 = new CosmosNoSqlHotel("key3") { HotelName = "Test Name 3" };
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.UpsertAsync([hotel1, hotel2, hotel3]);
}
[Fact]
public async Task VectorizedSearchReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
const double ExpectedScore = 0.99;
var jsonObject = new JsonObject
{
["id"] = RecordKey,
["HotelName"] = "Test Name",
["SimilarityScore"] = ExpectedScore
};
var mockFeedResponse = new Mock<FeedResponse<JsonObject>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns([jsonObject]);
var mockFeedIterator = new Mock<FeedIterator<JsonObject>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockContainer
.Setup(l => l.GetItemQueryIterator<JsonObject>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
var results = await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3).ToListAsync();
var result = results[0];
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.Record.HotelId);
Assert.Equal("Test Name", result.Record.HotelName);
Assert.Equal(ExpectedScore, result.Score);
}
[Fact]
public async Task VectorizedSearchWithUnsupportedVectorTypeThrowsExceptionAsync()
{
// Arrange
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act & Assert
await Assert.ThrowsAsync<NotSupportedException>(async () =>
await sut.SearchAsync(new List<double>([1, 2, 3]), top: 3).ToListAsync());
}
[Fact]
public async Task VectorizedSearchWithNonExistentVectorPropertyNameThrowsExceptionAsync()
{
// Arrange
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
var searchOptions = new VectorSearchOptions<CosmosNoSqlHotel> { VectorProperty = r => "non-existent-property" };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3, searchOptions).ToListAsync());
}
public static TheoryData<List<string>, string, bool> CollectionExistsData => new()
{
{ ["collection-2"], "collection-2", true },
{ [], "non-existent-collection", false }
};
public static TheoryData<List<string>, int> EnsureCollectionExistsData => new()
{
{ ["collection"], 0 },
{ [], 1 }
};
#region
private bool VerifyContainerProperties(ContainerProperties expected, ContainerProperties actual)
{
Assert.Equal(expected.Id, actual.Id);
Assert.Equal(expected.PartitionKeyPath, actual.PartitionKeyPath);
Assert.Equal(expected.IndexingPolicy.IndexingMode, actual.IndexingPolicy.IndexingMode);
Assert.Equal(expected.IndexingPolicy.Automatic, actual.IndexingPolicy.Automatic);
if (expected.IndexingPolicy.IndexingMode != IndexingMode.None)
{
for (var i = 0; i < expected.VectorEmbeddingPolicy.Embeddings.Count; i++)
{
var expectedEmbedding = expected.VectorEmbeddingPolicy.Embeddings[i];
var actualEmbedding = actual.VectorEmbeddingPolicy.Embeddings[i];
Assert.Equal(expectedEmbedding.DataType, actualEmbedding.DataType);
Assert.Equal(expectedEmbedding.Dimensions, actualEmbedding.Dimensions);
Assert.Equal(expectedEmbedding.DistanceFunction, actualEmbedding.DistanceFunction);
Assert.Equal(expectedEmbedding.Path, actualEmbedding.Path);
}
for (var i = 0; i < expected.IndexingPolicy.VectorIndexes.Count; i++)
{
var expectedIndexPath = expected.IndexingPolicy.VectorIndexes[i];
var actualIndexPath = actual.IndexingPolicy.VectorIndexes[i];
Assert.Equal(expectedIndexPath.Type, actualIndexPath.Type);
Assert.Equal(expectedIndexPath.Path, actualIndexPath.Path);
}
for (var i = 0; i < expected.IndexingPolicy.IncludedPaths.Count; i++)
{
var expectedIncludedPath = expected.IndexingPolicy.IncludedPaths[i].Path;
var actualIncludedPath = actual.IndexingPolicy.IncludedPaths[i].Path;
Assert.Equal(expectedIncludedPath, actualIncludedPath);
}
for (var i = 0; i < expected.IndexingPolicy.ExcludedPaths.Count; i++)
{
var expectedExcludedPath = expected.IndexingPolicy.ExcludedPaths[i].Path;
var actualExcludedPath = actual.IndexingPolicy.ExcludedPaths[i].Path;
Assert.Equal(expectedExcludedPath, actualExcludedPath);
}
}
return true;
}
#pragma warning disable CA1812
private sealed class TestModel
{
public string? Id { get; set; }
public string? HotelName { get; set; }
}
private sealed class TestIndexingModel
{
[VectorStoreKey]
public string? Id { get; set; }
[VectorStoreVector(Dimensions: 2, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Flat)]
public ReadOnlyMemory<float>? DescriptionEmbedding2 { get; set; }
[VectorStoreVector(Dimensions: 3, DistanceFunction = DistanceFunction.DotProductSimilarity, IndexKind = IndexKind.QuantizedFlat)]
public ReadOnlyMemory<byte>? DescriptionEmbedding3 { get; set; }
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.EuclideanDistance, IndexKind = IndexKind.DiskAnn)]
public ReadOnlyMemory<sbyte>? DescriptionEmbedding4 { get; set; }
[VectorStoreData(IsIndexed = true)]
public string? IndexableData1 { get; set; }
[VectorStoreData(IsFullTextIndexed = true)]
public string? IndexableData2 { get; set; }
[VectorStoreData]
public string? NonIndexableData1 { get; set; }
}
#pragma warning restore CA1812
[Fact]
public void ConstructorWithStringKeyInitializesCollection()
{
// String TKey should work; partition key auto-defaults to the key property.
using var collection = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithStringKeyAndExplicitPartitionKeyInitializesCollection()
{
// String TKey should work when partition key explicitly points to the key property.
using var collection = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = ["HotelId"] });
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithStringKeyRejectsNonKeyPartitionKey()
{
// String TKey requires the partition key to be the key property.
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = ["HotelName"] }));
Assert.Contains("partition key must be the key property", exception.Message);
}
[Fact]
public void ConstructorWithStringKeyRejectsEmptyPartitionKey()
{
// String TKey cannot use empty partition key (PartitionKey.None) - that requires CosmosNoSqlKey.
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = [] }));
Assert.Contains("partition key must be the key property", exception.Message);
}
[Fact]
public void ConstructorWithGuidKeyInitializesCollection()
{
// Guid TKey should work; partition key auto-defaults to the key property.
using var collection = new CosmosNoSqlCollection<Guid, GuidKeyModel>(
this._mockDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithGuidKeyRejectsNonKeyPartitionKey()
{
// Guid TKey requires the partition key to be the key property.
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<Guid, GuidKeyModel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = ["Name"] }));
Assert.Contains("partition key must be the key property", exception.Message);
}
[Fact]
public void ConstructorWithUnsupportedKeyTypeThrowsException()
{
var exception = Assert.Throws<NotSupportedException>(() => new CosmosNoSqlCollection<int, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection"));
Assert.Contains("string, Guid, or CosmosNoSqlKey", exception.Message);
}
[Fact]
public async Task DeleteWithStringKeyInvokesValidMethodsAsync()
{
// Arrange
const string RecordKey = "recordKey";
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.DeleteAsync(RecordKey);
// Assert: with string key, partition key = document id
this._mockContainer.Verify(l => l.DeleteItemAsync<JsonObject>(
RecordKey,
new PartitionKey(RecordKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task GetWithStringKeyReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
var jsonObject = new JsonObject { ["id"] = RecordKey, ["HotelName"] = "Test Name" };
var mockItemResponse = new Mock<ItemResponse<JsonObject>>();
mockItemResponse
.Setup(l => l.Resource)
.Returns(jsonObject);
this._mockContainer
.Setup(l => l.ReadItemAsync<JsonObject>(
RecordKey,
new PartitionKey(RecordKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockItemResponse.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
var result = await sut.GetAsync(RecordKey);
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.HotelId);
Assert.Equal("Test Name", result.HotelName);
}
#pragma warning disable CA1812
private sealed class GuidKeyModel
{
[VectorStoreKey]
public Guid Id { get; set; }
[VectorStoreData]
public string? Name { get; set; }
}
#pragma warning restore CA1812
#endregion
}
@@ -0,0 +1,313 @@
// 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 Microsoft.Extensions.VectorData;
using Microsoft.Extensions.VectorData.ProviderServices;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosNoSqlDynamicMapper"/> class.
/// </summary>
public sealed class CosmosNoSqlDynamicMapperTests
{
private static readonly JsonSerializerOptions s_jsonSerializerOptions = JsonSerializerOptions.Default;
private static readonly CollectionModel s_model = new CosmosNoSqlModelBuilder()
.BuildDynamic(
new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("BoolDataProp", typeof(bool)),
new VectorStoreDataProperty("NullableBoolDataProp", typeof(bool?)),
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("DateTimeOffsetDataProp", typeof(DateTimeOffset)),
new VectorStoreDataProperty("NullableDateTimeOffsetDataProp", typeof(DateTimeOffset?)),
new VectorStoreDataProperty("TagListDataProp", typeof(List<string>)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10),
new VectorStoreVectorProperty("ByteVector", typeof(ReadOnlyMemory<byte>), 10),
new VectorStoreVectorProperty("NullableByteVector", typeof(ReadOnlyMemory<byte>?), 10),
new VectorStoreVectorProperty("SByteVector", typeof(ReadOnlyMemory<sbyte>), 10),
new VectorStoreVectorProperty("NullableSByteVector", typeof(ReadOnlyMemory<sbyte>?), 10),
],
},
defaultEmbeddingGenerator: null);
private static readonly float[] s_floatVector = [1.0f, 2.0f, 3.0f];
private static readonly byte[] s_byteVector = [1, 2, 3];
private static readonly sbyte[] s_sbyteVector = [1, 2, 3];
private static readonly List<string> s_taglist = ["tag1", "tag2"];
[Fact]
public void MapFromDataToStorageModelMapsAllSupportedTypes()
{
// Arrange
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
var dataModel = new Dictionary<string, object?>
{
["Key"] = "key",
["BoolDataProp"] = true,
["NullableBoolDataProp"] = false,
["StringDataProp"] = "string",
["IntDataProp"] = 1,
["NullableIntDataProp"] = 2,
["LongDataProp"] = 3L,
["NullableLongDataProp"] = 4L,
["FloatDataProp"] = 5.0f,
["NullableFloatDataProp"] = 6.0f,
["DoubleDataProp"] = 7.0,
["NullableDoubleDataProp"] = 8.0,
["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_floatVector),
["NullableFloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
["ByteVector"] = new ReadOnlyMemory<byte>(s_byteVector),
["NullableByteVector"] = new ReadOnlyMemory<byte>(s_byteVector),
["SByteVector"] = new ReadOnlyMemory<sbyte>(s_sbyteVector),
["NullableSByteVector"] = new ReadOnlyMemory<sbyte>(s_sbyteVector)
};
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Equal("key", (string?)storageModel["id"]);
Assert.Equal(true, (bool?)storageModel["BoolDataProp"]);
Assert.Equal(false, (bool?)storageModel["NullableBoolDataProp"]);
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(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().GetValues<string>().ToArray());
Assert.Equal(s_floatVector, storageModel["FloatVector"]!.AsArray().GetValues<float>().ToArray());
Assert.Equal(s_floatVector, storageModel["NullableFloatVector"]!.AsArray().GetValues<float>().ToArray());
Assert.Equal(s_byteVector, storageModel["ByteVector"]!.AsArray().GetValues<byte>().ToArray());
Assert.Equal(s_byteVector, storageModel["NullableByteVector"]!.AsArray().GetValues<byte>().ToArray());
Assert.Equal(s_sbyteVector, storageModel["SByteVector"]!.AsArray().GetValues<sbyte>().ToArray());
Assert.Equal(s_sbyteVector, storageModel["NullableSByteVector"]!.AsArray().GetValues<sbyte>().ToArray());
}
[Fact]
public void MapFromDataToStorageModelMapsNullValues()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
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 CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Null(storageModel["StringDataProp"]);
Assert.Null(storageModel["NullableIntDataProp"]);
Assert.Null(storageModel["NullableFloatVector"]);
}
[Fact]
public void MapFromStorageToDataModelMapsAllSupportedTypes()
{
// Arrange
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
var storageModel = new JsonObject
{
["id"] = "key",
["BoolDataProp"] = true,
["NullableBoolDataProp"] = false,
["StringDataProp"] = "string",
["IntDataProp"] = 1,
["NullableIntDataProp"] = 2,
["LongDataProp"] = 3L,
["NullableLongDataProp"] = 4L,
["FloatDataProp"] = 5.0f,
["NullableFloatDataProp"] = 6.0f,
["DoubleDataProp"] = 7.0,
["NullableDoubleDataProp"] = 8.0,
["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(s_taglist.Select(l => (JsonValue)l).ToArray()),
["FloatVector"] = new JsonArray(s_floatVector.Select(l => (JsonValue)l).ToArray()),
["NullableFloatVector"] = new JsonArray(s_floatVector.Select(l => (JsonValue)l).ToArray()),
["ByteVector"] = new JsonArray(s_byteVector.Select(l => (JsonValue)l).ToArray()),
["NullableByteVector"] = new JsonArray(s_byteVector.Select(l => (JsonValue)l).ToArray()),
["SByteVector"] = new JsonArray(s_sbyteVector.Select(l => (JsonValue)l).ToArray()),
["NullableSByteVector"] = new JsonArray(s_sbyteVector.Select(l => (JsonValue)l).ToArray())
};
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.Equal(true, dataModel["BoolDataProp"]);
Assert.Equal(false, dataModel["NullableBoolDataProp"]);
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(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_floatVector, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["NullableFloatVector"]!)!.ToArray());
Assert.Equal(s_byteVector, ((ReadOnlyMemory<byte>)dataModel["ByteVector"]!).ToArray());
Assert.Equal(s_byteVector, ((ReadOnlyMemory<byte>)dataModel["NullableByteVector"]!)!.ToArray());
Assert.Equal(s_sbyteVector, ((ReadOnlyMemory<sbyte>)dataModel["SByteVector"]!).ToArray());
Assert.Equal(s_sbyteVector, ((ReadOnlyMemory<sbyte>)dataModel["NullableSByteVector"]!)!.ToArray());
}
[Fact]
public void MapFromStorageToDataModelMapsNullValues()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
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
{
["id"] = "key",
["StringDataProp"] = null,
["NullableIntDataProp"] = null,
["NullableFloatVector"] = null
};
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// 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 sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
var storageModel = new JsonObject();
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => sut.MapFromStorageToDataModel(storageModel, includeVectors: true));
}
[Fact]
public void MapFromDataToStorageModelSkipsMissingProperties()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
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 CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Equal("key", (string?)storageModel["id"]);
Assert.False(storageModel.ContainsKey("StringDataProp"));
Assert.False(storageModel.ContainsKey("FloatVector"));
}
[Fact]
public void MapFromStorageToDataModelSkipsMissingProperties()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
],
};
var storageModel = new JsonObject
{
["id"] = "key"
};
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.False(dataModel.ContainsKey("StringDataProp"));
Assert.False(dataModel.ContainsKey("FloatVector"));
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.Extensions.VectorData;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
public class CosmosNoSqlHotel(string hotelId)
{
/// <summary>The key of the record.</summary>
[VectorStoreKey]
public string HotelId { get; init; } = hotelId;
/// <summary>A string metadata field.</summary>
[VectorStoreData]
public string? HotelName { get; set; }
/// <summary>An int metadata field.</summary>
[VectorStoreData]
public int HotelCode { get; set; }
/// <summary>A float metadata field.</summary>
[VectorStoreData]
public float? HotelRating { get; set; }
/// <summary>A bool metadata field.</summary>
[VectorStoreData(StorageName = "parking_is_included")]
public bool ParkingIncluded { get; set; }
/// <summary>An array metadata field.</summary>
[VectorStoreData]
public List<string> Tags { get; set; } = [];
/// <summary>A data field.</summary>
[VectorStoreData]
public string? Description { get; set; }
/// <summary>A vector field.</summary>
[JsonPropertyName("description_embedding")]
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Flat)]
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
}
@@ -0,0 +1,82 @@
// 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 Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosNoSqlMapper{TRecord}"/> class.
/// </summary>
public sealed class CosmosNoSqlMapperTests
{
private readonly CosmosNoSqlMapper<CosmosNoSqlHotel> _sut
= new(
new CosmosNoSqlModelBuilder().BuildDynamic(
new()
{
Properties =
[
new VectorStoreKeyProperty("HotelId", typeof(string)),
new VectorStoreVectorProperty("TestProperty1", typeof(ReadOnlyMemory<float>), 10) { StorageName = "test_property_1" },
new VectorStoreDataProperty("TestProperty2", typeof(string)) { StorageName = "test_property_2" },
new VectorStoreDataProperty("TestProperty3", typeof(string)) { StorageName = "test_property_3" }
]
},
defaultEmbeddingGenerator: null,
JsonSerializerOptions.Default),
JsonSerializerOptions.Default);
[Fact]
public void MapFromDataToStorageModelReturnsValidObject()
{
// Arrange
var hotel = new CosmosNoSqlHotel("key")
{
HotelName = "Test Name",
Tags = ["tag1", "tag2"],
DescriptionEmbedding = new ReadOnlyMemory<float>([1f, 2f, 3f])
};
// Act
var document = this._sut.MapFromDataToStorageModel(hotel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.NotNull(document);
Assert.Equal("key", document["id"]!.GetValue<string>());
Assert.Equal("Test Name", document["HotelName"]!.GetValue<string>());
Assert.Equal(["tag1", "tag2"], document["Tags"]!.AsArray().Select(l => l!.GetValue<string>()));
Assert.Equal([1f, 2f, 3f], document["description_embedding"]!.AsArray().Select(l => l!.GetValue<float>()));
}
[Fact]
public void MapFromStorageToDataModelReturnsValidObject()
{
// Arrange
var document = new JsonObject
{
["id"] = "key",
["HotelName"] = "Test Name",
["Tags"] = new JsonArray(new List<string> { "tag1", "tag2" }.Select(l => JsonValue.Create(l)).ToArray()),
["description_embedding"] = new JsonArray(new List<float> { 1f, 2f, 3f }.Select(l => JsonValue.Create(l)).ToArray()),
};
// Act
var hotel = this._sut.MapFromStorageToDataModel(document, new());
// Assert
Assert.NotNull(hotel);
Assert.Equal("key", hotel.HotelId);
Assert.Equal("Test Name", hotel.HotelName);
Assert.Equal(["tag1", "tag2"], hotel.Tags);
Assert.True(new ReadOnlyMemory<float>([1f, 2f, 3f]).Span.SequenceEqual(hotel.DescriptionEmbedding!.Value.Span));
}
}
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Moq;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore"/> class.
/// </summary>
public sealed class CosmosNoSqlVectorStoreTests
{
private readonly Mock<Database> _mockDatabase = new();
public CosmosNoSqlVectorStoreTests()
{
var mockClient = new Mock<CosmosClient>();
mockClient.Setup(l => l.ClientOptions).Returns(new CosmosClientOptions() { UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default });
this._mockDatabase
.Setup(l => l.Client)
.Returns(mockClient.Object);
}
[Fact]
public void GetCollectionWithNotSupportedKeyThrowsException()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act & Assert
Assert.Throws<NotSupportedException>(() => sut.GetCollection<byte[], CosmosNoSqlHotel>("collection"));
}
[Fact]
public void GetCollectionWithSupportedKeyReturnsCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<CosmosNoSqlKey, CosmosNoSqlHotel>("collection1");
// Assert
Assert.NotNull(collection);
}
[Fact]
public void GetCollectionWithStringKeyReturnsCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<string, CosmosNoSqlHotel>("collection1");
// Assert
Assert.NotNull(collection);
}
[Fact]
public void GetCollectionWithGuidKeyReturnsCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<Guid, GuidKeyHotel>("collection1");
// Assert
Assert.NotNull(collection);
}
[Fact]
public void GetCollectionWithoutFactoryReturnsDefaultCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<CosmosNoSqlKey, CosmosNoSqlHotel>("collection");
// Assert
Assert.NotNull(collection);
}
[Fact]
public async Task ListCollectionNamesReturnsCollectionNamesAsync()
{
// Arrange
var expectedCollectionNames = new List<string> { "collection-1", "collection-2", "collection-3" };
var mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns(expectedCollectionNames);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync();
// Assert
Assert.Equal(expectedCollectionNames, actualCollectionNames);
}
#pragma warning disable CA1812
private sealed class GuidKeyHotel
{
[Microsoft.Extensions.VectorData.VectorStoreKey]
public Guid Id { get; set; }
[Microsoft.Extensions.VectorData.VectorStoreData]
public string? Name { get; set; }
}
#pragma warning restore CA1812
}
@@ -0,0 +1,24 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<NoWarn>$(NoWarn);MEVD9000,MEVD9001</NoWarn> <!-- Experimental MEVD connector-facing APIs -->
<NoWarn>$(NoWarn);CA1515</NoWarn> <!-- Because an application's API isn't typically referenced from outside the assembly, types can be made internal -->
<NoWarn>$(NoWarn);CA1707</NoWarn> <!-- Remove the underscores from member name -->
<NoWarn>$(NoWarn);CA1716</NoWarn> <!-- Rename virtual/interface member so that it no longer conflicts with the reserved language keyword -->
<NoWarn>$(NoWarn);CA1720</NoWarn> <!-- Identifier contains type name -->
<NoWarn>$(NoWarn);CA1721</NoWarn> <!-- The property name X is confusing given the existence of method Y. Rename or remove one of these members. -->
<NoWarn>$(NoWarn);CA1819</NoWarn> <!-- Properties should not return arrays -->
<NoWarn>$(NoWarn);CS1819</NoWarn> <!-- Properties should not return arrays -->
<NoWarn>$(NoWarn);CA1861</NoWarn> <!-- Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array -->
<NoWarn>$(NoWarn);CA1863</NoWarn> <!-- Cache a 'CompositeFormat' for repeated use in this formatting operation -->
<NoWarn>$(NoWarn);CA2007;VSTHRD111</NoWarn> <!-- Consider calling ConfigureAwait on the awaited task -->
<NoWarn>$(NoWarn);CS1591</NoWarn> <!-- Missing XML comment for publicly visible type or member -->
<NoWarn>$(NoWarn);IDE1006</NoWarn> <!-- Naming rule violation: Missing suffix: 'Async' -->
<!-- Remove this after switching to C# 14 -->
<NoWarn>$(NoWarn);IDE0340</NoWarn> <!-- Use unbound generic type -->
</PropertyGroup>
</Project>
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;net472</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<RootNamespace>InMemory.ConformanceTests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\VectorData\InMemory\InMemory.csproj" />
<ProjectReference Include="..\VectorData.ConformanceTests\VectorData.ConformanceTests.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using InMemory.ConformanceTests.Support;
using VectorData.ConformanceTests;
using Xunit;
namespace InMemory.ConformanceTests;
public class InMemoryCollectionManagementTests(InMemoryFixture fixture)
: CollectionManagementTests<string>(fixture), IClassFixture<InMemoryFixture>
{
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using InMemory.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace InMemory.ConformanceTests;
public class InMemoryDistanceFunctionTests(InMemoryDistanceFunctionTests.Fixture fixture)
: DistanceFunctionTests<int>(fixture), IClassFixture<InMemoryDistanceFunctionTests.Fixture>
{
public override Task EuclideanSquaredDistance() => Assert.ThrowsAsync<NotSupportedException>(base.EuclideanSquaredDistance);
public override Task NegativeDotProductSimilarity() => Assert.ThrowsAsync<NotSupportedException>(base.NegativeDotProductSimilarity);
public override Task HammingDistance() => Assert.ThrowsAsync<NotSupportedException>(base.HammingDistance);
public override Task ManhattanDistance() => Assert.ThrowsAsync<NotSupportedException>(base.ManhattanDistance);
public new class Fixture() : DistanceFunctionTests<int>.Fixture
{
public override TestStore TestStore => InMemoryTestStore.Instance;
}
}
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft. All rights reserved.
using InMemory.ConformanceTests.Support;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.VectorData;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace InMemory.ConformanceTests;
public class InMemoryEmbeddingGenerationTests(InMemoryEmbeddingGenerationTests.StringVectorFixture stringVectorFixture, InMemoryEmbeddingGenerationTests.RomOfFloatVectorFixture romOfFloatVectorFixture)
: EmbeddingGenerationTests<int>(stringVectorFixture, romOfFloatVectorFixture), IClassFixture<InMemoryEmbeddingGenerationTests.StringVectorFixture>, IClassFixture<InMemoryEmbeddingGenerationTests.RomOfFloatVectorFixture>
{
// InMemory doesn't allowing accessing the same collection via different .NET types (it's unique in this).
// The following dynamic tests attempt to access the fixture collection - which is created with Record - via
// Dictionary<string, object?>.
public override Task SearchAsync_with_property_generator_dynamic() => Task.CompletedTask;
public override Task UpsertAsync_dynamic() => Task.CompletedTask;
public override Task UpsertAsync_batch_dynamic() => Task.CompletedTask;
// The same applies to the custom type test:
public override Task SearchAsync_with_custom_input_type() => Task.CompletedTask;
// The test relies on creating a new InMemoryVectorStore configured with a store-default generator, but with InMemory that store
// doesn't share the seeded data with the fixture store (since each InMemoryVectorStore has its own private data).
// Test coverage is already largely sufficient via the property and collection tests.
public override Task SearchAsync_with_store_generator() => Task.CompletedTask;
public new class StringVectorFixture : EmbeddingGenerationTests<int>.StringVectorFixture
{
public override TestStore TestStore => InMemoryTestStore.Instance;
// Note that with InMemory specifically, we can't create a vector store with an embedding generator, since it wouldn't share the seeded data with the fixture store.
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> InMemoryTestStore.Instance.DefaultVectorStore;
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
// The InMemory DI methods register a new vector store instance, which doesn't share the collection seeded by the
// fixture and the test fails.
// services => services.AddInMemoryVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
// The InMemory DI methods register a new vector store instance, which doesn't share the collection seeded by the
// fixture and the test fails.
// services => services.AddInMemoryVectorStoreRecordCollection<int, RecordWithAttributes>(this.CollectionName)
];
}
public new class RomOfFloatVectorFixture : EmbeddingGenerationTests<int>.RomOfFloatVectorFixture
{
public override TestStore TestStore => InMemoryTestStore.Instance;
// Note that with InMemory specifically, we can't create a vector store with an embedding generator, since it wouldn't share the seeded data with the fixture store.
public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator)
=> InMemoryTestStore.Instance.DefaultVectorStore;
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates =>
[
// The InMemory DI methods register a new vector store instance, which doesn't share the collection seeded by the
// fixture and the test fails.
// services => services.AddInMemoryVectorStore()
];
public override Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates =>
[
// The InMemory DI methods register a new vector store instance, which doesn't share the collection seeded by the
// fixture and the test fails.
// services => services.AddInMemoryVectorStoreRecordCollection<int, RecordWithAttributes>(this.CollectionName)
];
}
}
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
using InMemory.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace InMemory.ConformanceTests;
public class InMemoryFilterTests(InMemoryFilterTests.Fixture fixture)
: FilterTests<int>(fixture), IClassFixture<InMemoryFilterTests.Fixture>
{
public new class Fixture : FilterTests<int>.Fixture
{
public override TestStore TestStore => InMemoryTestStore.Instance;
// BaseFilterTests attempts to create two InMemoryVectorStoreRecordCollection with different .NET types:
// 1. One for strongly-typed mapping (TRecord=FilterRecord)
// 2. One for dynamic mapping (TRecord=Dictionary<string, object?>)
// Unfortunately, InMemoryVectorStore does not allow mapping the same collection name to different types;
// at the same time, it simply evaluates all filtering via .NET AsQueryable(), so actual test coverage
// isn't very important here. So we disable the dynamic tests.
public override bool TestDynamic => false;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using InMemory.ConformanceTests.Support;
using VectorData.ConformanceTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace InMemory.ConformanceTests;
public class InMemoryIndexKindTests(InMemoryIndexKindTests.Fixture fixture)
: IndexKindTests<int>(fixture), IClassFixture<InMemoryIndexKindTests.Fixture>
{
public new class Fixture() : IndexKindTests<int>.Fixture
{
public override TestStore TestStore => InMemoryTestStore.Instance;
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using VectorData.ConformanceTests;
namespace InMemory.ConformanceTests;
public class InMemoryTestSuiteImplementationTests : TestSuiteImplementationTests
{
protected override ICollection<Type> IgnoredTestBases { get; } =
[
typeof(DependencyInjectionTests<,,,>),
typeof(DependencyInjectionTests<>),
// Hybrid search not supported
typeof(HybridSearchTests<>)
];
}
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft. All rights reserved.
using InMemory.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace InMemory.ConformanceTests.ModelTests;
public class InMemoryBasicModelTests(InMemoryBasicModelTests.Fixture fixture)
: BasicModelTests<int>(fixture), IClassFixture<InMemoryBasicModelTests.Fixture>
{
public override async Task GetAsync_single_record(bool includeVectors)
{
if (includeVectors)
{
await base.GetAsync_single_record(includeVectors);
return;
}
// InMemory always returns the vectors (IncludeVectors = false isn't respected)
var expectedRecord = fixture.TestData[0];
var received = await fixture.Collection.GetAsync(expectedRecord.Key, new() { IncludeVectors = false });
expectedRecord.AssertEqual(received, includeVectors: true, fixture.TestStore.VectorsComparable);
}
public override async Task GetAsync_multiple_records(bool includeVectors)
{
if (includeVectors)
{
await base.GetAsync_multiple_records(includeVectors);
return;
}
// InMemory always returns the vectors (IncludeVectors = false isn't respected)
var expectedRecords = fixture.TestData.Take(2); // the last two records can get deleted by other tests
var ids = expectedRecords.Select(record => record.Key);
var received = await fixture.Collection.GetAsync(ids, new() { IncludeVectors = false }).ToArrayAsync();
foreach (var record in expectedRecords)
{
record.AssertEqual(
received.Single(r => r.Key.Equals(record.Key)),
includeVectors: true,
fixture.TestStore.VectorsComparable);
}
}
public override async Task GetAsync_with_filter(bool includeVectors)
{
if (includeVectors)
{
await base.GetAsync_with_filter(includeVectors);
return;
}
// InMemory always returns the vectors (IncludeVectors = false isn't respected)
var expectedRecord = fixture.TestData[0];
var results = await this.Collection.GetAsync(
r => r.Number == 1,
top: 2,
new() { IncludeVectors = includeVectors })
.ToListAsync();
var receivedRecord = Assert.Single(results);
expectedRecord.AssertEqual(receivedRecord, includeVectors: true, fixture.TestStore.VectorsComparable);
}
public new class Fixture : BasicModelTests<int>.Fixture
{
public override TestStore TestStore => InMemoryTestStore.Instance;
}
}
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.
using InMemory.ConformanceTests.Support;
using VectorData.ConformanceTests.ModelTests;
using VectorData.ConformanceTests.Support;
using Xunit;
namespace InMemory.ConformanceTests.ModelTests;
public class InMemoryDynamicModelTests(InMemoryDynamicModelTests.Fixture fixture)
: DynamicModelTests<int>(fixture), IClassFixture<InMemoryDynamicModelTests.Fixture>
{
public override async Task GetAsync_single_record(bool includeVectors)
{
if (includeVectors)
{
await base.GetAsync_single_record(includeVectors);
return;
}
// InMemory always returns the vectors (IncludeVectors = false isn't respected)
var expectedRecord = fixture.TestData[0];
var received = await fixture.Collection.GetAsync(
(int)expectedRecord[KeyPropertyName]!,
new() { IncludeVectors = false });
AssertEquivalent(expectedRecord, received, includeVectors: true, fixture.TestStore.VectorsComparable);
}
public override async Task GetAsync_multiple_records(bool includeVectors)
{
if (includeVectors)
{
await base.GetAsync_multiple_records(includeVectors);
return;
}
// InMemory always returns the vectors (IncludeVectors = false isn't respected)
var expectedRecords = fixture.TestData.Take(2);
var ids = expectedRecords.Select(record => record[KeyPropertyName]!);
var received = await fixture.Collection.GetAsync(ids, new() { IncludeVectors = false }).ToArrayAsync();
foreach (var record in expectedRecords)
{
AssertEquivalent(
record,
received.Single(r => r[KeyPropertyName]!.Equals(record[KeyPropertyName])),
includeVectors: true,
fixture.TestStore.VectorsComparable);
}
}
public override async Task GetAsync_with_filter(bool includeVectors)
{
if (includeVectors)
{
await base.GetAsync_with_filter(includeVectors);
return;
}
// InMemory always returns the vectors (IncludeVectors = false isn't respected)
var expectedRecord = fixture.TestData[0];
var results = await fixture.Collection.GetAsync(
r => (int)r[IntegerPropertyName]! == 1,
top: 2,
new() { IncludeVectors = includeVectors })
.ToListAsync();
var receivedRecord = Assert.Single(results);
AssertEquivalent(expectedRecord, receivedRecord, includeVectors: true, fixture.TestStore.VectorsComparable);
}
public new class Fixture : DynamicModelTests<int>.Fixture
{
public override TestStore TestStore => InMemoryTestStore.Instance;
}
}

Some files were not shown because too many files have changed in this diff Show More