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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,6 @@
# Suppressing errors for Test projects under dotnet folder
[*.cs]
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
@@ -0,0 +1,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();
}
}