Files
microsoft--semantic-kernel/dotnet/test/VectorData/Weaviate.UnitTests/WeaviateCollectionTests.cs
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

531 lines
20 KiB
C#

// 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
}