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>Microsoft.SemanticKernel.Connectors.Redis.UnitTests</AssemblyName>
|
||||
<RootNamespace>Microsoft.SemanticKernel.Connectors.Redis.UnitTests</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0050</NoWarn>
|
||||
<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>
|
||||
<PackageReference Include="System.Numerics.Tensors" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/AssertExtensions.cs"
|
||||
Link="%(RecursiveDir)%(Filename)%(Extension)"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\VectorData\Redis\Redis.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using NRedisStack.Search;
|
||||
using Xunit;
|
||||
using static NRedisStack.Search.Schema;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisCollectionCreateMapping"/> class.
|
||||
/// </summary>
|
||||
public class RedisCollectionCreateMappingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void MapToSchemaCreatesSchema(bool useDollarPrefix)
|
||||
{
|
||||
// Arrange.
|
||||
PropertyModel[] properties =
|
||||
[
|
||||
new KeyPropertyModel("Key", typeof(string)),
|
||||
|
||||
new DataPropertyModel("FilterableString", typeof(string)) { IsIndexed = true },
|
||||
new DataPropertyModel("FullTextSearchableString", typeof(string)) { IsFullTextIndexed = true },
|
||||
new DataPropertyModel("FilterableStringEnumerable", typeof(string[])) { IsIndexed = true },
|
||||
new DataPropertyModel("FullTextSearchableStringEnumerable", typeof(string[])) { IsFullTextIndexed = true },
|
||||
|
||||
new DataPropertyModel("FilterableInt", typeof(int)) { IsIndexed = true },
|
||||
new DataPropertyModel("FilterableNullableInt", typeof(int)) { IsIndexed = true },
|
||||
|
||||
new DataPropertyModel("NonFilterableString", typeof(string)),
|
||||
|
||||
new VectorPropertyModel("VectorDefaultIndexingOptions", typeof(ReadOnlyMemory<float>)) { Dimensions = 10, EmbeddingType = typeof(ReadOnlyMemory<float>) },
|
||||
new VectorPropertyModel("VectorSpecificIndexingOptions", typeof(ReadOnlyMemory<float>))
|
||||
{
|
||||
Dimensions = 20,
|
||||
IndexKind = IndexKind.Flat,
|
||||
DistanceFunction = DistanceFunction.EuclideanSquaredDistance,
|
||||
StorageName = "vector_specific_indexing_options",
|
||||
EmbeddingType = typeof(ReadOnlyMemory<float>)
|
||||
}
|
||||
];
|
||||
|
||||
// Act.
|
||||
var schema = RedisCollectionCreateMapping.MapToSchema(properties, useDollarPrefix);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(schema);
|
||||
Assert.Equal(8, schema.Fields.Count);
|
||||
|
||||
Assert.IsType<TagField>(schema.Fields[0]);
|
||||
Assert.IsType<TextField>(schema.Fields[1]);
|
||||
Assert.IsType<TagField>(schema.Fields[2]);
|
||||
Assert.IsType<TextField>(schema.Fields[3]);
|
||||
Assert.IsType<NumericField>(schema.Fields[4]);
|
||||
Assert.IsType<NumericField>(schema.Fields[5]);
|
||||
Assert.IsType<VectorField>(schema.Fields[6]);
|
||||
Assert.IsType<VectorField>(schema.Fields[7]);
|
||||
|
||||
if (useDollarPrefix)
|
||||
{
|
||||
VerifyFieldName(schema.Fields[0].FieldName, ["$.FilterableString", "AS", "FilterableString"]);
|
||||
VerifyFieldName(schema.Fields[1].FieldName, ["$.FullTextSearchableString", "AS", "FullTextSearchableString"]);
|
||||
VerifyFieldName(schema.Fields[2].FieldName, ["$.FilterableStringEnumerable.*", "AS", "FilterableStringEnumerable"]);
|
||||
VerifyFieldName(schema.Fields[3].FieldName, ["$.FullTextSearchableStringEnumerable", "AS", "FullTextSearchableStringEnumerable"]);
|
||||
|
||||
VerifyFieldName(schema.Fields[4].FieldName, ["$.FilterableInt", "AS", "FilterableInt"]);
|
||||
VerifyFieldName(schema.Fields[5].FieldName, ["$.FilterableNullableInt", "AS", "FilterableNullableInt"]);
|
||||
|
||||
VerifyFieldName(schema.Fields[6].FieldName, ["$.VectorDefaultIndexingOptions", "AS", "VectorDefaultIndexingOptions"]);
|
||||
VerifyFieldName(schema.Fields[7].FieldName, ["$.vector_specific_indexing_options", "AS", "vector_specific_indexing_options"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyFieldName(schema.Fields[0].FieldName, ["FilterableString", "AS", "FilterableString"]);
|
||||
VerifyFieldName(schema.Fields[1].FieldName, ["FullTextSearchableString", "AS", "FullTextSearchableString"]);
|
||||
VerifyFieldName(schema.Fields[2].FieldName, ["FilterableStringEnumerable.*", "AS", "FilterableStringEnumerable"]);
|
||||
VerifyFieldName(schema.Fields[3].FieldName, ["FullTextSearchableStringEnumerable", "AS", "FullTextSearchableStringEnumerable"]);
|
||||
|
||||
VerifyFieldName(schema.Fields[4].FieldName, ["FilterableInt", "AS", "FilterableInt"]);
|
||||
VerifyFieldName(schema.Fields[5].FieldName, ["FilterableNullableInt", "AS", "FilterableNullableInt"]);
|
||||
|
||||
VerifyFieldName(schema.Fields[6].FieldName, ["VectorDefaultIndexingOptions", "AS", "VectorDefaultIndexingOptions"]);
|
||||
VerifyFieldName(schema.Fields[7].FieldName, ["vector_specific_indexing_options", "AS", "vector_specific_indexing_options"]);
|
||||
}
|
||||
|
||||
Assert.Equal("10", ((VectorField)schema.Fields[6]).Attributes!["DIM"]);
|
||||
Assert.Equal("FLOAT32", ((VectorField)schema.Fields[6]).Attributes!["TYPE"]);
|
||||
Assert.Equal("COSINE", ((VectorField)schema.Fields[6]).Attributes!["DISTANCE_METRIC"]);
|
||||
|
||||
Assert.Equal("20", ((VectorField)schema.Fields[7]).Attributes!["DIM"]);
|
||||
Assert.Equal("FLOAT32", ((VectorField)schema.Fields[7]).Attributes!["TYPE"]);
|
||||
Assert.Equal("L2", ((VectorField)schema.Fields[7]).Attributes!["DISTANCE_METRIC"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSDKIndexKindThrowsOnUnsupportedIndexKind()
|
||||
{
|
||||
// Arrange.
|
||||
var vectorProperty = new VectorPropertyModel("VectorProperty", typeof(ReadOnlyMemory<float>)) { IndexKind = "Unsupported" };
|
||||
|
||||
// Act and assert.
|
||||
Assert.Throws<InvalidOperationException>(() => RedisCollectionCreateMapping.GetSDKIndexKind(vectorProperty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSDKDistanceAlgorithmThrowsOnUnsupportedDistanceFunction()
|
||||
{
|
||||
// Arrange.
|
||||
var vectorProperty = new VectorPropertyModel("VectorProperty", typeof(ReadOnlyMemory<float>)) { DistanceFunction = "Unsupported" };
|
||||
|
||||
// Act and assert.
|
||||
Assert.Throws<NotSupportedException>(() => RedisCollectionCreateMapping.GetSDKDistanceAlgorithm(vectorProperty));
|
||||
}
|
||||
|
||||
private static void VerifyFieldName(FieldName fieldName, List<object> expected)
|
||||
{
|
||||
var args = new List<object>();
|
||||
fieldName.AddCommandArguments(args);
|
||||
Assert.Equal(expected, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisCollectionSearchMapping"/> class.
|
||||
/// </summary>
|
||||
public class RedisCollectionSearchMappingTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateVectorAndConvertToBytesConvertsFloatVector()
|
||||
{
|
||||
// Arrange.
|
||||
var floatVector = new ReadOnlyMemory<float>(new float[] { 1.0f, 2.0f, 3.0f });
|
||||
|
||||
// Act.
|
||||
var byteArray = RedisCollectionSearchMapping.ValidateVectorAndConvertToBytes(floatVector, "Test");
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(byteArray);
|
||||
Assert.Equal(12, byteArray.Length);
|
||||
Assert.Equal(new byte[12] { 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64 }, byteArray);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateVectorAndConvertToBytesConvertsDoubleVector()
|
||||
{
|
||||
// Arrange.
|
||||
var doubleVector = new ReadOnlyMemory<double>(new double[] { 1.0, 2.0, 3.0 });
|
||||
|
||||
// Act.
|
||||
var byteArray = RedisCollectionSearchMapping.ValidateVectorAndConvertToBytes(doubleVector, "Test");
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(byteArray);
|
||||
Assert.Equal(24, byteArray.Length);
|
||||
Assert.Equal(new byte[24] { 0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 8, 64 }, byteArray);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateVectorAndConvertToBytesThrowsForUnsupportedType()
|
||||
{
|
||||
// Arrange.
|
||||
var unsupportedVector = new ReadOnlyMemory<int>(new int[] { 1, 2, 3 });
|
||||
|
||||
// Act & Assert.
|
||||
var exception = Assert.Throws<NotSupportedException>(() =>
|
||||
{
|
||||
var byteArray = RedisCollectionSearchMapping.ValidateVectorAndConvertToBytes(unsupportedVector, "Test");
|
||||
});
|
||||
Assert.Equal("The provided vector type System.ReadOnlyMemory`1[[System.Int32, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] is not supported by the Redis Test connector.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildQueryBuildsRedisQueryWithDefaults()
|
||||
{
|
||||
// Arrange.
|
||||
var floatVector = new ReadOnlyMemory<float>(new float[] { 1.0f, 2.0f, 3.0f });
|
||||
var byteArray = MemoryMarshal.AsBytes(floatVector.Span).ToArray();
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10)
|
||||
]);
|
||||
|
||||
// Act.
|
||||
var query = RedisCollectionSearchMapping.BuildQuery(byteArray, top: 3, new VectorSearchOptions<DummyType>(), model, model.VectorProperty, null);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(query);
|
||||
Assert.Equal("*=>[KNN 3 @Vector $embedding AS vector_score]", query.QueryString);
|
||||
Assert.Equal("vector_score", query.SortBy);
|
||||
Assert.True(query.WithScores);
|
||||
Assert.Equal(2, query.dialect);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildQueryBuildsRedisQueryWithCustomVectorName()
|
||||
{
|
||||
// Arrange.
|
||||
var floatVector = new ReadOnlyMemory<float>(new float[] { 1.0f, 2.0f, 3.0f });
|
||||
var byteArray = MemoryMarshal.AsBytes(floatVector.Span).ToArray();
|
||||
var vectorSearchOptions = new VectorSearchOptions<DummyType> { Skip = 3 };
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10) { StorageName = "storage_Vector" }
|
||||
]);
|
||||
var selectFields = new string[] { "storage_Field1", "storage_Field2" };
|
||||
|
||||
// Act.
|
||||
var query = RedisCollectionSearchMapping.BuildQuery(byteArray, top: 5, vectorSearchOptions, model, model.VectorProperty, selectFields);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(query);
|
||||
Assert.Equal("*=>[KNN 8 @storage_Vector $embedding AS vector_score]", query.QueryString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveDistanceFunctionReturnsCosineSimilarityIfNoDistanceFunctionSpecified()
|
||||
{
|
||||
var property = new VectorPropertyModel("Prop", typeof(ReadOnlyMemory<float>));
|
||||
|
||||
// Act.
|
||||
var resolvedDistanceFunction = RedisCollectionSearchMapping.ResolveDistanceFunction(property);
|
||||
|
||||
// Assert.
|
||||
Assert.Equal(DistanceFunction.CosineSimilarity, resolvedDistanceFunction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveDistanceFunctionReturnsDistanceFunctionFromProvidedProperty()
|
||||
{
|
||||
var property = new VectorPropertyModel("Prop", typeof(ReadOnlyMemory<float>)) { DistanceFunction = DistanceFunction.DotProductSimilarity };
|
||||
|
||||
// Act.
|
||||
var resolvedDistanceFunction = RedisCollectionSearchMapping.ResolveDistanceFunction(property);
|
||||
|
||||
// Assert.
|
||||
Assert.Equal(DistanceFunction.DotProductSimilarity, resolvedDistanceFunction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOutputScoreFromRedisScoreConvertsCosineDistanceToSimilarity()
|
||||
{
|
||||
// Act & Assert.
|
||||
Assert.Equal(-1, RedisCollectionSearchMapping.GetOutputScoreFromRedisScore(2, DistanceFunction.CosineSimilarity));
|
||||
Assert.Equal(0, RedisCollectionSearchMapping.GetOutputScoreFromRedisScore(1, DistanceFunction.CosineSimilarity));
|
||||
Assert.Equal(1, RedisCollectionSearchMapping.GetOutputScoreFromRedisScore(0, DistanceFunction.CosineSimilarity));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(DistanceFunction.CosineDistance, 2)]
|
||||
[InlineData(DistanceFunction.DotProductSimilarity, 2)]
|
||||
[InlineData(DistanceFunction.EuclideanSquaredDistance, 2)]
|
||||
public void GetOutputScoreFromRedisScoreLeavesNonConsineSimilarityUntouched(string distanceFunction, float score)
|
||||
{
|
||||
// Act & Assert.
|
||||
Assert.Equal(score, RedisCollectionSearchMapping.GetOutputScoreFromRedisScore(score, distanceFunction));
|
||||
}
|
||||
|
||||
#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
|
||||
|
||||
private static CollectionModel BuildModel(List<VectorStoreProperty> properties)
|
||||
=> new RedisModelBuilder(RedisHashSetCollection<string, DummyType>.ModelBuildingOptions)
|
||||
.BuildDynamic(new() { Properties = properties }, defaultEmbeddingGenerator: null);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
#pragma warning disable MEVD9001 // Experimental
|
||||
|
||||
public sealed class RedisFilterTranslatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Contains_with_simple_string()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("foo"));
|
||||
Assert.Equal("""@Tags:{"foo"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_curly_brace_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("foo}bar"));
|
||||
Assert.Equal("""@Tags:{"foo}bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_pipe_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("foo|bar"));
|
||||
Assert.Equal("""@Tags:{"foo|bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_double_quote_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("foo\"bar"));
|
||||
Assert.Equal("""@Tags:{"foo\"bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_asterisk_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("foo*bar"));
|
||||
Assert.Equal("""@Tags:{"foo*bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_at_sign_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("foo@bar"));
|
||||
Assert.Equal("""@Tags:{"foo@bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_injection_attempt()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("evil} | @secret:{*"));
|
||||
Assert.Equal("""@Tags:{"evil} | @secret:{*"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Any_with_simple_strings()
|
||||
{
|
||||
var values = new[] { "a", "b" };
|
||||
var result = Translate<TestRecord>(r => r.Tags.Any(t => values.Contains(t)));
|
||||
Assert.Equal("""@Tags:{"a" | "b"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Any_with_metacharacters_in_values()
|
||||
{
|
||||
var values = new[] { "a|b", "c}d" };
|
||||
var result = Translate<TestRecord>(r => r.Tags.Any(t => values.Contains(t)));
|
||||
Assert.Equal("""@Tags:{"a|b" | "c}d"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Any_with_double_quotes_in_values()
|
||||
{
|
||||
var values = new[] { "a\"b", "c\"d" };
|
||||
var result = Translate<TestRecord>(r => r.Tags.Any(t => values.Contains(t)));
|
||||
Assert.Equal("""@Tags:{"a\"b" | "c\"d"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Any_with_injection_attempt()
|
||||
{
|
||||
var values = new[] { "safe", "x} | @admin:{true" };
|
||||
var result = Translate<TestRecord>(r => r.Tags.Any(t => values.Contains(t)));
|
||||
Assert.Equal("""@Tags:{"safe" | "x} | @admin:{true"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_with_simple_string()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Name == "foo");
|
||||
Assert.Equal("""@Name:{"foo"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_with_double_quote_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Name == "foo\"bar");
|
||||
Assert.Equal("""@Name:{"foo\"bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_with_backslash_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Name == "foo\\bar");
|
||||
Assert.Equal("""@Name:{"foo\\bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_with_single_backslash()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Name == "\\");
|
||||
Assert.Equal("""@Name:{"\\"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_with_backslash_quote_injection_attempt()
|
||||
{
|
||||
// Input: \" which should NOT break out of the quoted string
|
||||
var result = Translate<TestRecord>(r => r.Name == "\\\"");
|
||||
Assert.Equal("""@Name:{"\\\""}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_with_backslash_quote_wildcard_injection()
|
||||
{
|
||||
// The specific attack payload: \" | * | \"
|
||||
var result = Translate<TestRecord>(r => r.Name == "\\\" | * | \\\"");
|
||||
Assert.Equal("""@Name:{"\\\" | * | \\\""}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_backslash_in_value()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("foo\\bar"));
|
||||
Assert.Equal("""@Tags:{"foo\\bar"}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_with_backslash_quote_injection_attempt()
|
||||
{
|
||||
var result = Translate<TestRecord>(r => r.Tags.Contains("\\\""));
|
||||
Assert.Equal("""@Tags:{"\\\""}""", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Any_with_backslash_in_values()
|
||||
{
|
||||
var values = new[] { "a\\b", "c\\d" };
|
||||
var result = Translate<TestRecord>(r => r.Tags.Any(t => values.Contains(t)));
|
||||
Assert.Equal("""@Tags:{"a\\b" | "c\\d"}""", result);
|
||||
}
|
||||
|
||||
private static string Translate<TRecord>(Expression<Func<TRecord, bool>> filter)
|
||||
{
|
||||
var model = BuildModel();
|
||||
return new RedisFilterTranslator().Translate(filter, model);
|
||||
}
|
||||
|
||||
private static CollectionModel BuildModel()
|
||||
{
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Id", typeof(string)),
|
||||
new VectorStoreDataProperty("Name", typeof(string)),
|
||||
new VectorStoreDataProperty("Tags", typeof(string[])),
|
||||
new VectorStoreVectorProperty("Embedding", typeof(ReadOnlyMemory<float>), 10)
|
||||
]
|
||||
};
|
||||
|
||||
return new RedisJsonModelBuilder(RedisJsonCollection<string, object>.ModelBuildingOptions).BuildDynamic(definition, defaultEmbeddingGenerator: null);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812
|
||||
private sealed record TestRecord
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string[] Tags { get; set; } = [];
|
||||
public ReadOnlyMemory<float> Embedding { get; set; }
|
||||
}
|
||||
#pragma warning restore CA1812
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Moq;
|
||||
using NRedisStack;
|
||||
using StackExchange.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisHashSetCollection{TKey, TRecord}"/> class.
|
||||
/// </summary>
|
||||
public class RedisHashSetCollectionTests
|
||||
{
|
||||
private const string TestCollectionName = "testcollection";
|
||||
private const string TestRecordKey1 = "testid1";
|
||||
private const string TestRecordKey2 = "testid2";
|
||||
|
||||
private readonly Mock<IDatabase> _redisDatabaseMock;
|
||||
|
||||
public RedisHashSetCollectionTests()
|
||||
{
|
||||
this._redisDatabaseMock = new Mock<IDatabase>(MockBehavior.Strict);
|
||||
this._redisDatabaseMock.Setup(l => l.Database).Returns(0);
|
||||
|
||||
var batchMock = new Mock<IBatch>();
|
||||
this._redisDatabaseMock.Setup(x => x.CreateBatch(It.IsAny<object>())).Returns(batchMock.Object);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TestCollectionName, true)]
|
||||
[InlineData("nonexistentcollection", false)]
|
||||
public async Task CollectionExistsReturnsCollectionStateAsync(string collectionName, bool expectedExists)
|
||||
{
|
||||
// Arrange
|
||||
if (expectedExists)
|
||||
{
|
||||
SetupExecuteMock(this._redisDatabaseMock, ["index_name", collectionName]);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetupExecuteMock(this._redisDatabaseMock, new RedisServerException("Unknown index name"));
|
||||
}
|
||||
using var sut = new RedisHashSetCollection<string, SinglePropsModel>(
|
||||
this._redisDatabaseMock.Object,
|
||||
collectionName);
|
||||
|
||||
// Act
|
||||
var actual = await sut.CollectionExistsAsync();
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { collectionName };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.INFO",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
Assert.Equal(expectedExists, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanEnsureCollectionExistsAsync()
|
||||
{
|
||||
// Arrange.
|
||||
SetupExecuteMock(this._redisDatabaseMock, "FT.INFO", new RedisServerException("Unknown index name"));
|
||||
SetupExecuteMock(this._redisDatabaseMock, "FT.CREATE", string.Empty);
|
||||
using var sut = new RedisHashSetCollection<string, SinglePropsModel>(this._redisDatabaseMock.Object, TestCollectionName);
|
||||
|
||||
// Act.
|
||||
await sut.EnsureCollectionExistsAsync();
|
||||
|
||||
// Assert.
|
||||
var expectedArgs = new object[] {
|
||||
"testcollection",
|
||||
"PREFIX",
|
||||
1,
|
||||
"testcollection:",
|
||||
"SCHEMA",
|
||||
"OriginalNameData",
|
||||
"AS",
|
||||
"OriginalNameData",
|
||||
"TAG",
|
||||
"data_storage_name",
|
||||
"AS",
|
||||
"data_storage_name",
|
||||
"TAG",
|
||||
"vector_storage_name",
|
||||
"AS",
|
||||
"vector_storage_name",
|
||||
"VECTOR",
|
||||
"HNSW",
|
||||
6,
|
||||
"TYPE",
|
||||
"FLOAT32",
|
||||
"DIM",
|
||||
"4",
|
||||
"DISTANCE_METRIC",
|
||||
"COSINE" };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.CREATE",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDeleteCollectionAsync()
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, string.Empty);
|
||||
using var sut = this.CreateRecordCollection(false);
|
||||
|
||||
// Act
|
||||
await sut.EnsureCollectionDeletedAsync();
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { TestCollectionName };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.DROPINDEX",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanGetRecordWithVectorsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
var hashEntries = new HashEntry[]
|
||||
{
|
||||
new("OriginalNameData", "data 1"),
|
||||
new("data_storage_name", "data 1"),
|
||||
new("vector_storage_name", MemoryMarshal.AsBytes(new ReadOnlySpan<float>(new float[] { 1, 2, 3, 4 })).ToArray())
|
||||
};
|
||||
this._redisDatabaseMock.Setup(x => x.HashGetAllAsync(It.IsAny<RedisKey>(), CommandFlags.None)).ReturnsAsync(hashEntries);
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
var actual = await sut.GetAsync(
|
||||
TestRecordKey1,
|
||||
new() { IncludeVectors = true });
|
||||
|
||||
// Assert
|
||||
this._redisDatabaseMock.Verify(x => x.HashGetAllAsync(TestRecordKey1, CommandFlags.None), Times.Once);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal(TestRecordKey1, actual.Key);
|
||||
Assert.Equal("data 1", actual.OriginalNameData);
|
||||
Assert.Equal("data 1", actual.Data);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.Vector!.Value.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanGetRecordWithoutVectorsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
var redisValues = new RedisValue[] { new("data 1"), new("data 1") };
|
||||
this._redisDatabaseMock.Setup(x => x.HashGetAsync(It.IsAny<RedisKey>(), It.IsAny<RedisValue[]>(), CommandFlags.None)).ReturnsAsync(redisValues);
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
var actual = await sut.GetAsync(
|
||||
TestRecordKey1,
|
||||
new() { IncludeVectors = false });
|
||||
|
||||
// Assert
|
||||
var fieldNames = new RedisValue[] { "OriginalNameData", "data_storage_name" };
|
||||
this._redisDatabaseMock.Verify(x => x.HashGetAsync(TestRecordKey1, fieldNames, CommandFlags.None), Times.Once);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal(TestRecordKey1, actual.Key);
|
||||
Assert.Equal("data 1", actual.OriginalNameData);
|
||||
Assert.Equal("data 1", actual.Data);
|
||||
Assert.False(actual.Vector.HasValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanGetManyRecordsWithVectorsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
var hashEntries1 = new HashEntry[]
|
||||
{
|
||||
new("OriginalNameData", "data 1"),
|
||||
new("data_storage_name", "data 1"),
|
||||
new("vector_storage_name", MemoryMarshal.AsBytes(new ReadOnlySpan<float>(new float[] { 1, 2, 3, 4 })).ToArray())
|
||||
};
|
||||
var hashEntries2 = new HashEntry[]
|
||||
{
|
||||
new("OriginalNameData", "data 2"),
|
||||
new("data_storage_name", "data 2"),
|
||||
new("vector_storage_name", MemoryMarshal.AsBytes(new ReadOnlySpan<float>(new float[] { 5, 6, 7, 8 })).ToArray())
|
||||
};
|
||||
this._redisDatabaseMock.Setup(x => x.HashGetAllAsync(It.IsAny<RedisKey>(), CommandFlags.None)).Returns((RedisKey key, CommandFlags flags) =>
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
RedisKey k when k == TestRecordKey1 => Task.FromResult(hashEntries1),
|
||||
RedisKey k when k == TestRecordKey2 => Task.FromResult(hashEntries2),
|
||||
_ => throw new ArgumentException("Unexpected key."),
|
||||
};
|
||||
});
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
var actual = await sut.GetAsync(
|
||||
[TestRecordKey1, TestRecordKey2],
|
||||
new() { IncludeVectors = true }).ToListAsync();
|
||||
|
||||
// Assert
|
||||
this._redisDatabaseMock.Verify(x => x.HashGetAllAsync(TestRecordKey1, CommandFlags.None), Times.Once);
|
||||
this._redisDatabaseMock.Verify(x => x.HashGetAllAsync(TestRecordKey2, CommandFlags.None), Times.Once);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal(2, actual.Count);
|
||||
Assert.Equal(TestRecordKey1, actual[0].Key);
|
||||
Assert.Equal("data 1", actual[0].OriginalNameData);
|
||||
Assert.Equal("data 1", actual[0].Data);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual[0].Vector!.Value.ToArray());
|
||||
Assert.Equal(TestRecordKey2, actual[1].Key);
|
||||
Assert.Equal("data 2", actual[1].OriginalNameData);
|
||||
Assert.Equal("data 2", actual[1].Data);
|
||||
Assert.Equal(new float[] { 5, 6, 7, 8 }, actual[1].Vector!.Value.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanDeleteRecordAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
this._redisDatabaseMock.Setup(x => x.KeyDeleteAsync(It.IsAny<RedisKey>(), CommandFlags.None)).ReturnsAsync(true);
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync(TestRecordKey1);
|
||||
|
||||
// Assert
|
||||
this._redisDatabaseMock.Verify(x => x.KeyDeleteAsync(TestRecordKey1, CommandFlags.None), Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanDeleteManyRecordsWithVectorsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
this._redisDatabaseMock.Setup(x => x.KeyDeleteAsync(It.IsAny<RedisKey>(), CommandFlags.None)).ReturnsAsync(true);
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync([TestRecordKey1, TestRecordKey2]);
|
||||
|
||||
// Assert
|
||||
this._redisDatabaseMock.Verify(x => x.KeyDeleteAsync(TestRecordKey1, CommandFlags.None), Times.Once);
|
||||
this._redisDatabaseMock.Verify(x => x.KeyDeleteAsync(TestRecordKey2, CommandFlags.None), Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanUpsertRecordAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
this._redisDatabaseMock.Setup(x => x.HashSetAsync(It.IsAny<RedisKey>(), It.IsAny<HashEntry[]>(), CommandFlags.None)).Returns(Task.CompletedTask);
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
var model = CreateModel(TestRecordKey1, true);
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync(model);
|
||||
|
||||
// Assert
|
||||
this._redisDatabaseMock.Verify(
|
||||
x => x.HashSetAsync(
|
||||
TestRecordKey1,
|
||||
It.Is<HashEntry[]>(x => x.Length == 3 && x[0].Name == "OriginalNameData" && x[1].Name == "data_storage_name" && x[2].Name == "vector_storage_name"),
|
||||
CommandFlags.None),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanUpsertManyRecordsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
this._redisDatabaseMock.Setup(x => x.HashSetAsync(It.IsAny<RedisKey>(), It.IsAny<HashEntry[]>(), CommandFlags.None)).Returns(Task.CompletedTask);
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
var model1 = CreateModel(TestRecordKey1, true);
|
||||
var model2 = CreateModel(TestRecordKey2, true);
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync([model1, model2]);
|
||||
|
||||
// Assert
|
||||
this._redisDatabaseMock.Verify(
|
||||
x => x.HashSetAsync(
|
||||
TestRecordKey1,
|
||||
It.Is<HashEntry[]>(x => x.Length == 3 && x[0].Name == "OriginalNameData" && x[1].Name == "data_storage_name" && x[2].Name == "vector_storage_name"),
|
||||
CommandFlags.None),
|
||||
Times.Once);
|
||||
this._redisDatabaseMock.Verify(
|
||||
x => x.HashSetAsync(
|
||||
TestRecordKey2,
|
||||
It.Is<HashEntry[]>(x => x.Length == 3 && x[0].Name == "OriginalNameData" && x[1].Name == "data_storage_name" && x[2].Name == "vector_storage_name"),
|
||||
CommandFlags.None),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(false, false)]
|
||||
public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition, bool includeVectors)
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, new RedisResult[]
|
||||
{
|
||||
RedisResult.Create(new RedisValue("1")),
|
||||
RedisResult.Create(new RedisValue(TestRecordKey1)),
|
||||
RedisResult.Create(new RedisValue("0.8")),
|
||||
RedisResult.Create(
|
||||
[
|
||||
new RedisValue("OriginalNameData"),
|
||||
new RedisValue("original data 1"),
|
||||
new RedisValue("data_storage_name"),
|
||||
new RedisValue("data 1"),
|
||||
new RedisValue("vector_storage_name"),
|
||||
RedisValue.Unbox(MemoryMarshal.AsBytes(new ReadOnlySpan<float>(new float[] { 1, 2, 3, 4 })).ToArray()),
|
||||
new RedisValue("vector_score"),
|
||||
new RedisValue("0.25"),
|
||||
]),
|
||||
});
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act.
|
||||
var results = await sut.SearchAsync(
|
||||
new ReadOnlyMemory<float>(new[] { 1f, 2f, 3f, 4f }),
|
||||
top: 5,
|
||||
new()
|
||||
{
|
||||
IncludeVectors = includeVectors,
|
||||
Filter = r => r.Data == "data 1",
|
||||
Skip = 2
|
||||
}).ToListAsync();
|
||||
|
||||
// Assert.
|
||||
var expectedArgsPart1 = new object[]
|
||||
{
|
||||
"testcollection",
|
||||
"@data_storage_name:{\"data 1\"}=>[KNN 7 @vector_storage_name $embedding AS vector_score]",
|
||||
"WITHSCORES",
|
||||
"SORTBY",
|
||||
"vector_score",
|
||||
"LIMIT",
|
||||
2,
|
||||
7
|
||||
};
|
||||
var returnArgs = includeVectors ? Array.Empty<object>() : new object[]
|
||||
{
|
||||
"RETURN",
|
||||
3,
|
||||
"OriginalNameData",
|
||||
"data_storage_name",
|
||||
"vector_score"
|
||||
};
|
||||
var expectedArgsPart2 = new object[]
|
||||
{
|
||||
"PARAMS",
|
||||
2,
|
||||
"embedding",
|
||||
MemoryMarshal.AsBytes(new ReadOnlySpan<float>(new float[] { 1, 2, 3, 4 })).ToArray(),
|
||||
"DIALECT",
|
||||
2
|
||||
};
|
||||
var expectedArgs = expectedArgsPart1.Concat(returnArgs).Concat(expectedArgsPart2).ToArray();
|
||||
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.SEARCH",
|
||||
It.Is<object[]>(x => x.Where(y => !(y is byte[])).SequenceEqual(expectedArgs.Where(y => !(y is byte[]))))),
|
||||
Times.Once);
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Equal(TestRecordKey1, results.First().Record.Key);
|
||||
Assert.Equal(0.25d, results.First().Score);
|
||||
Assert.Equal("original data 1", results.First().Record.OriginalNameData);
|
||||
Assert.Equal("data 1", results.First().Record.Data);
|
||||
if (includeVectors)
|
||||
{
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, results.First().Record.Vector!.Value.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.False(results.First().Record.Vector.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the collection can be created even if the definition and the type do not match.
|
||||
/// In this case, the expectation is that a custom mapper will be provided to map between the
|
||||
/// schema as defined by the definition and the different data model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CanCreateCollectionWithMismatchedDefinitionAndType()
|
||||
{
|
||||
// Arrange.
|
||||
var definition = new VectorStoreCollectionDefinition()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(nameof(SinglePropsModel.Key), typeof(string)),
|
||||
new VectorStoreDataProperty(nameof(SinglePropsModel.OriginalNameData), typeof(string)),
|
||||
new VectorStoreVectorProperty(nameof(SinglePropsModel.Vector), typeof(ReadOnlyMemory<float>?), 4),
|
||||
]
|
||||
};
|
||||
|
||||
// Act.
|
||||
using var sut = new RedisHashSetCollection<string, SinglePropsModel>(
|
||||
this._redisDatabaseMock.Object,
|
||||
TestCollectionName,
|
||||
new() { Definition = definition });
|
||||
}
|
||||
|
||||
private RedisHashSetCollection<string, SinglePropsModel> CreateRecordCollection(bool useDefinition)
|
||||
{
|
||||
return new RedisHashSetCollection<string, SinglePropsModel>(
|
||||
this._redisDatabaseMock.Object,
|
||||
TestCollectionName,
|
||||
new()
|
||||
{
|
||||
PrefixCollectionNameToKeyNames = false,
|
||||
Definition = useDefinition ? this._singlePropsDefinition : null
|
||||
});
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, Exception exception)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.ThrowsAsync(exception);
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, string command, Exception exception)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
command,
|
||||
It.IsAny<object[]>()))
|
||||
.ThrowsAsync(exception);
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, IEnumerable<string> redisResultStrings)
|
||||
{
|
||||
var results = redisResultStrings
|
||||
.Select(x => RedisResult.Create(new RedisValue(x)))
|
||||
.ToArray();
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.ReturnsAsync(RedisResult.Create(results));
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, IEnumerable<RedisResult> redisResultStrings)
|
||||
{
|
||||
var results = redisResultStrings
|
||||
.Select(x => x)
|
||||
.ToArray();
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.ReturnsAsync(RedisResult.Create(results));
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, string redisResultString)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.Callback((string command, object[] args) =>
|
||||
{
|
||||
Console.WriteLine(args);
|
||||
})
|
||||
.ReturnsAsync(RedisResult.Create(new RedisValue(redisResultString)));
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, string command, string redisResultString)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
command,
|
||||
It.IsAny<object[]>()))
|
||||
.Callback((string command, object[] args) =>
|
||||
{
|
||||
Console.WriteLine(args);
|
||||
})
|
||||
.ReturnsAsync(RedisResult.Create(new RedisValue(redisResultString)));
|
||||
}
|
||||
|
||||
private static SinglePropsModel CreateModel(string key, bool withVectors)
|
||||
{
|
||||
return new SinglePropsModel
|
||||
{
|
||||
Key = key,
|
||||
OriginalNameData = "data 1",
|
||||
Data = "data 1",
|
||||
Vector = withVectors ? new float[] { 1, 2, 3, 4 } : null,
|
||||
NotAnnotated = null,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly VectorStoreCollectionDefinition _singlePropsDefinition = new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("OriginalNameData", typeof(string)),
|
||||
new VectorStoreDataProperty("Data", typeof(string)) { StorageName = "data_storage_name" },
|
||||
new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory<float>), 10) { StorageName = "vector_storage_name", DistanceFunction = DistanceFunction.CosineDistance }
|
||||
]
|
||||
};
|
||||
|
||||
public sealed class SinglePropsModel
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreData(IsIndexed = true)]
|
||||
public string OriginalNameData { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ignored_data_json_name")]
|
||||
[VectorStoreData(IsIndexed = true, StorageName = "data_storage_name")]
|
||||
public string Data { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ignored_vector_json_name")]
|
||||
[VectorStoreVector(4, DistanceFunction = DistanceFunction.CosineDistance, StorageName = "vector_storage_name")]
|
||||
public ReadOnlyMemory<float>? Vector { get; set; }
|
||||
|
||||
public string? NotAnnotated { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using StackExchange.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains dynamic mapping tests for the <see cref="RedisHashSetMapper{TConsumerDataModel}"/> class.
|
||||
/// </summary>
|
||||
public class RedisHashSetDynamicMappingTests
|
||||
{
|
||||
private static readonly CollectionModel s_model = BuildModel(RedisHashSetMappingTestHelpers.s_definition);
|
||||
|
||||
private static readonly float[] s_floatVector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f };
|
||||
private static readonly double[] s_doubleVector = new double[] { 5.0d, 6.0d, 7.0d, 8.0d };
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsAllSupportedTypes()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisHashSetMapper<Dictionary<string, object?>>(s_model);
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = "key",
|
||||
|
||||
["StringData"] = "data 1",
|
||||
["IntData"] = 1,
|
||||
["UIntData"] = 2u,
|
||||
["LongData"] = 3L,
|
||||
["ULongData"] = 4ul,
|
||||
["DoubleData"] = 5.5d,
|
||||
["FloatData"] = 6.6f,
|
||||
["NullableIntData"] = 7,
|
||||
["NullableUIntData"] = 8u,
|
||||
["NullableLongData"] = 9L,
|
||||
["NullableULongData"] = 10ul,
|
||||
["NullableDoubleData"] = 11.1d,
|
||||
["NullableFloatData"] = 12.2f,
|
||||
|
||||
["FloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
|
||||
["DoubleVector"] = new ReadOnlyMemory<double>(s_doubleVector),
|
||||
};
|
||||
|
||||
// Act.
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", storageModel.Key);
|
||||
RedisHashSetMappingTestHelpers.VerifyHashSet(storageModel.HashEntries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsNullValues()
|
||||
{
|
||||
// Arrange
|
||||
CollectionModel model = BuildModel(new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringData", typeof(string)) { StorageName = "storage_string_data" },
|
||||
new VectorStoreDataProperty("NullableIntData", typeof(int?)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>?), 10),
|
||||
],
|
||||
});
|
||||
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = "key",
|
||||
["StringData"] = null,
|
||||
["NullableIntData"] = null,
|
||||
["FloatVector"] = null,
|
||||
};
|
||||
|
||||
var sut = new RedisHashSetMapper<Dictionary<string, object?>>(model);
|
||||
|
||||
// Act
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", storageModel.Key);
|
||||
|
||||
Assert.Equal("storage_string_data", storageModel.HashEntries[0].Name.ToString());
|
||||
Assert.True(storageModel.HashEntries[0].Value.IsNull);
|
||||
|
||||
Assert.Equal("NullableIntData", storageModel.HashEntries[1].Name.ToString());
|
||||
Assert.True(storageModel.HashEntries[1].Value.IsNull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelMapsAllSupportedTypes()
|
||||
{
|
||||
// Arrange.
|
||||
var hashSet = RedisHashSetMappingTestHelpers.CreateHashSet();
|
||||
var sut = new RedisHashSetMapper<Dictionary<string, object?>>(s_model);
|
||||
|
||||
// Act.
|
||||
var dataModel = sut.MapFromStorageToDataModel(("key", hashSet), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.Equal("data 1", dataModel["StringData"]);
|
||||
Assert.Equal(1, dataModel["IntData"]);
|
||||
Assert.Equal(2u, dataModel["UIntData"]);
|
||||
Assert.Equal(3L, dataModel["LongData"]);
|
||||
Assert.Equal(4ul, dataModel["ULongData"]);
|
||||
Assert.Equal(5.5d, dataModel["DoubleData"]);
|
||||
Assert.Equal(6.6f, dataModel["FloatData"]);
|
||||
Assert.Equal(7, dataModel["NullableIntData"]);
|
||||
Assert.Equal(8u, dataModel["NullableUIntData"]);
|
||||
Assert.Equal(9L, dataModel["NullableLongData"]);
|
||||
Assert.Equal(10ul, dataModel["NullableULongData"]);
|
||||
Assert.Equal(11.1d, dataModel["NullableDoubleData"]);
|
||||
Assert.Equal(12.2f, dataModel["NullableFloatData"]);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
|
||||
Assert.Equal(new double[] { 5, 6, 7, 8 }, ((ReadOnlyMemory<double>)dataModel["DoubleVector"]!).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelMapsNullValues()
|
||||
{
|
||||
// Arrange
|
||||
var model = BuildModel(new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringData", typeof(string)) { StorageName = "storage_string_data" },
|
||||
new VectorStoreDataProperty("NullableIntData", typeof(int?)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>?), 10),
|
||||
]
|
||||
});
|
||||
|
||||
var hashSet = new HashEntry[]
|
||||
{
|
||||
new("storage_string_data", RedisValue.Null),
|
||||
new("NullableIntData", RedisValue.Null),
|
||||
new("FloatVector", RedisValue.Null),
|
||||
};
|
||||
|
||||
var sut = new RedisHashSetMapper<Dictionary<string, object?>>(model);
|
||||
|
||||
// Act
|
||||
var dataModel = sut.MapFromStorageToDataModel(("key", hashSet), includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.Null(dataModel["StringData"]);
|
||||
Assert.Null(dataModel["NullableIntData"]);
|
||||
Assert.Null(dataModel["FloatVector"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelSkipsMissingProperties()
|
||||
{
|
||||
// Arrange.
|
||||
var model = BuildModel(new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringData", typeof(string)) { StorageName = "storage_string_data" },
|
||||
new VectorStoreDataProperty("NullableIntData", typeof(int?)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>?), 10),
|
||||
]
|
||||
});
|
||||
|
||||
var sut = new RedisHashSetMapper<Dictionary<string, object?>>(model);
|
||||
var dataModel = new Dictionary<string, object?> { ["Key"] = "key" };
|
||||
|
||||
// Act.
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", storageModel.Key);
|
||||
|
||||
Assert.Equal("storage_string_data", storageModel.HashEntries[0].Name.ToString());
|
||||
Assert.True(storageModel.HashEntries[0].Value.IsNull);
|
||||
|
||||
Assert.Equal("NullableIntData", storageModel.HashEntries[1].Name.ToString());
|
||||
Assert.True(storageModel.HashEntries[1].Value.IsNull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelSkipsMissingProperties()
|
||||
{
|
||||
// Arrange.
|
||||
var hashSet = Array.Empty<HashEntry>();
|
||||
|
||||
var sut = new RedisHashSetMapper<Dictionary<string, object?>>(s_model);
|
||||
|
||||
// Act.
|
||||
var dataModel = sut.MapFromStorageToDataModel(("key", hashSet), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.Single(dataModel);
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
}
|
||||
|
||||
private static CollectionModel BuildModel(VectorStoreCollectionDefinition definition)
|
||||
=> new RedisModelBuilder(RedisHashSetCollection<object, Dictionary<string, object?>>.ModelBuildingOptions)
|
||||
.BuildDynamic(definition, defaultEmbeddingGenerator: null);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.Redis;
|
||||
using Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisHashSetMapper{TConsumerDataModel}"/> class.
|
||||
/// </summary>
|
||||
public sealed class RedisHashSetMapperTests
|
||||
{
|
||||
private static readonly CollectionModel s_model
|
||||
= new RedisModelBuilder(RedisHashSetCollection<string, AllTypesModel>.ModelBuildingOptions)
|
||||
.Build(typeof(AllTypesModel), typeof(string), RedisHashSetMappingTestHelpers.s_definition, defaultEmbeddingGenerator: null);
|
||||
|
||||
[Fact]
|
||||
public void MapsAllFieldsFromDataToStorageModel()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisHashSetMapper<AllTypesModel>(s_model);
|
||||
|
||||
// Act.
|
||||
var actual = sut.MapFromDataToStorageModel(CreateModel("test key"), recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual.HashEntries);
|
||||
Assert.Equal("test key", actual.Key);
|
||||
RedisHashSetMappingTestHelpers.VerifyHashSet(actual.HashEntries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapsAllFieldsFromStorageToDataModel()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisHashSetMapper<AllTypesModel>(s_model);
|
||||
|
||||
// Act.
|
||||
var actual = sut.MapFromStorageToDataModel(("test key", RedisHashSetMappingTestHelpers.CreateHashSet()), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("test key", actual.Key);
|
||||
Assert.Equal("data 1", actual.StringData);
|
||||
Assert.Equal(1, actual.IntData);
|
||||
Assert.Equal(2u, actual.UIntData);
|
||||
Assert.Equal(3, actual.LongData);
|
||||
Assert.Equal(4ul, actual.ULongData);
|
||||
Assert.Equal(5.5d, actual.DoubleData);
|
||||
Assert.Equal(6.6f, actual.FloatData);
|
||||
Assert.Equal(7, actual.NullableIntData);
|
||||
Assert.Equal(8u, actual.NullableUIntData);
|
||||
Assert.Equal(9, actual.NullableLongData);
|
||||
Assert.Equal(10ul, actual.NullableULongData);
|
||||
Assert.Equal(11.1d, actual.NullableDoubleData);
|
||||
Assert.Equal(12.2f, actual.NullableFloatData);
|
||||
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.FloatVector!.Value.ToArray());
|
||||
Assert.Equal(new double[] { 5, 6, 7, 8 }, actual.DoubleVector!.Value.ToArray());
|
||||
}
|
||||
|
||||
private static AllTypesModel CreateModel(string key)
|
||||
{
|
||||
return new AllTypesModel
|
||||
{
|
||||
Key = key,
|
||||
StringData = "data 1",
|
||||
IntData = 1,
|
||||
UIntData = 2,
|
||||
LongData = 3,
|
||||
ULongData = 4,
|
||||
DoubleData = 5.5d,
|
||||
FloatData = 6.6f,
|
||||
NullableIntData = 7,
|
||||
NullableUIntData = 8,
|
||||
NullableLongData = 9,
|
||||
NullableULongData = 10,
|
||||
NullableDoubleData = 11.1d,
|
||||
NullableFloatData = 12.2f,
|
||||
FloatVector = new float[] { 1, 2, 3, 4 },
|
||||
DoubleVector = new double[] { 5, 6, 7, 8 },
|
||||
NotAnnotated = "notAnnotated",
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class AllTypesModel
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreData(StorageName = "storage_string_data")]
|
||||
public string StringData { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreData]
|
||||
public int IntData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public uint UIntData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public long LongData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public ulong ULongData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public double DoubleData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public float FloatData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public int? NullableIntData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public uint? NullableUIntData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public long? NullableLongData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public ulong? NullableULongData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public double? NullableDoubleData { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public float? NullableFloatData { get; set; }
|
||||
|
||||
[VectorStoreVector(10)]
|
||||
public ReadOnlyMemory<float>? FloatVector { get; set; }
|
||||
|
||||
[VectorStoreVector(10)]
|
||||
public ReadOnlyMemory<double>? DoubleVector { get; set; }
|
||||
|
||||
public string NotAnnotated { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using StackExchange.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains helper methods and data for testing the mapping of records between storage and data models.
|
||||
/// These helpers are shared between the different mapping tests.
|
||||
/// </summary>
|
||||
internal static class RedisHashSetMappingTestHelpers
|
||||
{
|
||||
public static readonly VectorStoreCollectionDefinition s_definition = new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringData", typeof(string)) { StorageName = "storage_string_data" },
|
||||
new VectorStoreDataProperty("IntData", typeof(int)),
|
||||
new VectorStoreDataProperty("UIntData", typeof(uint)),
|
||||
new VectorStoreDataProperty("LongData", typeof(long)),
|
||||
new VectorStoreDataProperty("ULongData", typeof(ulong)),
|
||||
new VectorStoreDataProperty("DoubleData", typeof(double)),
|
||||
new VectorStoreDataProperty("FloatData", typeof(float)),
|
||||
new VectorStoreDataProperty("NullableIntData", typeof(int?)),
|
||||
new VectorStoreDataProperty("NullableUIntData", typeof(uint?)),
|
||||
new VectorStoreDataProperty("NullableLongData", typeof(long?)),
|
||||
new VectorStoreDataProperty("NullableULongData", typeof(ulong?)),
|
||||
new VectorStoreDataProperty("NullableDoubleData", typeof(double?)),
|
||||
new VectorStoreDataProperty("NullableFloatData", typeof(float?)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
|
||||
new VectorStoreVectorProperty("DoubleVector", typeof(ReadOnlyMemory<double>), 10),
|
||||
]
|
||||
};
|
||||
|
||||
public static HashEntry[] CreateHashSet()
|
||||
{
|
||||
var hashSet = new HashEntry[15];
|
||||
hashSet[0] = new HashEntry("storage_string_data", "data 1");
|
||||
hashSet[1] = new HashEntry("IntData", 1);
|
||||
hashSet[2] = new HashEntry("UIntData", 2);
|
||||
hashSet[3] = new HashEntry("LongData", 3);
|
||||
hashSet[4] = new HashEntry("ULongData", 4);
|
||||
hashSet[5] = new HashEntry("DoubleData", 5.5);
|
||||
hashSet[6] = new HashEntry("FloatData", 6.6);
|
||||
hashSet[7] = new HashEntry("NullableIntData", 7);
|
||||
hashSet[8] = new HashEntry("NullableUIntData", 8);
|
||||
hashSet[9] = new HashEntry("NullableLongData", 9);
|
||||
hashSet[10] = new HashEntry("NullableULongData", 10);
|
||||
hashSet[11] = new HashEntry("NullableDoubleData", 11.1);
|
||||
hashSet[12] = new HashEntry("NullableFloatData", 12.2);
|
||||
hashSet[13] = new HashEntry("FloatVector", MemoryMarshal.AsBytes(new ReadOnlySpan<float>(new float[] { 1, 2, 3, 4 })).ToArray());
|
||||
hashSet[14] = new HashEntry("DoubleVector", MemoryMarshal.AsBytes(new ReadOnlySpan<double>(new double[] { 5, 6, 7, 8 })).ToArray());
|
||||
return hashSet;
|
||||
}
|
||||
|
||||
public static void VerifyHashSet(HashEntry[] hashEntries)
|
||||
{
|
||||
Assert.Equal("storage_string_data", hashEntries[0].Name.ToString());
|
||||
Assert.Equal("data 1", hashEntries[0].Value.ToString());
|
||||
|
||||
Assert.Equal("IntData", hashEntries[1].Name.ToString());
|
||||
Assert.Equal(1, (int)hashEntries[1].Value);
|
||||
|
||||
Assert.Equal("UIntData", hashEntries[2].Name.ToString());
|
||||
Assert.Equal(2u, (uint)hashEntries[2].Value);
|
||||
|
||||
Assert.Equal("LongData", hashEntries[3].Name.ToString());
|
||||
Assert.Equal(3, (long)hashEntries[3].Value);
|
||||
|
||||
Assert.Equal("ULongData", hashEntries[4].Name.ToString());
|
||||
Assert.Equal(4ul, (ulong)hashEntries[4].Value);
|
||||
|
||||
Assert.Equal("DoubleData", hashEntries[5].Name.ToString());
|
||||
Assert.Equal(5.5d, (double)hashEntries[5].Value);
|
||||
|
||||
Assert.Equal("FloatData", hashEntries[6].Name.ToString());
|
||||
Assert.Equal(6.6f, (float)hashEntries[6].Value);
|
||||
|
||||
Assert.Equal("NullableIntData", hashEntries[7].Name.ToString());
|
||||
Assert.Equal(7, (int)hashEntries[7].Value);
|
||||
|
||||
Assert.Equal("NullableUIntData", hashEntries[8].Name.ToString());
|
||||
Assert.Equal(8u, (uint)hashEntries[8].Value);
|
||||
|
||||
Assert.Equal("NullableLongData", hashEntries[9].Name.ToString());
|
||||
Assert.Equal(9, (long)hashEntries[9].Value);
|
||||
|
||||
Assert.Equal("NullableULongData", hashEntries[10].Name.ToString());
|
||||
Assert.Equal(10ul, (ulong)hashEntries[10].Value);
|
||||
|
||||
Assert.Equal("NullableDoubleData", hashEntries[11].Name.ToString());
|
||||
Assert.Equal(11.1d, (double)hashEntries[11].Value);
|
||||
|
||||
Assert.Equal("NullableFloatData", hashEntries[12].Name.ToString());
|
||||
Assert.Equal(12.2f, (float)hashEntries[12].Value);
|
||||
|
||||
Assert.Equal("FloatVector", hashEntries[13].Name.ToString());
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, MemoryMarshal.Cast<byte, float>((byte[])hashEntries[13].Value!).ToArray());
|
||||
|
||||
Assert.Equal("DoubleVector", hashEntries[14].Name.ToString());
|
||||
Assert.Equal(new double[] { 5, 6, 7, 8 }, MemoryMarshal.Cast<byte, double>((byte[])hashEntries[14].Value!).ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Moq;
|
||||
using NRedisStack;
|
||||
using StackExchange.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisJsonCollection{TKey, TRecord}"/> class.
|
||||
/// </summary>
|
||||
public class RedisJsonCollectionTests
|
||||
{
|
||||
private const string TestCollectionName = "testcollection";
|
||||
private const string TestRecordKey1 = "testid1";
|
||||
private const string TestRecordKey2 = "testid2";
|
||||
|
||||
private readonly Mock<IDatabase> _redisDatabaseMock;
|
||||
|
||||
public RedisJsonCollectionTests()
|
||||
{
|
||||
this._redisDatabaseMock = new Mock<IDatabase>(MockBehavior.Strict);
|
||||
this._redisDatabaseMock.Setup(l => l.Database).Returns(0);
|
||||
|
||||
var batchMock = new Mock<IBatch>();
|
||||
this._redisDatabaseMock.Setup(x => x.CreateBatch(It.IsAny<object>())).Returns(batchMock.Object);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TestCollectionName, true)]
|
||||
[InlineData("nonexistentcollection", false)]
|
||||
public async Task CollectionExistsReturnsCollectionStateAsync(string collectionName, bool expectedExists)
|
||||
{
|
||||
// Arrange
|
||||
if (expectedExists)
|
||||
{
|
||||
SetupExecuteMock(this._redisDatabaseMock, ["index_name", collectionName]);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetupExecuteMock(this._redisDatabaseMock, new RedisServerException("Unknown index name"));
|
||||
}
|
||||
using var sut = new RedisJsonCollection<string, MultiPropsModel>(
|
||||
this._redisDatabaseMock.Object,
|
||||
collectionName);
|
||||
|
||||
// Act
|
||||
var actual = await sut.CollectionExistsAsync();
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { collectionName };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.INFO",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
Assert.Equal(expectedExists, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true, "data2", "vector2")]
|
||||
[InlineData(true, false, "Data2", "Vector2")]
|
||||
[InlineData(false, true, "data2", "vector2")]
|
||||
[InlineData(false, false, "Data2", "Vector2")]
|
||||
public async Task CanEnsureCollectionExistsAsync(bool useDefinition, bool useCustomJsonSerializerOptions, string expectedData2Name, string expectedVector2Name)
|
||||
{
|
||||
// Arrange.
|
||||
SetupExecuteMock(this._redisDatabaseMock, "FT.INFO", new RedisServerException("Unknown index name"));
|
||||
SetupExecuteMock(this._redisDatabaseMock, "FT.CREATE", string.Empty);
|
||||
using var sut = this.CreateRecordCollection(useDefinition, useCustomJsonSerializerOptions);
|
||||
|
||||
// Act.
|
||||
await sut.EnsureCollectionExistsAsync();
|
||||
|
||||
// Assert.
|
||||
var expectedArgs = new object[] {
|
||||
"testcollection",
|
||||
"ON",
|
||||
"JSON",
|
||||
"PREFIX",
|
||||
1,
|
||||
"testcollection:",
|
||||
"SCHEMA",
|
||||
"$.data1_json_name",
|
||||
"AS",
|
||||
"data1_json_name",
|
||||
"TAG",
|
||||
$"$.{expectedData2Name}",
|
||||
"AS",
|
||||
expectedData2Name,
|
||||
"TAG",
|
||||
"$.vector1_json_name",
|
||||
"AS",
|
||||
"vector1_json_name",
|
||||
"VECTOR",
|
||||
"HNSW",
|
||||
6,
|
||||
"TYPE",
|
||||
"FLOAT32",
|
||||
"DIM",
|
||||
"4",
|
||||
"DISTANCE_METRIC",
|
||||
"COSINE",
|
||||
$"$.{expectedVector2Name}",
|
||||
"AS",
|
||||
expectedVector2Name,
|
||||
"VECTOR",
|
||||
"HNSW",
|
||||
6,
|
||||
"TYPE",
|
||||
"FLOAT32",
|
||||
"DIM",
|
||||
"4",
|
||||
"DISTANCE_METRIC",
|
||||
"COSINE" };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.CREATE",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDeleteCollectionAsync()
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, string.Empty);
|
||||
using var sut = this.CreateRecordCollection(false);
|
||||
|
||||
// Act
|
||||
await sut.EnsureCollectionDeletedAsync();
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { TestCollectionName };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.DROPINDEX",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true, """{ "data1_json_name": "data 1", "data2": "data 2", "vector1_json_name": [1, 2, 3, 4], "vector2": [1, 2, 3, 4] }""")]
|
||||
[InlineData(true, false, """{ "data1_json_name": "data 1", "Data2": "data 2", "vector1_json_name": [1, 2, 3, 4], "Vector2": [1, 2, 3, 4] }""")]
|
||||
[InlineData(false, true, """{ "data1_json_name": "data 1", "data2": "data 2", "vector1_json_name": [1, 2, 3, 4], "vector2": [1, 2, 3, 4] }""")]
|
||||
[InlineData(false, false, """{ "data1_json_name": "data 1", "Data2": "data 2", "vector1_json_name": [1, 2, 3, 4], "Vector2": [1, 2, 3, 4] }""")]
|
||||
public async Task CanGetRecordWithVectorsAsync(bool useDefinition, bool useCustomJsonSerializerOptions, string redisResultString)
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, redisResultString);
|
||||
using var sut = this.CreateRecordCollection(useDefinition, useCustomJsonSerializerOptions);
|
||||
|
||||
// Act
|
||||
var actual = await sut.GetAsync(
|
||||
TestRecordKey1,
|
||||
new() { IncludeVectors = true });
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { TestRecordKey1 };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.GET",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal(TestRecordKey1, actual.Key);
|
||||
Assert.Equal("data 1", actual.Data1);
|
||||
Assert.Equal("data 2", actual.Data2);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.Vector1!.Value.ToArray());
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.Vector2!.Value.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true, """{ "data1_json_name": "data 1", "data2": "data 2" }""", "data2")]
|
||||
[InlineData(true, false, """{ "data1_json_name": "data 1", "Data2": "data 2" }""", "Data2")]
|
||||
[InlineData(false, true, """{ "data1_json_name": "data 1", "data2": "data 2" }""", "data2")]
|
||||
[InlineData(false, false, """{ "data1_json_name": "data 1", "Data2": "data 2" }""", "Data2")]
|
||||
public async Task CanGetRecordWithoutVectorsAsync(bool useDefinition, bool useCustomJsonSerializerOptions, string redisResultString, string expectedData2Name)
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, redisResultString);
|
||||
using var sut = this.CreateRecordCollection(useDefinition, useCustomJsonSerializerOptions);
|
||||
|
||||
// Act
|
||||
var actual = await sut.GetAsync(
|
||||
TestRecordKey1,
|
||||
new() { IncludeVectors = false });
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { TestRecordKey1, "data1_json_name", expectedData2Name };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.GET",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal(TestRecordKey1, actual.Key);
|
||||
Assert.Equal("data 1", actual.Data1);
|
||||
Assert.Equal("data 2", actual.Data2);
|
||||
Assert.False(actual.Vector1.HasValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanGetManyRecordsWithVectorsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
var redisResultString1 = """{ "data1_json_name": "data 1", "Data2": "data 2", "vector1_json_name": [1, 2, 3, 4], "Vector2": [1, 2, 3, 4] }""";
|
||||
var redisResultString2 = """{ "data1_json_name": "data 1", "Data2": "data 2", "vector1_json_name": [5, 6, 7, 8], "Vector2": [1, 2, 3, 4] }""";
|
||||
SetupExecuteMock(this._redisDatabaseMock, [redisResultString1, redisResultString2]);
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
var actual = await sut.GetAsync(
|
||||
[TestRecordKey1, TestRecordKey2],
|
||||
new() { IncludeVectors = true }).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { TestRecordKey1, TestRecordKey2, "$" };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.MGET",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal(2, actual.Count);
|
||||
Assert.Equal(TestRecordKey1, actual[0].Key);
|
||||
Assert.Equal("data 1", actual[0].Data1);
|
||||
Assert.Equal("data 2", actual[0].Data2);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual[0].Vector1!.Value.ToArray());
|
||||
Assert.Equal(TestRecordKey2, actual[1].Key);
|
||||
Assert.Equal("data 1", actual[1].Data1);
|
||||
Assert.Equal("data 2", actual[1].Data2);
|
||||
Assert.Equal(new float[] { 5, 6, 7, 8 }, actual[1].Vector1!.Value.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanDeleteRecordAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, "200");
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync(TestRecordKey1);
|
||||
|
||||
// Assert
|
||||
var expectedArgs = new object[] { TestRecordKey1 };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.DEL",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanDeleteManyRecordsWithVectorsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, "200");
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync([TestRecordKey1, TestRecordKey2]);
|
||||
|
||||
// Assert
|
||||
var expectedArgs1 = new object[] { TestRecordKey1 };
|
||||
var expectedArgs2 = new object[] { TestRecordKey2 };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.DEL",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs1))),
|
||||
Times.Once);
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.DEL",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs2))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true, """{"data1_json_name":"data 1","data2":"data 2","vector1_json_name":[1,2,3,4],"vector2":[1,2,3,4],"notAnnotated":null}""")]
|
||||
[InlineData(true, false, """{"data1_json_name":"data 1","Data2":"data 2","vector1_json_name":[1,2,3,4],"Vector2":[1,2,3,4],"NotAnnotated":null}""")]
|
||||
[InlineData(false, true, """{"data1_json_name":"data 1","data2":"data 2","vector1_json_name":[1,2,3,4],"vector2":[1,2,3,4],"notAnnotated":null}""")]
|
||||
[InlineData(false, false, """{"data1_json_name":"data 1","Data2":"data 2","vector1_json_name":[1,2,3,4],"Vector2":[1,2,3,4],"NotAnnotated":null}""")]
|
||||
public async Task CanUpsertRecordAsync(bool useDefinition, bool useCustomJsonSerializerOptions, string expectedUpsertedJson)
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, "OK");
|
||||
using var sut = this.CreateRecordCollection(useDefinition, useCustomJsonSerializerOptions);
|
||||
var model = CreateModel(TestRecordKey1, true);
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync(model);
|
||||
|
||||
// Assert
|
||||
// TODO: Fix issue where NotAnnotated is being included in the JSON.
|
||||
var expectedArgs = new object[] { TestRecordKey1, "$", expectedUpsertedJson };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.SET",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanUpsertManyRecordsAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
SetupExecuteMock(this._redisDatabaseMock, "OK");
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
var model1 = CreateModel(TestRecordKey1, true);
|
||||
var model2 = CreateModel(TestRecordKey2, true);
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync([model1, model2]);
|
||||
|
||||
// Assert
|
||||
// TODO: Fix issue where NotAnnotated is being included in the JSON.
|
||||
var expectedArgs = new object[] { TestRecordKey1, "$", """{"data1_json_name":"data 1","Data2":"data 2","vector1_json_name":[1,2,3,4],"Vector2":[1,2,3,4],"NotAnnotated":null}""", TestRecordKey2, "$", """{"data1_json_name":"data 1","Data2":"data 2","vector1_json_name":[1,2,3,4],"Vector2":[1,2,3,4],"NotAnnotated":null}""" };
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"JSON.MSET",
|
||||
It.Is<object[]>(x => x.SequenceEqual(expectedArgs))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task CanSearchWithVectorAndFilterAsync(bool useDefinition)
|
||||
{
|
||||
// Arrange
|
||||
var jsonResult = """{ "data1_json_name": "data 1", "Data2": "data 2", "vector1_json_name": [1, 2, 3, 4], "Vector2": [1, 2, 3, 4] }""";
|
||||
SetupExecuteMock(this._redisDatabaseMock, new RedisResult[]
|
||||
{
|
||||
RedisResult.Create(new RedisValue("1")),
|
||||
RedisResult.Create(new RedisValue(TestRecordKey1)),
|
||||
RedisResult.Create(new RedisValue("0.8")),
|
||||
RedisResult.Create(
|
||||
[
|
||||
new RedisValue("$"),
|
||||
new RedisValue(jsonResult),
|
||||
new RedisValue("vector_score"),
|
||||
new RedisValue("0.25")
|
||||
]),
|
||||
});
|
||||
using var sut = this.CreateRecordCollection(useDefinition);
|
||||
|
||||
// Act.
|
||||
var results = await sut.SearchAsync(
|
||||
new ReadOnlyMemory<float>(new[] { 1f, 2f, 3f, 4f }),
|
||||
top: 5,
|
||||
new()
|
||||
{
|
||||
IncludeVectors = true,
|
||||
Filter = r => r.Data1 == "data 1",
|
||||
VectorProperty = r => r.Vector1,
|
||||
Skip = 2
|
||||
}).ToListAsync();
|
||||
|
||||
// Assert.
|
||||
var expectedArgs = new object[]
|
||||
{
|
||||
"testcollection",
|
||||
"@data1_json_name:{\"data 1\"}=>[KNN 7 @vector1_json_name $embedding AS vector_score]",
|
||||
"WITHSCORES",
|
||||
"SORTBY",
|
||||
"vector_score",
|
||||
"LIMIT",
|
||||
2,
|
||||
7,
|
||||
"PARAMS",
|
||||
2,
|
||||
"embedding",
|
||||
MemoryMarshal.AsBytes(new ReadOnlySpan<float>(new float[] { 1, 2, 3, 4 })).ToArray(),
|
||||
"DIALECT",
|
||||
2
|
||||
};
|
||||
this._redisDatabaseMock
|
||||
.Verify(
|
||||
x => x.ExecuteAsync(
|
||||
"FT.SEARCH",
|
||||
It.Is<object[]>(x => x.Where(y => !(y is byte[])).SequenceEqual(expectedArgs.Where(y => !(y is byte[]))))),
|
||||
Times.Once);
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Equal(TestRecordKey1, results.First().Record.Key);
|
||||
Assert.Equal(0.25d, results.First().Score);
|
||||
Assert.Equal("data 1", results.First().Record.Data1);
|
||||
Assert.Equal("data 2", results.First().Record.Data2);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, results.First().Record.Vector1!.Value.ToArray());
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, results.First().Record.Vector2!.Value.ToArray());
|
||||
}
|
||||
|
||||
private RedisJsonCollection<string, MultiPropsModel> CreateRecordCollection(bool useDefinition, bool useCustomJsonSerializerOptions = false)
|
||||
{
|
||||
return new RedisJsonCollection<string, MultiPropsModel>(
|
||||
this._redisDatabaseMock.Object,
|
||||
TestCollectionName,
|
||||
new()
|
||||
{
|
||||
PrefixCollectionNameToKeyNames = false,
|
||||
Definition = useDefinition ? this._multiPropsDefinition : null,
|
||||
JsonSerializerOptions = useCustomJsonSerializerOptions ? this._customJsonSerializerOptions : null
|
||||
});
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, Exception exception)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.ThrowsAsync(exception);
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, string command, Exception exception)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
command,
|
||||
It.IsAny<object[]>()))
|
||||
.ThrowsAsync(exception);
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, IEnumerable<string> redisResultStrings)
|
||||
{
|
||||
var results = redisResultStrings
|
||||
.Select(x => RedisResult.Create(new RedisValue(x)))
|
||||
.ToArray();
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.ReturnsAsync(RedisResult.Create(results));
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, IEnumerable<RedisResult> redisResultStrings)
|
||||
{
|
||||
var results = redisResultStrings
|
||||
.Select(x => x)
|
||||
.ToArray();
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.ReturnsAsync(RedisResult.Create(results));
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, string redisResultString)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.Callback((string command, object[] args) =>
|
||||
{
|
||||
Console.WriteLine(args);
|
||||
})
|
||||
.ReturnsAsync(RedisResult.Create(new RedisValue(redisResultString)));
|
||||
}
|
||||
|
||||
private static void SetupExecuteMock(Mock<IDatabase> redisDatabaseMock, string command, string redisResultString)
|
||||
{
|
||||
redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
command,
|
||||
It.IsAny<object[]>()))
|
||||
.Callback((string command, object[] args) =>
|
||||
{
|
||||
Console.WriteLine(args);
|
||||
})
|
||||
.ReturnsAsync(RedisResult.Create(new RedisValue(redisResultString)));
|
||||
}
|
||||
|
||||
private static MultiPropsModel CreateModel(string key, bool withVectors)
|
||||
{
|
||||
return new MultiPropsModel
|
||||
{
|
||||
Key = key,
|
||||
Data1 = "data 1",
|
||||
Data2 = "data 2",
|
||||
Vector1 = withVectors ? new float[] { 1, 2, 3, 4 } : null,
|
||||
Vector2 = withVectors ? new float[] { 1, 2, 3, 4 } : null,
|
||||
NotAnnotated = null,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly JsonSerializerOptions _customJsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
private readonly VectorStoreCollectionDefinition _multiPropsDefinition = new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("Data1", typeof(string)) { IsIndexed = true, StorageName = "ignored_data1_storage_name" },
|
||||
new VectorStoreDataProperty("Data2", typeof(string)) { IsIndexed = true },
|
||||
new VectorStoreVectorProperty("Vector1", typeof(ReadOnlyMemory<float>), 4) { DistanceFunction = DistanceFunction.CosineDistance, StorageName = "ignored_vector1_storage_name" },
|
||||
new VectorStoreVectorProperty("Vector2", typeof(ReadOnlyMemory<float>), 4)
|
||||
]
|
||||
};
|
||||
|
||||
public sealed class MultiPropsModel
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("data1_json_name")]
|
||||
[VectorStoreData(IsIndexed = true, StorageName = "ignored_data1_storage_name")]
|
||||
public string Data1 { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreData(IsIndexed = true)]
|
||||
public string Data2 { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vector1_json_name")]
|
||||
[VectorStoreVector(4, DistanceFunction = DistanceFunction.CosineDistance, StorageName = "ignored_vector1_storage_name")]
|
||||
public ReadOnlyMemory<float>? Vector1 { get; set; }
|
||||
|
||||
[VectorStoreVector(4)]
|
||||
public ReadOnlyMemory<float>? Vector2 { get; set; }
|
||||
|
||||
public string? NotAnnotated { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisJsonDynamicMapper"/> class.
|
||||
/// </summary>
|
||||
public class RedisJsonDynamicMapperTests
|
||||
{
|
||||
private static readonly float[] s_floatVector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f };
|
||||
|
||||
private static readonly CollectionModel s_model
|
||||
= new RedisJsonModelBuilder(RedisJsonCollection<object, Dictionary<string, object?>>.ModelBuildingOptions)
|
||||
.BuildDynamic(
|
||||
new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringData", typeof(string)),
|
||||
new VectorStoreDataProperty("IntData", typeof(int)),
|
||||
new VectorStoreDataProperty("NullableIntData", typeof(int?)),
|
||||
new VectorStoreDataProperty("ComplexObjectData", typeof(ComplexObject)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
|
||||
]
|
||||
},
|
||||
defaultEmbeddingGenerator: null);
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsAllSupportedTypes()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisJsonDynamicMapper(s_model, JsonSerializerOptions.Default);
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = "key",
|
||||
["StringData"] = "data 1",
|
||||
["IntData"] = 1,
|
||||
["NullableIntData"] = 2,
|
||||
["ComplexObjectData"] = new ComplexObject { Prop1 = "prop 1", Prop2 = "prop 2" },
|
||||
["FloatVector"] = new ReadOnlyMemory<float>(s_floatVector)
|
||||
};
|
||||
|
||||
// Act.
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", storageModel.Key);
|
||||
Assert.Equal("data 1", (string)storageModel.Node["StringData"]!);
|
||||
Assert.Equal(1, (int)storageModel.Node["IntData"]!);
|
||||
Assert.Equal(2, (int?)storageModel.Node["NullableIntData"]!);
|
||||
Assert.Equal("prop 1", (string)storageModel.Node["ComplexObjectData"]!.AsObject()["Prop1"]!);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, storageModel.Node["FloatVector"]?.AsArray().GetValues<float>().ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsNullValues()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisJsonDynamicMapper(s_model, JsonSerializerOptions.Default);
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = "key",
|
||||
["StringData"] = null,
|
||||
["IntData"] = null,
|
||||
["NullableIntData"] = null,
|
||||
["ComplexObjectData"] = null,
|
||||
["FloatVector"] = null,
|
||||
};
|
||||
|
||||
// Act.
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", storageModel.Key);
|
||||
Assert.Null(storageModel.Node["storage_string_data"]);
|
||||
Assert.Null(storageModel.Node["IntData"]);
|
||||
Assert.Null(storageModel.Node["NullableIntData"]);
|
||||
Assert.Null(storageModel.Node["ComplexObjectData"]);
|
||||
Assert.Null(storageModel.Node["FloatVector"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelMapsAllSupportedTypes()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisJsonDynamicMapper(s_model, JsonSerializerOptions.Default);
|
||||
var storageModel = new JsonObject
|
||||
{
|
||||
{ "StringData", "data 1" },
|
||||
{ "IntData", 1 },
|
||||
{ "NullableIntData", 2 },
|
||||
{ "ComplexObjectData", new JsonObject(new KeyValuePair<string, JsonNode?>[] { new("Prop1", JsonValue.Create("prop 1")), new("Prop2", JsonValue.Create("prop 2")) }) },
|
||||
{ "FloatVector", new JsonArray(new float[] { 1, 2, 3, 4 }.Select(x => JsonValue.Create(x)).ToArray()) }
|
||||
};
|
||||
|
||||
// Act.
|
||||
var dataModel = sut.MapFromStorageToDataModel(("key", storageModel), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.Equal("data 1", dataModel["StringData"]);
|
||||
Assert.Equal(1, dataModel["IntData"]);
|
||||
Assert.Equal(2, dataModel["NullableIntData"]);
|
||||
Assert.Equal("prop 1", ((ComplexObject)dataModel["ComplexObjectData"]!).Prop1);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelMapsNullValues()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisJsonDynamicMapper(s_model, JsonSerializerOptions.Default);
|
||||
var storageModel = new JsonObject
|
||||
{
|
||||
{ "StringData", null },
|
||||
{ "IntData", null },
|
||||
{ "NullableIntData", null },
|
||||
{ "ComplexObjectData", null },
|
||||
{ "FloatVector", null }
|
||||
};
|
||||
|
||||
// Act.
|
||||
var dataModel = sut.MapFromStorageToDataModel(("key", storageModel), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.Null(dataModel["StringData"]);
|
||||
Assert.Null(dataModel["IntData"]);
|
||||
Assert.Null(dataModel["NullableIntData"]);
|
||||
Assert.Null(dataModel["ComplexObjectData"]);
|
||||
Assert.Null(dataModel["FloatVector"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelSkipsMissingProperties()
|
||||
{
|
||||
// Arrange.
|
||||
var sut = new RedisJsonDynamicMapper(s_model, JsonSerializerOptions.Default);
|
||||
var dataModel = new Dictionary<string, object?> { ["Key"] = "key" };
|
||||
|
||||
// Act.
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", storageModel.Key);
|
||||
Assert.Empty(storageModel.Node.AsObject());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelSkipsMissingProperties()
|
||||
{
|
||||
// Arrange.
|
||||
var storageModel = new JsonObject();
|
||||
|
||||
var sut = new RedisJsonDynamicMapper(s_model, JsonSerializerOptions.Default);
|
||||
|
||||
// Act.
|
||||
var dataModel = sut.MapFromStorageToDataModel(("key", storageModel), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.Single(dataModel);
|
||||
}
|
||||
|
||||
private sealed class ComplexObject
|
||||
{
|
||||
public string Prop1 { get; set; } = string.Empty;
|
||||
|
||||
public string Prop2 { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisJsonMapper{TConsumerDataModel}"/> class.
|
||||
/// </summary>
|
||||
public sealed class RedisJsonMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void MapsAllFieldsFromDataToStorageModel()
|
||||
{
|
||||
// Arrange.
|
||||
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
|
||||
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, JsonSerializerOptions.Default);
|
||||
var sut = new RedisJsonMapper<MultiPropsModel>(model, JsonSerializerOptions.Default);
|
||||
|
||||
// Act.
|
||||
var actual = sut.MapFromDataToStorageModel(CreateModel("test key"), recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual.Node);
|
||||
Assert.Equal("test key", actual.Key);
|
||||
var jsonObject = actual.Node.AsObject();
|
||||
Assert.Equal("data 1", jsonObject?["Data1"]?.ToString());
|
||||
Assert.Equal("data 2", jsonObject?["Data2"]?.ToString());
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, jsonObject?["Vector1"]?.AsArray().GetValues<float>().ToArray());
|
||||
Assert.Equal(new float[] { 5, 6, 7, 8 }, jsonObject?["Vector2"]?.AsArray().GetValues<float>().ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapsAllFieldsFromDataToStorageModelWithCustomSerializerOptions()
|
||||
{
|
||||
// Arrange.
|
||||
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
|
||||
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, jsonSerializerOptions);
|
||||
var sut = new RedisJsonMapper<MultiPropsModel>(model, jsonSerializerOptions);
|
||||
|
||||
// Act.
|
||||
var actual = sut.MapFromDataToStorageModel(CreateModel("test key"), recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual.Node);
|
||||
Assert.Equal("test key", actual.Key);
|
||||
var jsonObject = actual.Node.AsObject();
|
||||
Assert.Equal("data 1", jsonObject?["data1"]?.ToString());
|
||||
Assert.Equal("data 2", jsonObject?["data2"]?.ToString());
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, jsonObject?["vector1"]?.AsArray().GetValues<float>().ToArray());
|
||||
Assert.Equal(new float[] { 5, 6, 7, 8 }, jsonObject?["vector2"]?.AsArray().GetValues<float>().ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapsAllFieldsFromStorageToDataModel()
|
||||
{
|
||||
// Arrange.
|
||||
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
|
||||
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, JsonSerializerOptions.Default);
|
||||
var sut = new RedisJsonMapper<MultiPropsModel>(model, JsonSerializerOptions.Default);
|
||||
|
||||
// Act.
|
||||
var jsonObject = new JsonObject
|
||||
{
|
||||
{ "Data1", "data 1" },
|
||||
{ "Data2", "data 2" },
|
||||
{ "Vector1", new JsonArray(new[] { 1, 2, 3, 4 }.Select(x => JsonValue.Create(x)).ToArray()) },
|
||||
{ "Vector2", new JsonArray(new[] { 5, 6, 7, 8 }.Select(x => JsonValue.Create(x)).ToArray()) }
|
||||
};
|
||||
var actual = sut.MapFromStorageToDataModel(("test key", jsonObject), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("test key", actual.Key);
|
||||
Assert.Equal("data 1", actual.Data1);
|
||||
Assert.Equal("data 2", actual.Data2);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.Vector1!.Value.ToArray());
|
||||
Assert.Equal(new float[] { 5, 6, 7, 8 }, actual.Vector2!.Value.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapsAllFieldsFromStorageToDataModelWithCustomSerializerOptions()
|
||||
{
|
||||
// Arrange.
|
||||
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
var model = new RedisJsonModelBuilder(RedisJsonCollection<string, MultiPropsModel>.ModelBuildingOptions)
|
||||
.Build(typeof(MultiPropsModel), typeof(string), definition: null, defaultEmbeddingGenerator: null, jsonSerializerOptions);
|
||||
var sut = new RedisJsonMapper<MultiPropsModel>(model, jsonSerializerOptions);
|
||||
|
||||
// Act.
|
||||
var jsonObject = new JsonObject
|
||||
{
|
||||
{ "data1", "data 1" },
|
||||
{ "data2", "data 2" },
|
||||
{ "vector1", new JsonArray(new[] { 1, 2, 3, 4 }.Select(x => JsonValue.Create(x)).ToArray()) },
|
||||
{ "vector2", new JsonArray(new[] { 5, 6, 7, 8 }.Select(x => JsonValue.Create(x)).ToArray()) }
|
||||
};
|
||||
var actual = sut.MapFromStorageToDataModel(("test key", jsonObject), includeVectors: true);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("test key", actual.Key);
|
||||
Assert.Equal("data 1", actual.Data1);
|
||||
Assert.Equal("data 2", actual.Data2);
|
||||
Assert.Equal(new float[] { 1, 2, 3, 4 }, actual.Vector1!.Value.ToArray());
|
||||
Assert.Equal(new float[] { 5, 6, 7, 8 }, actual.Vector2!.Value.ToArray());
|
||||
}
|
||||
|
||||
private static MultiPropsModel CreateModel(string key)
|
||||
{
|
||||
return new MultiPropsModel
|
||||
{
|
||||
Key = key,
|
||||
Data1 = "data 1",
|
||||
Data2 = "data 2",
|
||||
Vector1 = new float[] { 1, 2, 3, 4 },
|
||||
Vector2 = new float[] { 5, 6, 7, 8 },
|
||||
NotAnnotated = "notAnnotated",
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class MultiPropsModel
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreData]
|
||||
public string Data1 { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreData]
|
||||
public string Data2 { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreVector(10)]
|
||||
public ReadOnlyMemory<float>? Vector1 { get; set; }
|
||||
|
||||
[VectorStoreVector(10)]
|
||||
public ReadOnlyMemory<float>? Vector2 { get; set; }
|
||||
|
||||
public string NotAnnotated { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="RedisVectorStore"/> class.
|
||||
/// </summary>
|
||||
public class RedisVectorStoreTests
|
||||
{
|
||||
private const string TestCollectionName = "testcollection";
|
||||
|
||||
private readonly Mock<IDatabase> _redisDatabaseMock;
|
||||
|
||||
public RedisVectorStoreTests()
|
||||
{
|
||||
this._redisDatabaseMock = new Mock<IDatabase>(MockBehavior.Strict);
|
||||
this._redisDatabaseMock.Setup(l => l.Database).Returns(0);
|
||||
|
||||
var batchMock = new Mock<IBatch>();
|
||||
this._redisDatabaseMock.Setup(x => x.CreateBatch(It.IsAny<object>())).Returns(batchMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCollectionReturnsJsonCollection()
|
||||
{
|
||||
// Arrange.
|
||||
using var sut = new RedisVectorStore(this._redisDatabaseMock.Object);
|
||||
|
||||
// Act.
|
||||
var actual = sut.GetCollection<string, SinglePropsModel<string>>(TestCollectionName);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual);
|
||||
Assert.IsType<RedisJsonCollection<string, SinglePropsModel<string>>>(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCollectionReturnsHashSetCollection()
|
||||
{
|
||||
// Arrange.
|
||||
using var sut = new RedisVectorStore(this._redisDatabaseMock.Object, new() { StorageType = RedisStorageType.HashSet });
|
||||
|
||||
// Act.
|
||||
var actual = sut.GetCollection<string, SinglePropsModel<string>>(TestCollectionName);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(actual);
|
||||
Assert.IsType<RedisHashSetCollection<string, SinglePropsModel<string>>>(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCollectionThrowsForInvalidKeyType()
|
||||
{
|
||||
// Arrange.
|
||||
using var sut = new RedisVectorStore(this._redisDatabaseMock.Object);
|
||||
|
||||
// Act & Assert.
|
||||
Assert.Throws<NotSupportedException>(() => sut.GetCollection<int, SinglePropsModel<int>>(TestCollectionName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCollectionNamesCallsSDKAsync()
|
||||
{
|
||||
// Arrange.
|
||||
var redisResultStrings = new string[] { "collection1", "collection2" };
|
||||
var results = redisResultStrings
|
||||
.Select(x => RedisResult.Create(new RedisValue(x)))
|
||||
.ToArray();
|
||||
this._redisDatabaseMock
|
||||
.Setup(
|
||||
x => x.ExecuteAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object[]>()))
|
||||
.ReturnsAsync(RedisResult.Create(results));
|
||||
using var sut = new RedisVectorStore(this._redisDatabaseMock.Object);
|
||||
|
||||
// Act.
|
||||
var collectionNames = sut.ListCollectionNamesAsync();
|
||||
|
||||
// Assert.
|
||||
var collectionNamesList = await collectionNames.ToListAsync();
|
||||
Assert.Equal(new[] { "collection1", "collection2" }, collectionNamesList);
|
||||
}
|
||||
|
||||
public sealed class SinglePropsModel<TKey>
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public required TKey Key { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string Data { get; set; } = string.Empty;
|
||||
|
||||
[VectorStoreVector(4)]
|
||||
public ReadOnlyMemory<float>? Vector { get; set; }
|
||||
|
||||
public string? NotAnnotated { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user