chore: import upstream snapshot with attribution
This commit is contained in:
+43
@@ -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>
|
||||
+106
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -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>;
|
||||
+118
@@ -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!));
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+63
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using VectorData.ConformanceTests;
|
||||
|
||||
namespace AzureAISearch.ConformanceTests;
|
||||
|
||||
public class AzureAISearchTestSuiteImplementationTests : TestSuiteImplementationTests;
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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]
|
||||
+168
@@ -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;
|
||||
}
|
||||
+33
@@ -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"];
|
||||
}
|
||||
}
|
||||
+68
@@ -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));
|
||||
}
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
+78
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+21
@@ -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>
|
||||
+222
@@ -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; }
|
||||
}
|
||||
}
|
||||
+12
@@ -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>
|
||||
{
|
||||
}
|
||||
+40
@@ -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>
|
||||
+142
@@ -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: ""));
|
||||
}
|
||||
}
|
||||
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
+59
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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<>),
|
||||
];
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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]
|
||||
+19
@@ -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;
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
+40
@@ -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>
|
||||
+12
@@ -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>
|
||||
{
|
||||
}
|
||||
+126
@@ -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; }
|
||||
}
|
||||
}
|
||||
+148
@@ -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: ""));
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
+59
@@ -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;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using VectorData.ConformanceTests;
|
||||
|
||||
namespace CosmosNoSql.ConformanceTests;
|
||||
|
||||
public class CosmosNoSqlTestSuiteImplementationTests : TestSuiteImplementationTests;
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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]
|
||||
+19
@@ -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;
|
||||
}
|
||||
+26
@@ -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();
|
||||
}
|
||||
}
|
||||
+39
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
+61
@@ -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;
|
||||
}
|
||||
}
|
||||
+21
@@ -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>
|
||||
+67
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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<>)
|
||||
];
|
||||
}
|
||||
+76
@@ -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;
|
||||
}
|
||||
}
|
||||
+79
@@ -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
Reference in New Issue
Block a user