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,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SemanticKernel.Connectors.Weaviate.UnitTests</AssemblyName>
|
||||
<RootNamespace>SemanticKernel.Connectors.Weaviate.UnitTests</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);SKEXP0001,VSTHRD111,CA2007</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/HttpMessageHandlerStub.cs"
|
||||
Link="%(RecursiveDir)%(Filename)%(Extension)"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\VectorData\Weaviate\Weaviate.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.Weaviate;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="WeaviateCollectionCreateMapping"/> class.
|
||||
/// </summary>
|
||||
public sealed class WeaviateCollectionCreateMappingTests
|
||||
{
|
||||
private const bool HasNamedVectors = true;
|
||||
|
||||
[Fact]
|
||||
public void ItThrowsExceptionWithInvalidIndexKind()
|
||||
{
|
||||
// Arrange
|
||||
var model = new WeaviateModelBuilder(HasNamedVectors)
|
||||
.BuildDynamic(
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10) { IndexKind = "non-existent-index-kind" }
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() => WeaviateCollectionCreateMapping.MapToSchema(collectionName: "CollectionName", HasNamedVectors, model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(IndexKind.Hnsw, "hnsw")]
|
||||
[InlineData(IndexKind.Flat, "flat")]
|
||||
[InlineData(IndexKind.Dynamic, "dynamic")]
|
||||
public void ItReturnsCorrectSchemaWithValidIndexKind(string indexKind, string expectedIndexKind)
|
||||
{
|
||||
// Arrange
|
||||
var model = new WeaviateModelBuilder(HasNamedVectors)
|
||||
.BuildDynamic(
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10) { IndexKind = indexKind }
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
// Act
|
||||
var schema = WeaviateCollectionCreateMapping.MapToSchema(collectionName: "CollectionName", HasNamedVectors, model);
|
||||
var actualIndexKind = schema.VectorConfigurations["Vector"].VectorIndexType;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedIndexKind, actualIndexKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItThrowsExceptionWithUnsupportedDistanceFunction()
|
||||
{
|
||||
// Arrange
|
||||
var model = new WeaviateModelBuilder(HasNamedVectors)
|
||||
.BuildDynamic(
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10) { DistanceFunction = "unsupported-distance-function" }
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => WeaviateCollectionCreateMapping.MapToSchema(collectionName: "CollectionName", HasNamedVectors, model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(DistanceFunction.CosineDistance, "cosine")]
|
||||
[InlineData(DistanceFunction.NegativeDotProductSimilarity, "dot")]
|
||||
[InlineData(DistanceFunction.EuclideanSquaredDistance, "l2-squared")]
|
||||
[InlineData(DistanceFunction.HammingDistance, "hamming")]
|
||||
[InlineData(DistanceFunction.ManhattanDistance, "manhattan")]
|
||||
public void ItReturnsCorrectSchemaWithValidDistanceFunction(string distanceFunction, string expectedDistanceFunction)
|
||||
{
|
||||
// Arrange
|
||||
var model = new WeaviateModelBuilder(HasNamedVectors)
|
||||
.BuildDynamic(
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10) { DistanceFunction = distanceFunction }
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
// Act
|
||||
var schema = WeaviateCollectionCreateMapping.MapToSchema(collectionName: "CollectionName", HasNamedVectors, model);
|
||||
|
||||
var actualDistanceFunction = schema.VectorConfigurations["Vector"].VectorIndexConfig?.Distance;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedDistanceFunction, actualDistanceFunction);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(string), "text")]
|
||||
[InlineData(typeof(List<string>), "text[]")]
|
||||
[InlineData(typeof(int), "int")]
|
||||
[InlineData(typeof(int?), "int")]
|
||||
[InlineData(typeof(List<int>), "int[]")]
|
||||
[InlineData(typeof(List<int?>), "int[]")]
|
||||
[InlineData(typeof(long), "int")]
|
||||
[InlineData(typeof(long?), "int")]
|
||||
[InlineData(typeof(List<long>), "int[]")]
|
||||
[InlineData(typeof(List<long?>), "int[]")]
|
||||
[InlineData(typeof(short), "int")]
|
||||
[InlineData(typeof(short?), "int")]
|
||||
[InlineData(typeof(List<short>), "int[]")]
|
||||
[InlineData(typeof(List<short?>), "int[]")]
|
||||
[InlineData(typeof(byte), "int")]
|
||||
[InlineData(typeof(byte?), "int")]
|
||||
[InlineData(typeof(List<byte>), "int[]")]
|
||||
[InlineData(typeof(List<byte?>), "int[]")]
|
||||
[InlineData(typeof(float), "number")]
|
||||
[InlineData(typeof(float?), "number")]
|
||||
[InlineData(typeof(List<float>), "number[]")]
|
||||
[InlineData(typeof(List<float?>), "number[]")]
|
||||
[InlineData(typeof(double), "number")]
|
||||
[InlineData(typeof(double?), "number")]
|
||||
[InlineData(typeof(List<double>), "number[]")]
|
||||
[InlineData(typeof(List<double?>), "number[]")]
|
||||
[InlineData(typeof(decimal), "number")]
|
||||
[InlineData(typeof(decimal?), "number")]
|
||||
[InlineData(typeof(List<decimal>), "number[]")]
|
||||
[InlineData(typeof(List<decimal?>), "number[]")]
|
||||
[InlineData(typeof(DateTime), "date")]
|
||||
[InlineData(typeof(DateTime?), "date")]
|
||||
[InlineData(typeof(List<DateTime>), "date[]")]
|
||||
[InlineData(typeof(List<DateTime?>), "date[]")]
|
||||
[InlineData(typeof(DateTimeOffset), "date")]
|
||||
[InlineData(typeof(DateTimeOffset?), "date")]
|
||||
[InlineData(typeof(List<DateTimeOffset>), "date[]")]
|
||||
[InlineData(typeof(List<DateTimeOffset?>), "date[]")]
|
||||
[InlineData(typeof(Guid), "uuid")]
|
||||
[InlineData(typeof(Guid?), "uuid")]
|
||||
[InlineData(typeof(List<Guid>), "uuid[]")]
|
||||
[InlineData(typeof(List<Guid?>), "uuid[]")]
|
||||
[InlineData(typeof(bool), "boolean")]
|
||||
[InlineData(typeof(bool?), "boolean")]
|
||||
[InlineData(typeof(List<bool>), "boolean[]")]
|
||||
[InlineData(typeof(List<bool?>), "boolean[]")]
|
||||
public void ItMapsPropertyCorrectly(Type propertyType, string expectedPropertyType)
|
||||
{
|
||||
// Arrange
|
||||
var model = new WeaviateModelBuilder(HasNamedVectors)
|
||||
.BuildDynamic(
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreDataProperty("PropertyName", propertyType) { IsIndexed = true, IsFullTextIndexed = true },
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null,
|
||||
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
// Act
|
||||
var schema = WeaviateCollectionCreateMapping.MapToSchema(collectionName: "CollectionName", HasNamedVectors, model);
|
||||
|
||||
var property = schema.Properties[0];
|
||||
|
||||
// Assert
|
||||
Assert.Equal("propertyName", property.Name);
|
||||
Assert.Equal(expectedPropertyType, property.DataType[0]);
|
||||
Assert.True(property.IndexSearchable);
|
||||
Assert.True(property.IndexFilterable);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void ItReturnsCorrectSchemaWithValidVectorConfiguration(bool hasNamedVectors)
|
||||
{
|
||||
// Arrange
|
||||
var model = new WeaviateModelBuilder(hasNamedVectors)
|
||||
.BuildDynamic(
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 4)
|
||||
{
|
||||
DistanceFunction = DistanceFunction.CosineDistance,
|
||||
IndexKind = IndexKind.Hnsw
|
||||
}
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
// Act
|
||||
var schema = WeaviateCollectionCreateMapping.MapToSchema(collectionName: "CollectionName", hasNamedVectors, model);
|
||||
|
||||
// Assert
|
||||
if (hasNamedVectors)
|
||||
{
|
||||
Assert.Null(schema.VectorIndexConfig?.Distance);
|
||||
Assert.Null(schema.VectorIndexType);
|
||||
Assert.True(schema.VectorConfigurations.ContainsKey("Vector"));
|
||||
|
||||
Assert.Equal("cosine", schema.VectorConfigurations["Vector"].VectorIndexConfig?.Distance);
|
||||
Assert.Equal("hnsw", schema.VectorConfigurations["Vector"].VectorIndexType);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.False(schema.VectorConfigurations.ContainsKey("Vector"));
|
||||
|
||||
Assert.Equal("cosine", schema.VectorIndexConfig?.Distance);
|
||||
Assert.Equal("hnsw", schema.VectorIndexType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.SemanticKernel.Connectors.Weaviate;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="WeaviateCollectionSearchMapping"/> class.
|
||||
/// </summary>
|
||||
public sealed class WeaviateCollectionSearchMappingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void MapSearchResultByDefaultReturnsValidResult(bool hasNamedVectors)
|
||||
{
|
||||
// Arrange
|
||||
var jsonObject = new JsonObject
|
||||
{
|
||||
["_additional"] = new JsonObject
|
||||
{
|
||||
["distance"] = 0.5,
|
||||
["id"] = "55555555-5555-5555-5555-555555555555"
|
||||
},
|
||||
["description"] = "This is a great hotel.",
|
||||
["hotelCode"] = 42,
|
||||
["hotelName"] = "My Hotel",
|
||||
["hotelRating"] = 4.5,
|
||||
["parking_is_included"] = true,
|
||||
["tags"] = new JsonArray(new List<string> { "t1", "t2" }.Select(l => (JsonNode)l).ToArray()),
|
||||
["timestamp"] = "2024-08-28T10:11:12-07:00"
|
||||
};
|
||||
|
||||
var vector = new JsonArray(new List<float> { 30, 31, 32, 33 }.Select(l => (JsonNode)l).ToArray());
|
||||
|
||||
if (hasNamedVectors)
|
||||
{
|
||||
jsonObject["_additional"]!["vectors"] = new JsonObject
|
||||
{
|
||||
["descriptionEmbedding"] = vector
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
jsonObject["_additional"]!["vector"] = vector;
|
||||
}
|
||||
|
||||
// Act
|
||||
var (storageModel, score) = WeaviateCollectionSearchMapping.MapSearchResult(jsonObject, "distance", hasNamedVectors);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0.5, score);
|
||||
|
||||
Assert.Equal("55555555-5555-5555-5555-555555555555", storageModel["id"]!.GetValue<string>());
|
||||
Assert.Equal("This is a great hotel.", storageModel["properties"]!["description"]!.GetValue<string>());
|
||||
Assert.Equal(42, storageModel["properties"]!["hotelCode"]!.GetValue<int>());
|
||||
Assert.Equal(4.5, storageModel["properties"]!["hotelRating"]!.GetValue<double>());
|
||||
Assert.Equal("My Hotel", storageModel["properties"]!["hotelName"]!.GetValue<string>());
|
||||
Assert.True(storageModel["properties"]!["parking_is_included"]!.GetValue<bool>());
|
||||
Assert.Equal(["t1", "t2"], storageModel["properties"]!["tags"]!.AsArray().Select(l => l!.GetValue<string>()));
|
||||
Assert.Equal("2024-08-28T10:11:12-07:00", storageModel["properties"]!["timestamp"]!.GetValue<string>());
|
||||
|
||||
var vectorProperty = hasNamedVectors ? storageModel["vectors"]!["descriptionEmbedding"] : storageModel["vector"];
|
||||
|
||||
Assert.Equal([30f, 31f, 32f, 33f], vectorProperty!.AsArray().Select(l => l!.GetValue<float>()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.Weaviate;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="WeaviateCollection{TKey, TRecord}"/> class.
|
||||
/// </summary>
|
||||
public sealed class WeaviateCollectionTests : IDisposable
|
||||
{
|
||||
private readonly HttpMessageHandlerStub _messageHandlerStub = new();
|
||||
private readonly HttpClient _mockHttpClient;
|
||||
|
||||
public WeaviateCollectionTests()
|
||||
{
|
||||
this._mockHttpClient = new(this._messageHandlerStub, false) { BaseAddress = new Uri("http://default-endpoint") };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorForModelWithoutKeyThrowsException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<NotSupportedException>(() => new WeaviateCollection<Guid, object>(this._mockHttpClient, "Collection"));
|
||||
Assert.Contains("No key property found", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithoutEndpointThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
using var httpClient = new HttpClient();
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentException>(() => new WeaviateCollection<Guid, WeaviateHotel>(httpClient, "Collection"));
|
||||
Assert.Contains("Weaviate endpoint should be provided", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithDeclarativeModelInitializesCollection()
|
||||
{
|
||||
// Act & Assert
|
||||
using var collection = new WeaviateCollection<Guid, WeaviateHotel>(
|
||||
this._mockHttpClient,
|
||||
"Collection");
|
||||
|
||||
Assert.NotNull(collection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithImperativeModelInitializesCollection()
|
||||
{
|
||||
// Arrange
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties = [new VectorStoreKeyProperty("Id", typeof(Guid))]
|
||||
};
|
||||
|
||||
// Act
|
||||
using var collection = new WeaviateCollection<Guid, TestModel>(
|
||||
this._mockHttpClient,
|
||||
"Collection",
|
||||
new() { Definition = definition });
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(collection);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CollectionExistsData))]
|
||||
public async Task CollectionExistsReturnsValidResultAsync(HttpResponseMessage responseMessage, bool expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
this._messageHandlerStub.ResponseToReturn = responseMessage;
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, "Collection");
|
||||
|
||||
// Act
|
||||
var actualResult = await sut.CollectionExistsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("notStartingWithCapitalLetter")]
|
||||
[InlineData("0startingWithDigit")]
|
||||
[InlineData("contains spaces")]
|
||||
[InlineData("contains-dashes")]
|
||||
[InlineData("contains_underscores")]
|
||||
[InlineData("contains$specialCharacters")]
|
||||
[InlineData("contains!specialCharacters")]
|
||||
[InlineData("contains@specialCharacters")]
|
||||
[InlineData("contains#specialCharacters")]
|
||||
[InlineData("contains%specialCharacters")]
|
||||
[InlineData("contains^specialCharacters")]
|
||||
[InlineData("contains&specialCharacters")]
|
||||
[InlineData("contains*specialCharacters")]
|
||||
[InlineData("contains(specialCharacters")]
|
||||
[InlineData("contains)specialCharacters")]
|
||||
[InlineData("containsNonAsciiĄ")]
|
||||
[InlineData("containsNonAsciią")]
|
||||
public void CollectionCtorRejectsInvalidNames(string collectionName)
|
||||
{
|
||||
ArgumentException argumentException = Assert.Throws<ArgumentException>(() => new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, collectionName));
|
||||
Assert.Equal("collectionName", argumentException.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureCollectionExistsUsesValidCollectionSchemaAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CollectionName = "Collection";
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, CollectionName);
|
||||
|
||||
using var collectionExistsResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(collectionExistsResponse);
|
||||
|
||||
using var createCollectionResponse = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(createCollectionResponse);
|
||||
|
||||
// Act
|
||||
await sut.EnsureCollectionExistsAsync();
|
||||
|
||||
// Assert
|
||||
var schemaRequest = JsonSerializer.Deserialize<WeaviateCreateCollectionSchemaRequest>(this._messageHandlerStub.RequestContent);
|
||||
|
||||
Assert.NotNull(schemaRequest);
|
||||
|
||||
Assert.Equal(CollectionName, schemaRequest.CollectionName);
|
||||
|
||||
Assert.NotNull(schemaRequest.VectorConfigurations);
|
||||
Assert.Equal("descriptionEmbedding", schemaRequest.VectorConfigurations.Keys.First());
|
||||
|
||||
var vectorConfiguration = schemaRequest.VectorConfigurations["descriptionEmbedding"];
|
||||
|
||||
Assert.Equal("cosine", vectorConfiguration.VectorIndexConfig?.Distance);
|
||||
Assert.Equal("hnsw", vectorConfiguration.VectorIndexType);
|
||||
|
||||
Assert.NotNull(schemaRequest.Properties);
|
||||
|
||||
this.AssertSchemaProperty(schemaRequest.Properties[0], "hotelName", "text", true, false);
|
||||
this.AssertSchemaProperty(schemaRequest.Properties[1], "hotelCode", "int", false, false);
|
||||
this.AssertSchemaProperty(schemaRequest.Properties[2], "hotelRating", "number", false, false);
|
||||
this.AssertSchemaProperty(schemaRequest.Properties[3], "parking_is_included", "boolean", false, false);
|
||||
this.AssertSchemaProperty(schemaRequest.Properties[4], "tags", "text[]", false, false);
|
||||
this.AssertSchemaProperty(schemaRequest.Properties[5], "description", "text", false, true);
|
||||
this.AssertSchemaProperty(schemaRequest.Properties[6], "timestamp", "date", false, false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteCollectionSendsValidRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CollectionName = "Collection";
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, CollectionName);
|
||||
|
||||
// Act
|
||||
await sut.EnsureCollectionDeletedAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://default-endpoint/schema/Collection", this._messageHandlerStub.RequestUri?.AbsoluteUri);
|
||||
Assert.Equal(HttpMethod.Delete, this._messageHandlerStub.Method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSendsValidRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CollectionName = "Collection";
|
||||
var id = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, CollectionName);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync(id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://default-endpoint/objects/Collection/55555555-5555-5555-5555-555555555555", this._messageHandlerStub.RequestUri?.AbsoluteUri);
|
||||
Assert.Equal(HttpMethod.Delete, this._messageHandlerStub.Method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteBatchUsesValidQueryMatchAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CollectionName = "Collection";
|
||||
List<Guid> ids = [new Guid("11111111-1111-1111-1111-111111111111"), new Guid("22222222-2222-2222-2222-222222222222")];
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, CollectionName);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync(ids);
|
||||
|
||||
// Assert
|
||||
var request = JsonSerializer.Deserialize<WeaviateDeleteObjectBatchRequest>(this._messageHandlerStub.RequestContent);
|
||||
|
||||
Assert.NotNull(request?.Match);
|
||||
|
||||
Assert.Equal(CollectionName, request.Match.CollectionName);
|
||||
|
||||
Assert.NotNull(request.Match.WhereClause);
|
||||
|
||||
var clause = request.Match.WhereClause;
|
||||
|
||||
Assert.Equal("ContainsAny", clause.Operator);
|
||||
Assert.Equal(["id"], clause.Path);
|
||||
Assert.Equal(["11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"], clause.Values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExistingRecordReturnsValidRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
var id = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
|
||||
var jsonObject = new JsonObject { ["id"] = id.ToString(), ["properties"] = new JsonObject() };
|
||||
|
||||
jsonObject["properties"]!["hotelName"] = "Test Name";
|
||||
|
||||
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(jsonObject))
|
||||
};
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, "Collection");
|
||||
|
||||
// Act
|
||||
var result = await sut.GetAsync(id);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(id, result.HotelId);
|
||||
Assert.Equal("Test Name", result.HotelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExistingBatchRecordsReturnsValidRecordsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var id1 = new Guid("11111111-1111-1111-1111-111111111111");
|
||||
var id2 = new Guid("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
var jsonObject1 = new JsonObject { ["id"] = id1.ToString(), ["properties"] = new JsonObject() };
|
||||
var jsonObject2 = new JsonObject { ["id"] = id2.ToString(), ["properties"] = new JsonObject() };
|
||||
|
||||
jsonObject1["properties"]!["hotelName"] = "Test Name 1";
|
||||
jsonObject2["properties"]!["hotelName"] = "Test Name 2";
|
||||
|
||||
using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(jsonObject1)) };
|
||||
using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(jsonObject2)) };
|
||||
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(response1);
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(response2);
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, "Collection");
|
||||
|
||||
// Act
|
||||
var results = await sut.GetAsync([id1, id2]).ToListAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(results[0]);
|
||||
Assert.Equal(id1, results[0].HotelId);
|
||||
Assert.Equal("Test Name 1", results[0].HotelName);
|
||||
|
||||
Assert.NotNull(results[1]);
|
||||
Assert.Equal(id2, results[1].HotelId);
|
||||
Assert.Equal("Test Name 2", results[1].HotelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertReturnsRecordKeyAsync()
|
||||
{
|
||||
// Arrange
|
||||
var id = new Guid("11111111-1111-1111-1111-111111111111");
|
||||
var hotel = new WeaviateHotel { HotelId = id, HotelName = "Test Name" };
|
||||
|
||||
var batchResponse = new List<WeaviateUpsertCollectionObjectBatchResponse> { new() { Id = id, Result = new() { Status = "Success" } } };
|
||||
|
||||
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(batchResponse)),
|
||||
};
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, "Collection");
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync(hotel);
|
||||
|
||||
// Assert
|
||||
var request = JsonSerializer.Deserialize<WeaviateUpsertCollectionObjectBatchRequest>(this._messageHandlerStub.RequestContent);
|
||||
|
||||
Assert.NotNull(request?.CollectionObjects);
|
||||
|
||||
var jsonObject = request.CollectionObjects[0];
|
||||
|
||||
Assert.Equal("11111111-1111-1111-1111-111111111111", jsonObject["id"]?.GetValue<string>());
|
||||
Assert.Equal("Test Name", jsonObject["properties"]?["hotelName"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertReturnsRecordKeysAsync()
|
||||
{
|
||||
// Arrange
|
||||
var id1 = new Guid("11111111-1111-1111-1111-111111111111");
|
||||
var id2 = new Guid("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
var hotel1 = new WeaviateHotel { HotelId = id1, HotelName = "Test Name 1" };
|
||||
var hotel2 = new WeaviateHotel { HotelId = id2, HotelName = "Test Name 2" };
|
||||
|
||||
var batchResponse = new List<WeaviateUpsertCollectionObjectBatchResponse>
|
||||
{
|
||||
new() { Id = id1, Result = new() { Status = "Success" } },
|
||||
new() { Id = id2, Result = new() { Status = "Success" } }
|
||||
};
|
||||
|
||||
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(batchResponse)),
|
||||
};
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, "Collection");
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync([hotel1, hotel2]);
|
||||
|
||||
// Assert
|
||||
var request = JsonSerializer.Deserialize<WeaviateUpsertCollectionObjectBatchRequest>(this._messageHandlerStub.RequestContent);
|
||||
|
||||
Assert.NotNull(request?.CollectionObjects);
|
||||
|
||||
var jsonObject1 = request.CollectionObjects[0];
|
||||
var jsonObject2 = request.CollectionObjects[1];
|
||||
|
||||
Assert.Equal("11111111-1111-1111-1111-111111111111", jsonObject1["id"]?.GetValue<string>());
|
||||
Assert.Equal("Test Name 1", jsonObject1["properties"]?["hotelName"]?.GetValue<string>());
|
||||
|
||||
Assert.Equal("22222222-2222-2222-2222-222222222222", jsonObject2["id"]?.GetValue<string>());
|
||||
Assert.Equal("Test Name 2", jsonObject2["properties"]?["hotelName"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, "http://test-endpoint/schema", "Bearer fake-key")]
|
||||
[InlineData(false, "http://default-endpoint/schema", null)]
|
||||
public async Task ItUsesHttpClientParametersAsync(bool initializeOptions, string expectedEndpoint, string? expectedHeader)
|
||||
{
|
||||
// Arrange
|
||||
const string CollectionName = "Collection";
|
||||
|
||||
var options = initializeOptions ?
|
||||
new WeaviateCollectionOptions() { Endpoint = new Uri("http://test-endpoint"), ApiKey = "fake-key" } :
|
||||
null;
|
||||
|
||||
using var collectionExistsResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(collectionExistsResponse);
|
||||
|
||||
using var createCollectionResponse = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
this._messageHandlerStub.ResponseQueue.Enqueue(createCollectionResponse);
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, CollectionName, options);
|
||||
|
||||
// Act
|
||||
await sut.EnsureCollectionExistsAsync();
|
||||
|
||||
var headers = this._messageHandlerStub.RequestHeaders;
|
||||
var endpoint = this._messageHandlerStub.RequestUri;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedEndpoint, endpoint?.AbsoluteUri);
|
||||
Assert.Equal(expectedHeader, headers?.Authorization?.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task SearchReturnsValidRecordAsync(bool includeVectors)
|
||||
{
|
||||
// Arrange
|
||||
const string CollectionName = "SearchCollection";
|
||||
var id = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
var vector = new ReadOnlyMemory<float>([30f, 31f, 32f, 33f]);
|
||||
|
||||
var jsonObject = new JsonObject
|
||||
{
|
||||
["data"] = new JsonObject
|
||||
{
|
||||
["Get"] = new JsonObject
|
||||
{
|
||||
[CollectionName] = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["_additional"] = new JsonObject
|
||||
{
|
||||
["distance"] = 0.5,
|
||||
["id"] = id.ToString(),
|
||||
["vectors"] = new JsonObject
|
||||
{
|
||||
["descriptionEmbedding"] = new JsonArray(new List<float> {30, 31, 32, 33}.Select(l => (JsonNode)l).ToArray())
|
||||
}
|
||||
},
|
||||
["description"] = "This is a great hotel.",
|
||||
["hotelCode"] = 42,
|
||||
["hotelName"] = "My Hotel",
|
||||
["hotelRating"] = 4.5,
|
||||
["parking_is_included"] = true,
|
||||
["tags"] = new JsonArray(new List<string> { "t1", "t2" }.Select(l => (JsonNode)l).ToArray()),
|
||||
["timestamp"] = "2024-08-28T10:11:12-07:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(jsonObject))
|
||||
};
|
||||
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, CollectionName);
|
||||
|
||||
// Act
|
||||
var results = await sut.SearchAsync(vector, top: 3, new()
|
||||
{
|
||||
IncludeVectors = includeVectors
|
||||
}).ToListAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
|
||||
var score = results[0].Score;
|
||||
var record = results[0].Record;
|
||||
|
||||
Assert.Equal(0.5, score);
|
||||
|
||||
Assert.Equal(id, record.HotelId);
|
||||
Assert.Equal("My Hotel", record.HotelName);
|
||||
Assert.Equal("This is a great hotel.", record.Description);
|
||||
Assert.Equal(42, record.HotelCode);
|
||||
Assert.Equal(4.5f, record.HotelRating);
|
||||
Assert.True(record.ParkingIncluded);
|
||||
Assert.Equal(["t1", "t2"], record.Tags);
|
||||
Assert.Equal(new DateTimeOffset(new DateTime(2024, 8, 28, 10, 11, 12), TimeSpan.FromHours(-7)), record.Timestamp);
|
||||
|
||||
if (includeVectors)
|
||||
{
|
||||
Assert.True(record.DescriptionEmbedding.HasValue);
|
||||
Assert.Equal(vector.ToArray(), record.DescriptionEmbedding.Value.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.False(record.DescriptionEmbedding.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchWithUnsupportedVectorTypeThrowsExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, "Collection");
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotSupportedException>(async () =>
|
||||
await sut.SearchAsync(new List<double>([1, 2, 3]), top: 3).ToListAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchWithNonExistentVectorPropertyNameThrowsExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var sut = new WeaviateCollection<Guid, WeaviateHotel>(this._mockHttpClient, "Collection");
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sut.SearchAsync(
|
||||
new ReadOnlyMemory<float>([1f, 2f, 3f]),
|
||||
top: 3,
|
||||
new() { VectorProperty = r => "non-existent-property" })
|
||||
.ToListAsync());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._mockHttpClient.Dispose();
|
||||
this._messageHandlerStub.Dispose();
|
||||
}
|
||||
|
||||
public static TheoryData<HttpResponseMessage, bool> CollectionExistsData => new()
|
||||
{
|
||||
{ new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(new WeaviateGetCollectionSchemaResponse { CollectionName = "Collection" })) }, true },
|
||||
{ new HttpResponseMessage(HttpStatusCode.NotFound), false }
|
||||
};
|
||||
|
||||
#region private
|
||||
|
||||
private void AssertSchemaProperty(
|
||||
WeaviateCollectionSchemaProperty property,
|
||||
string propertyName,
|
||||
string dataType,
|
||||
bool indexFilterable,
|
||||
bool indexSearchable)
|
||||
{
|
||||
Assert.NotNull(property);
|
||||
Assert.Equal(propertyName, property.Name);
|
||||
Assert.Equal(dataType, property.DataType[0]);
|
||||
Assert.Equal(indexFilterable, property.IndexFilterable);
|
||||
Assert.Equal(indexSearchable, property.IndexSearchable);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812
|
||||
private sealed class TestModel
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string? HotelName { get; set; }
|
||||
}
|
||||
#pragma warning restore CA1812
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
// 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 Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.Weaviate;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for dynamic mapping.
|
||||
/// </summary>
|
||||
public sealed class WeaviateDynamicMapperTests
|
||||
{
|
||||
private const bool HasNamedVectors = true;
|
||||
|
||||
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters =
|
||||
{
|
||||
new WeaviateDateTimeOffsetConverter(),
|
||||
new WeaviateNullableDateTimeOffsetConverter()
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly CollectionModel s_model = new WeaviateModelBuilder(HasNamedVectors)
|
||||
.BuildDynamic(
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreDataProperty("BoolDataProp", typeof(bool)),
|
||||
new VectorStoreDataProperty("NullableBoolDataProp", typeof(bool?)),
|
||||
new VectorStoreDataProperty("IntDataProp", typeof(int)),
|
||||
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
|
||||
new VectorStoreDataProperty("LongDataProp", typeof(long)),
|
||||
new VectorStoreDataProperty("NullableLongDataProp", typeof(long?)),
|
||||
new VectorStoreDataProperty("ShortDataProp", typeof(short)),
|
||||
new VectorStoreDataProperty("NullableShortDataProp", typeof(short?)),
|
||||
new VectorStoreDataProperty("ByteDataProp", typeof(byte)),
|
||||
new VectorStoreDataProperty("NullableByteDataProp", typeof(byte?)),
|
||||
new VectorStoreDataProperty("FloatDataProp", typeof(float)),
|
||||
new VectorStoreDataProperty("NullableFloatDataProp", typeof(float?)),
|
||||
new VectorStoreDataProperty("DoubleDataProp", typeof(double)),
|
||||
new VectorStoreDataProperty("NullableDoubleDataProp", typeof(double?)),
|
||||
new VectorStoreDataProperty("DecimalDataProp", typeof(decimal)),
|
||||
new VectorStoreDataProperty("NullableDecimalDataProp", typeof(decimal?)),
|
||||
new VectorStoreDataProperty("DateTimeDataProp", typeof(DateTime)),
|
||||
new VectorStoreDataProperty("NullableDateTimeDataProp", typeof(DateTime?)),
|
||||
new VectorStoreDataProperty("DateTimeOffsetDataProp", typeof(DateTimeOffset)),
|
||||
new VectorStoreDataProperty("NullableDateTimeOffsetDataProp", typeof(DateTimeOffset?)),
|
||||
new VectorStoreDataProperty("GuidDataProp", typeof(Guid)),
|
||||
new VectorStoreDataProperty("NullableGuidDataProp", typeof(Guid?)),
|
||||
new VectorStoreDataProperty("TagListDataProp", typeof(List<string>)),
|
||||
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
|
||||
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null,
|
||||
s_jsonSerializerOptions);
|
||||
|
||||
private static readonly float[] s_floatVector = [1.0f, 2.0f, 3.0f];
|
||||
private static readonly List<string> s_taglist = ["tag1", "tag2"];
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsAllSupportedTypes()
|
||||
{
|
||||
// Arrange
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", HasNamedVectors, s_model, s_jsonSerializerOptions);
|
||||
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = key,
|
||||
|
||||
["StringDataProp"] = "string",
|
||||
["BoolDataProp"] = true,
|
||||
["NullableBoolDataProp"] = false,
|
||||
["IntDataProp"] = 1,
|
||||
["NullableIntDataProp"] = 2,
|
||||
["LongDataProp"] = 3L,
|
||||
["NullableLongDataProp"] = 4L,
|
||||
["ShortDataProp"] = (short)5,
|
||||
["NullableShortDataProp"] = (short)6,
|
||||
["ByteDataProp"] = (byte)7,
|
||||
["NullableByteDataProp"] = (byte)8,
|
||||
["FloatDataProp"] = 9.0f,
|
||||
["NullableFloatDataProp"] = 10.0f,
|
||||
["DoubleDataProp"] = 11.0,
|
||||
["NullableDoubleDataProp"] = 12.0,
|
||||
["DecimalDataProp"] = 13.99m,
|
||||
["NullableDecimalDataProp"] = 14.00m,
|
||||
["DateTimeDataProp"] = new DateTime(2021, 1, 1),
|
||||
["NullableDateTimeDataProp"] = new DateTime(2021, 1, 1),
|
||||
["DateTimeOffsetDataProp"] = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
["NullableDateTimeOffsetDataProp"] = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
["GuidDataProp"] = new Guid("11111111-1111-1111-1111-111111111111"),
|
||||
["NullableGuidDataProp"] = new Guid("22222222-2222-2222-2222-222222222222"),
|
||||
["TagListDataProp"] = s_taglist,
|
||||
|
||||
["FloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
|
||||
["NullableFloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
|
||||
}
|
||||
;
|
||||
|
||||
// Act
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(key, (Guid?)storageModel["id"]);
|
||||
Assert.Equal("Collection", (string?)storageModel["class"]);
|
||||
Assert.Equal("string", (string?)storageModel["properties"]?["stringDataProp"]);
|
||||
Assert.Equal(true, (bool?)storageModel["properties"]?["boolDataProp"]);
|
||||
Assert.Equal(false, (bool?)storageModel["properties"]?["nullableBoolDataProp"]);
|
||||
Assert.Equal(1, (int?)storageModel["properties"]?["intDataProp"]);
|
||||
Assert.Equal(2, (int?)storageModel["properties"]?["nullableIntDataProp"]);
|
||||
Assert.Equal(3L, (long?)storageModel["properties"]?["longDataProp"]);
|
||||
Assert.Equal(4L, (long?)storageModel["properties"]?["nullableLongDataProp"]);
|
||||
Assert.Equal((short)5, (short?)storageModel["properties"]?["shortDataProp"]);
|
||||
Assert.Equal((short)6, (short?)storageModel["properties"]?["nullableShortDataProp"]);
|
||||
Assert.Equal((byte)7, (byte?)storageModel["properties"]?["byteDataProp"]);
|
||||
Assert.Equal((byte)8, (byte?)storageModel["properties"]?["nullableByteDataProp"]);
|
||||
Assert.Equal(9.0f, (float?)storageModel["properties"]?["floatDataProp"]);
|
||||
Assert.Equal(10.0f, (float?)storageModel["properties"]?["nullableFloatDataProp"]);
|
||||
Assert.Equal(11.0, (double?)storageModel["properties"]?["doubleDataProp"]);
|
||||
Assert.Equal(12.0, (double?)storageModel["properties"]?["nullableDoubleDataProp"]);
|
||||
Assert.Equal(13.99m, (decimal?)storageModel["properties"]?["decimalDataProp"]);
|
||||
Assert.Equal(14.00m, (decimal?)storageModel["properties"]?["nullableDecimalDataProp"]);
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0), (DateTime?)storageModel["properties"]?["dateTimeDataProp"]);
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0), (DateTime?)storageModel["properties"]?["nullableDateTimeDataProp"]);
|
||||
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), (DateTimeOffset?)storageModel["properties"]?["dateTimeOffsetDataProp"]);
|
||||
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), (DateTimeOffset?)storageModel["properties"]?["nullableDateTimeOffsetDataProp"]);
|
||||
Assert.Equal(new Guid("11111111-1111-1111-1111-111111111111"), (Guid?)storageModel["properties"]?["guidDataProp"]);
|
||||
Assert.Equal(new Guid("22222222-2222-2222-2222-222222222222"), (Guid?)storageModel["properties"]?["nullableGuidDataProp"]);
|
||||
Assert.Equal(s_taglist, storageModel["properties"]?["tagListDataProp"]!.AsArray().GetValues<string>().ToArray());
|
||||
Assert.Equal(s_floatVector, storageModel["vectors"]?["floatVector"]!.AsArray().GetValues<float>().ToArray());
|
||||
Assert.Equal(s_floatVector, storageModel["vectors"]?["nullableFloatVector"]!.AsArray().GetValues<float>().ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsNullValues()
|
||||
{
|
||||
// Arrange
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
var keyProperty = new VectorStoreKeyProperty("Key", typeof(Guid));
|
||||
|
||||
var dataProperties = new List<VectorStoreDataProperty>
|
||||
{
|
||||
new("StringDataProp", typeof(string)),
|
||||
new("NullableIntDataProp", typeof(int?)),
|
||||
};
|
||||
|
||||
var vectorProperties = new List<VectorStoreVectorProperty>
|
||||
{
|
||||
new("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
|
||||
};
|
||||
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = key,
|
||||
|
||||
["StringDataProp"] = null,
|
||||
["NullableIntDataProp"] = null,
|
||||
|
||||
["NullableFloatVector"] = null
|
||||
};
|
||||
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", HasNamedVectors, 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 key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", HasNamedVectors, s_model, s_jsonSerializerOptions);
|
||||
|
||||
var storageModel = new JsonObject
|
||||
{
|
||||
["id"] = key,
|
||||
["properties"] = new JsonObject
|
||||
{
|
||||
["stringDataProp"] = "string",
|
||||
["boolDataProp"] = true,
|
||||
["nullableBoolDataProp"] = false,
|
||||
["intDataProp"] = 1,
|
||||
["nullableIntDataProp"] = 2,
|
||||
["longDataProp"] = 3L,
|
||||
["nullableLongDataProp"] = 4L,
|
||||
["shortDataProp"] = (short)5,
|
||||
["nullableShortDataProp"] = (short)6,
|
||||
["byteDataProp"] = (byte)7,
|
||||
["nullableByteDataProp"] = (byte)8,
|
||||
["floatDataProp"] = 9.0f,
|
||||
["nullableFloatDataProp"] = 10.0f,
|
||||
["doubleDataProp"] = 11.0,
|
||||
["nullableDoubleDataProp"] = 12.0,
|
||||
["decimalDataProp"] = 13.99m,
|
||||
["nullableDecimalDataProp"] = 14.00m,
|
||||
["dateTimeDataProp"] = new DateTime(2021, 1, 1),
|
||||
["nullableDateTimeDataProp"] = new DateTime(2021, 1, 1),
|
||||
["dateTimeOffsetDataProp"] = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
["nullableDateTimeOffsetDataProp"] = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
["guidDataProp"] = new Guid("11111111-1111-1111-1111-111111111111"),
|
||||
["nullableGuidDataProp"] = new Guid("22222222-2222-2222-2222-222222222222"),
|
||||
["tagListDataProp"] = new JsonArray(s_taglist.Select(l => (JsonValue)l).ToArray())
|
||||
},
|
||||
["vectors"] = new JsonObject
|
||||
{
|
||||
["floatVector"] = new JsonArray(s_floatVector.Select(l => (JsonValue)l).ToArray()),
|
||||
["nullableFloatVector"] = new JsonArray(s_floatVector.Select(l => (JsonValue)l).ToArray()),
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(key, dataModel["Key"]);
|
||||
Assert.Equal("string", dataModel["StringDataProp"]);
|
||||
Assert.Equal(true, dataModel["BoolDataProp"]);
|
||||
Assert.Equal(false, dataModel["NullableBoolDataProp"]);
|
||||
Assert.Equal(1, dataModel["IntDataProp"]);
|
||||
Assert.Equal(2, dataModel["NullableIntDataProp"]);
|
||||
Assert.Equal(3L, dataModel["LongDataProp"]);
|
||||
Assert.Equal(4L, dataModel["NullableLongDataProp"]);
|
||||
Assert.Equal((short)5, dataModel["ShortDataProp"]);
|
||||
Assert.Equal((short)6, dataModel["NullableShortDataProp"]);
|
||||
Assert.Equal((byte)7, dataModel["ByteDataProp"]);
|
||||
Assert.Equal((byte)8, dataModel["NullableByteDataProp"]);
|
||||
Assert.Equal(9.0f, dataModel["FloatDataProp"]);
|
||||
Assert.Equal(10.0f, dataModel["NullableFloatDataProp"]);
|
||||
Assert.Equal(11.0, dataModel["DoubleDataProp"]);
|
||||
Assert.Equal(12.0, dataModel["NullableDoubleDataProp"]);
|
||||
Assert.Equal(13.99m, dataModel["DecimalDataProp"]);
|
||||
Assert.Equal(14.00m, dataModel["NullableDecimalDataProp"]);
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0), dataModel["DateTimeDataProp"]);
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0), dataModel["NullableDateTimeDataProp"]);
|
||||
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), dataModel["DateTimeOffsetDataProp"]);
|
||||
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), dataModel["NullableDateTimeOffsetDataProp"]);
|
||||
Assert.Equal(new Guid("11111111-1111-1111-1111-111111111111"), dataModel["GuidDataProp"]);
|
||||
Assert.Equal(new Guid("22222222-2222-2222-2222-222222222222"), dataModel["NullableGuidDataProp"]);
|
||||
Assert.Equal(s_taglist, dataModel["TagListDataProp"]);
|
||||
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
|
||||
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["NullableFloatVector"]!)!.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelMapsNullValues()
|
||||
{
|
||||
// Arrange
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
var keyProperty = new VectorStoreKeyProperty("Key", typeof(Guid));
|
||||
|
||||
var storageModel = new JsonObject
|
||||
{
|
||||
["id"] = key,
|
||||
["properties"] = new JsonObject
|
||||
{
|
||||
["stringDataProp"] = null,
|
||||
["nullableIntDataProp"] = null,
|
||||
},
|
||||
["vectors"] = new JsonObject
|
||||
{
|
||||
["nullableFloatVector"] = null
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", HasNamedVectors, 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 WeaviateMapper<Dictionary<string, object?>>("Collection", HasNamedVectors, 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
|
||||
var recordDefinition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
var model = new WeaviateModelBuilder(HasNamedVectors).BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null, s_jsonSerializerOptions);
|
||||
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
|
||||
var record = new Dictionary<string, object?> { ["Key"] = key };
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", HasNamedVectors, model, s_jsonSerializerOptions);
|
||||
|
||||
// Act
|
||||
var storageModel = sut.MapFromDataToStorageModel(record, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(key, (Guid?)storageModel["id"]);
|
||||
Assert.False(storageModel.ContainsKey("StringDataProp"));
|
||||
Assert.False(storageModel.ContainsKey("FloatVector"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelSkipsMissingProperties()
|
||||
{
|
||||
// Arrange
|
||||
var recordDefinition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
var model = new WeaviateModelBuilder(HasNamedVectors).BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null, s_jsonSerializerOptions);
|
||||
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", HasNamedVectors, model, s_jsonSerializerOptions);
|
||||
|
||||
var storageModel = new JsonObject
|
||||
{
|
||||
["id"] = key
|
||||
};
|
||||
|
||||
// Act
|
||||
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(key, dataModel["Key"]);
|
||||
Assert.False(dataModel.ContainsKey("StringDataProp"));
|
||||
Assert.False(dataModel.ContainsKey("FloatVector"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void MapFromDataToStorageModelMapsNamedVectorsCorrectly(bool hasNamedVectors)
|
||||
{
|
||||
// Arrange
|
||||
var recordDefinition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 4)
|
||||
]
|
||||
};
|
||||
|
||||
var model = new WeaviateModelBuilder(hasNamedVectors).BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null, s_jsonSerializerOptions);
|
||||
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
|
||||
var record = new Dictionary<string, object?> { ["Key"] = key, ["FloatVector"] = new ReadOnlyMemory<float>(s_floatVector) };
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", hasNamedVectors, model, s_jsonSerializerOptions);
|
||||
|
||||
// Act
|
||||
var storageModel = sut.MapFromDataToStorageModel(record, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
var vectorProperty = hasNamedVectors ? storageModel["vectors"]!["floatVector"] : storageModel["vector"];
|
||||
|
||||
Assert.Equal(key, (Guid?)storageModel["id"]);
|
||||
Assert.Equal(s_floatVector, vectorProperty!.AsArray().GetValues<float>().ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void MapFromStorageToDataModelMapsNamedVectorsCorrectly(bool hasNamedVectors)
|
||||
{
|
||||
// Arrange
|
||||
var recordDefinition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(Guid)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 4)
|
||||
]
|
||||
};
|
||||
|
||||
var model = new WeaviateModelBuilder(hasNamedVectors).BuildDynamic(recordDefinition, defaultEmbeddingGenerator: null, s_jsonSerializerOptions);
|
||||
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
|
||||
var sut = new WeaviateMapper<Dictionary<string, object?>>("Collection", hasNamedVectors, model, s_jsonSerializerOptions);
|
||||
|
||||
var storageModel = new JsonObject { ["id"] = key };
|
||||
|
||||
var vector = new JsonArray(s_floatVector.Select(l => (JsonValue)l).ToArray());
|
||||
|
||||
if (hasNamedVectors)
|
||||
{
|
||||
storageModel["vectors"] = new JsonObject
|
||||
{
|
||||
["floatVector"] = vector
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
storageModel["vector"] = vector;
|
||||
}
|
||||
|
||||
// Act
|
||||
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(key, dataModel["Key"]);
|
||||
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
#pragma warning disable CS8618
|
||||
|
||||
public sealed record WeaviateHotel
|
||||
{
|
||||
/// <summary>The key of the record.</summary>
|
||||
[VectorStoreKey]
|
||||
public Guid HotelId { get; init; }
|
||||
|
||||
/// <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>
|
||||
[JsonPropertyName("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(IsFullTextIndexed = true)]
|
||||
public string Description { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
/// <summary>A vector field.</summary>
|
||||
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.Hnsw)]
|
||||
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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 Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.Weaviate;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="WeaviateMapper{TRecord}"/> class.
|
||||
/// </summary>
|
||||
public sealed class WeaviateMapperTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters =
|
||||
{
|
||||
new WeaviateDateTimeOffsetConverter(),
|
||||
new WeaviateNullableDateTimeOffsetConverter()
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void MapFromDataToStorageModelReturnsValidObject(bool hasNamedVectors)
|
||||
{
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
// Arrange
|
||||
var hotel = new WeaviateHotel
|
||||
{
|
||||
HotelId = key,
|
||||
HotelName = "Test Name",
|
||||
Tags = ["tag1", "tag2"],
|
||||
DescriptionEmbedding = new ReadOnlyMemory<float>([1f, 2f, 3f])
|
||||
};
|
||||
|
||||
var sut = GetMapper(hasNamedVectors);
|
||||
|
||||
// Act
|
||||
var document = sut.MapFromDataToStorageModel(hotel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(document);
|
||||
|
||||
Assert.Equal(key, document["id"]!.GetValue<Guid>());
|
||||
Assert.Equal("Test Name", document["properties"]!["hotelName"]!.GetValue<string>());
|
||||
Assert.Equal(["tag1", "tag2"], document["properties"]!["tags"]!.AsArray().Select(l => l!.GetValue<string>()));
|
||||
|
||||
var vectorNode = hasNamedVectors ? document["vectors"]!["descriptionEmbedding"] : document["vector"];
|
||||
|
||||
Assert.Equal([1f, 2f, 3f], vectorNode!.AsArray().Select(l => l!.GetValue<float>()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void MapFromStorageToDataModelReturnsValidObject(bool hasNamedVectors)
|
||||
{
|
||||
var key = new Guid("55555555-5555-5555-5555-555555555555");
|
||||
|
||||
// Arrange
|
||||
var document = new JsonObject
|
||||
{
|
||||
["id"] = key,
|
||||
["properties"] = new JsonObject(),
|
||||
["vectors"] = new JsonObject()
|
||||
};
|
||||
|
||||
document["properties"]!["hotelName"] = "Test Name";
|
||||
document["properties"]!["tags"] = new JsonArray(new List<string> { "tag1", "tag2" }.Select(l => JsonValue.Create(l)).ToArray());
|
||||
|
||||
var vectorNode = new JsonArray(new List<float> { 1f, 2f, 3f }.Select(l => JsonValue.Create(l)).ToArray());
|
||||
|
||||
if (hasNamedVectors)
|
||||
{
|
||||
document["vectors"]!["descriptionEmbedding"] = vectorNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
document["vector"] = vectorNode;
|
||||
}
|
||||
|
||||
var sut = GetMapper(hasNamedVectors);
|
||||
|
||||
// Act
|
||||
var hotel = sut.MapFromStorageToDataModel(document, includeVectors: true);
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
#region private
|
||||
|
||||
private static WeaviateMapper<WeaviateHotel> GetMapper(bool hasNamedVectors) => new(
|
||||
"CollectionName",
|
||||
hasNamedVectors,
|
||||
new WeaviateModelBuilder(hasNamedVectors)
|
||||
.Build(
|
||||
typeof(WeaviateHotel),
|
||||
typeof(Guid),
|
||||
new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("HotelId", typeof(Guid)),
|
||||
new VectorStoreDataProperty("HotelName", typeof(string)),
|
||||
new VectorStoreDataProperty("Tags", typeof(List<string>)),
|
||||
new VectorStoreVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null,
|
||||
s_jsonSerializerOptions),
|
||||
s_jsonSerializerOptions);
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.Weaviate;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="WeaviateQueryBuilder"/> class.
|
||||
/// </summary>
|
||||
public sealed class WeaviateQueryBuilderTests
|
||||
{
|
||||
private const string CollectionName = "Collection";
|
||||
private const string VectorPropertyName = "descriptionEmbedding";
|
||||
|
||||
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters =
|
||||
{
|
||||
new WeaviateDateTimeOffsetConverter(),
|
||||
new WeaviateNullableDateTimeOffsetConverter()
|
||||
}
|
||||
};
|
||||
|
||||
private readonly CollectionModel _model = new WeaviateModelBuilder(hasNamedVectors: true)
|
||||
.BuildDynamic(
|
||||
new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("HotelId", typeof(Guid)) { StorageName = "hotelId" },
|
||||
new VectorStoreDataProperty("HotelName", typeof(string)) { StorageName = "hotelName" },
|
||||
new VectorStoreDataProperty("HotelCode", typeof(string)) { StorageName = "hotelCode" },
|
||||
new VectorStoreDataProperty("Tags", typeof(string[])) { StorageName = "tags" },
|
||||
new VectorStoreVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory<float>), 10) { StorageName = "descriptionEmbeddding" },
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
private readonly ReadOnlyMemory<float> _vector = new([31f, 32f, 33f, 34f]);
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void BuildSearchQueryByDefaultReturnsValidQuery(bool hasNamedVectors)
|
||||
{
|
||||
// Arrange
|
||||
var expectedQuery = $$"""
|
||||
{
|
||||
Get {
|
||||
Collection (
|
||||
limit: 3
|
||||
offset: 2
|
||||
{{string.Empty}}
|
||||
nearVector: {
|
||||
{{(hasNamedVectors ? "targetVectors: [\"descriptionEmbedding\"]" : string.Empty)}}
|
||||
vector: [31,32,33,34]
|
||||
{{string.Empty}}
|
||||
}
|
||||
) {
|
||||
HotelName HotelCode Tags
|
||||
_additional {
|
||||
id
|
||||
distance
|
||||
{{string.Empty}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var searchOptions = new VectorSearchOptions<DummyType>
|
||||
{
|
||||
Skip = 2,
|
||||
};
|
||||
|
||||
// Act
|
||||
var query = WeaviateQueryBuilder.BuildSearchQuery(
|
||||
this._vector,
|
||||
CollectionName,
|
||||
VectorPropertyName,
|
||||
s_jsonSerializerOptions,
|
||||
top: 3,
|
||||
searchOptions,
|
||||
this._model,
|
||||
hasNamedVectors);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedQuery, query);
|
||||
|
||||
Assert.DoesNotContain("vectors", query);
|
||||
Assert.DoesNotContain("where", query);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void BuildSearchQueryWithIncludedVectorsReturnsValidQuery(bool hasNamedVectors)
|
||||
{
|
||||
// Arrange
|
||||
var searchOptions = new VectorSearchOptions<DummyType>
|
||||
{
|
||||
Skip = 2,
|
||||
IncludeVectors = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var query = WeaviateQueryBuilder.BuildSearchQuery(
|
||||
this._vector,
|
||||
CollectionName,
|
||||
VectorPropertyName,
|
||||
s_jsonSerializerOptions,
|
||||
top: 3,
|
||||
searchOptions,
|
||||
this._model,
|
||||
hasNamedVectors);
|
||||
|
||||
// Assert
|
||||
var vectorQuery = hasNamedVectors ? "vectors { DescriptionEmbedding }" : "vector";
|
||||
|
||||
Assert.Contains(vectorQuery, query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHybridSearchQueryEscapesDoubleQuotesInKeywords()
|
||||
{
|
||||
// Arrange
|
||||
var searchOptions = new HybridSearchOptions<DummyType> { Skip = 0 };
|
||||
var vectorProperty = this._model.VectorProperties[0];
|
||||
var textProperty = this._model.DataProperties[0];
|
||||
|
||||
// Act
|
||||
var query = WeaviateQueryBuilder.BuildHybridSearchQuery(
|
||||
this._vector,
|
||||
top: 3,
|
||||
keywords: "test \"injection\"",
|
||||
CollectionName,
|
||||
this._model,
|
||||
vectorProperty,
|
||||
textProperty,
|
||||
s_jsonSerializerOptions,
|
||||
searchOptions,
|
||||
hasNamedVectors: true);
|
||||
|
||||
// Assert - the double quote must be escaped in the GraphQL string
|
||||
Assert.Contains("query: \"test \\\"injection\\\"\"", query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHybridSearchQueryEscapesBackslashInKeywords()
|
||||
{
|
||||
// Arrange
|
||||
var searchOptions = new HybridSearchOptions<DummyType> { Skip = 0 };
|
||||
var vectorProperty = this._model.VectorProperties[0];
|
||||
var textProperty = this._model.DataProperties[0];
|
||||
|
||||
// Act
|
||||
var query = WeaviateQueryBuilder.BuildHybridSearchQuery(
|
||||
this._vector,
|
||||
top: 3,
|
||||
keywords: @"test\path",
|
||||
CollectionName,
|
||||
this._model,
|
||||
vectorProperty,
|
||||
textProperty,
|
||||
s_jsonSerializerOptions,
|
||||
searchOptions,
|
||||
hasNamedVectors: true);
|
||||
|
||||
// Assert - backslash must be escaped
|
||||
Assert.Contains(@"query: ""test\\path""", query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildHybridSearchQueryWithPlainKeywordsWorks()
|
||||
{
|
||||
// Arrange
|
||||
var searchOptions = new HybridSearchOptions<DummyType> { Skip = 0 };
|
||||
var vectorProperty = this._model.VectorProperties[0];
|
||||
var textProperty = this._model.DataProperties[0];
|
||||
|
||||
// Act
|
||||
var query = WeaviateQueryBuilder.BuildHybridSearchQuery(
|
||||
this._vector,
|
||||
top: 3,
|
||||
keywords: "hello world",
|
||||
CollectionName,
|
||||
this._model,
|
||||
vectorProperty,
|
||||
textProperty,
|
||||
s_jsonSerializerOptions,
|
||||
searchOptions,
|
||||
hasNamedVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("query: \"hello world\"", query);
|
||||
}
|
||||
|
||||
#region private
|
||||
|
||||
#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
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Connectors.Weaviate;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Weaviate.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="WeaviateVectorStore"/> class.
|
||||
/// </summary>
|
||||
public sealed class WeaviateVectorStoreTests : IDisposable
|
||||
{
|
||||
private readonly HttpMessageHandlerStub _messageHandlerStub = new();
|
||||
private readonly HttpClient _mockHttpClient;
|
||||
|
||||
public WeaviateVectorStoreTests()
|
||||
{
|
||||
this._mockHttpClient = new(this._messageHandlerStub, false) { BaseAddress = new Uri("http://test") };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCollectionWithNotSupportedKeyThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
using var sut = new WeaviateVectorStore(this._mockHttpClient);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => sut.GetCollection<string, WeaviateHotel>("Collection"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCollectionWithSupportedKeyReturnsCollection()
|
||||
{
|
||||
// Arrange
|
||||
using var sut = new WeaviateVectorStore(this._mockHttpClient);
|
||||
|
||||
// Act
|
||||
var collection = sut.GetCollection<Guid, WeaviateHotel>("Collection1");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(collection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCollectionNamesReturnsCollectionNamesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var expectedCollectionNames = new List<string> { "Collection1", "Collection2", "Collection3" };
|
||||
var response = new WeaviateGetCollectionsResponse
|
||||
{
|
||||
Collections = expectedCollectionNames.Select(name => new WeaviateCollectionSchema(name)).ToList()
|
||||
};
|
||||
|
||||
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(response))
|
||||
};
|
||||
|
||||
using var sut = new WeaviateVectorStore(this._mockHttpClient);
|
||||
|
||||
// Act
|
||||
var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedCollectionNames, actualCollectionNames);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._mockHttpClient.Dispose();
|
||||
this._messageHandlerStub.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user