chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user