// Copyright (c) Microsoft. All rights reserved. using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.VectorData; using VectorData.ConformanceTests.Support; using VectorData.ConformanceTests.Xunit; using Xunit; namespace VectorData.ConformanceTests; #pragma warning disable CA2000 // Don't actually need to dispose FakeEmbeddingGenerator #pragma warning disable CS8605 // Unboxing a possibly null value. public abstract class EmbeddingGenerationTests(EmbeddingGenerationTests.StringVectorFixture stringVectorFixture, EmbeddingGenerationTests.RomOfFloatVectorFixture romOfFloatVectorFixture) where TKey : notnull { #region Search [ConditionalFact] public virtual async Task SearchAsync_with_property_generator() { // Property level: embedding generators are defined at all levels. The property generator should take precedence. var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); var result = await collection.SearchAsync("[1, 1, 0]", top: 1).SingleAsync(); Assert.Equal("Property ([1, 1, 3])", result.Record.Text); } [ConditionalFact] public virtual async Task SearchAsync_with_property_generator_dynamic() { // Property level: embedding generators are defined at all levels. The property generator should take precedence. var collection = this.GetDynamicCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); var result = await collection.SearchAsync("[1, 1, 0]", top: 1).SingleAsync(); Assert.Equal("Property ([1, 1, 3])", result.Record[nameof(Record.Text)]); } [ConditionalFact] public virtual async Task SearchAsync_with_collection_generator() { // Collection level: embedding generators are defined at the collection and store level - the collection generator should take precedence. var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: false); var result = await collection.SearchAsync("[1, 1, 0]", top: 1).SingleAsync(); Assert.Equal("Collection ([1, 1, 2])", result.Record.Text); } [ConditionalFact] public virtual async Task SearchAsync_with_store_generator() { // Store level: an embedding generator is defined at the store level only. var collection = this.GetCollection(storeGenerator: true, collectionGenerator: false, propertyGenerator: false); var result = await collection.SearchAsync("[1, 1, 0]", top: 1).SingleAsync(); Assert.Equal("Store ([1, 1, 1])", result.Record.Text); } [ConditionalFact] public virtual async Task SearchAsync_with_store_dependency_injection() { foreach (var registrationDelegate in stringVectorFixture.DependencyInjectionStoreRegistrationDelegates) { IServiceCollection serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(new FakeEmbeddingGenerator(replaceLast: 1)); registrationDelegate(serviceCollection); await using var serviceProvider = serviceCollection.BuildServiceProvider(); var vectorStore = serviceProvider.GetRequiredService(); var collection = vectorStore.GetCollection(stringVectorFixture.CollectionName, stringVectorFixture.CreateRecordDefinition()); var result = await collection.SearchAsync("[1, 1, 0]", top: 1).SingleAsync(); Assert.Equal("Store ([1, 1, 1])", result.Record.Text); } } [ConditionalFact] public virtual async Task SearchAsync_with_collection_dependency_injection() { foreach (var registrationDelegate in stringVectorFixture.DependencyInjectionCollectionRegistrationDelegates) { IServiceCollection serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(new FakeEmbeddingGenerator(replaceLast: 1)); registrationDelegate(serviceCollection); await using var serviceProvider = serviceCollection.BuildServiceProvider(); var collection = serviceProvider.GetRequiredService>(); var result = await collection.SearchAsync("[1, 1, 0]", top: 1).SingleAsync(); Assert.Equal("Store ([1, 1, 1])", result.Record.Text); } } [ConditionalFact] public virtual async Task SearchAsync_with_custom_input_type() { var recordDefinition = new VectorStoreCollectionDefinition() { Properties = stringVectorFixture.CreateRecordDefinition().Properties .Select(p => p is VectorStoreVectorProperty vectorProperty ? new VectorStoreVectorProperty(nameof(Record.Embedding), dimensions: 3) { DistanceFunction = stringVectorFixture.DefaultDistanceFunction, IndexKind = stringVectorFixture.DefaultIndexKind } : p) .ToList() }; var collection = stringVectorFixture.GetCollection( stringVectorFixture.CreateVectorStore(new FakeCustomerEmbeddingGenerator([1, 1, 1])), stringVectorFixture.CollectionName, recordDefinition); var result = await collection.SearchAsync(new Customer(), top: 1).SingleAsync(); Assert.Equal("Store ([1, 1, 1])", result.Record.Text); } [ConditionalFact] public virtual async Task SearchAsync_with_search_only_embedding_generation() { var collection = romOfFloatVectorFixture.GetCollection( romOfFloatVectorFixture.CreateVectorStore(new FakeEmbeddingGenerator()), romOfFloatVectorFixture.CollectionName, romOfFloatVectorFixture.CreateRecordDefinition()); var result = await collection.SearchAsync("[1, 1, 1]", top: 1).SingleAsync(); Assert.Equal("Store ([1, 1, 1])", result.Record.Text); } [ConditionalFact] public virtual async Task SearchAsync_string_without_generator_throws() { // The database doesn't support embedding generation, and no client-side generator has been configured at any level, // so SearchAsync should throw for string. var collection = stringVectorFixture.GetCollection(stringVectorFixture.TestStore.DefaultVectorStore, stringVectorFixture.CollectionName + "withoutgenerator"); var exception = await Assert.ThrowsAsync(() => collection.SearchAsync("foo", top: 1).ToListAsync().AsTask()); Assert.StartsWith( "A value of type 'string' was passed to 'SearchAsync', but that isn't a supported vector type by your provider and no embedding generator was configured. The supported vector types are:", exception.Message); } public class RawRecord { [VectorStoreKey] public TKey Key { get; set; } = default!; [VectorStoreVector(Dimensions: 3)] public ReadOnlyMemory Embedding { get; set; } } [ConditionalFact] public virtual async Task SearchAsync_with_incompatible_generator_throws() { var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); // We have a generator configured for string, not int. var exception = await Assert.ThrowsAsync(() => collection.SearchAsync(8, top: 1).ToListAsync().AsTask()); Assert.Equal($"An input of type 'int' was provided, but an incompatible embedding generator of type '{nameof(FakeEmbeddingGenerator)}' was configured.", exception.Message); } #endregion Search #region Upsert [ConditionalFact] public virtual async Task UpsertAsync() { var counter = stringVectorFixture.GenerateNextCounter(); var record = new Record { Key = stringVectorFixture.GenerateNextKey(), Embedding = "[100, 1, 0]", Counter = counter, Text = nameof(UpsertAsync) }; // Property level: embedding generators are defined at all levels. The property generator should take precedence. var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); await collection.UpsertAsync(record).ConfigureAwait(false); await stringVectorFixture.TestStore.WaitForDataAsync(collection, 1, filter: r => r.Counter == counter); var result = await collection.SearchAsync(new ReadOnlyMemory([100, 1, 3]), top: 1).SingleAsync(); Assert.Equal(counter, result.Record.Counter); } [ConditionalFact] public virtual async Task UpsertAsync_dynamic() { var counter = stringVectorFixture.GenerateNextCounter(); var record = new Dictionary { [nameof(Record.Key)] = stringVectorFixture.GenerateNextKey(), [nameof(Record.Embedding)] = "[200, 1, 0]", [nameof(Record.Counter)] = counter, [nameof(Record.Text)] = nameof(UpsertAsync_dynamic) }; // Property level: embedding generators are defined at all levels. The property generator should take precedence. var collection = this.GetDynamicCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); await collection.UpsertAsync(record).ConfigureAwait(false); await stringVectorFixture.TestStore.WaitForDataAsync(collection, 1, filter: r => (int)r[nameof(Record.Counter)] == counter); var result = await collection.SearchAsync(new ReadOnlyMemory([200, 1, 3]), top: 1).SingleAsync(); Assert.Equal(counter, result.Record[nameof(Record.Counter)]); } [ConditionalFact] public virtual async Task UpsertAsync_batch() { var (counter1, counter2) = (stringVectorFixture.GenerateNextCounter(), stringVectorFixture.GenerateNextCounter()); Record[] records = [ new() { Key = stringVectorFixture.GenerateNextKey(), Embedding = "[300, 1, 0]", Counter = counter1, Text = nameof(UpsertAsync_batch) + "1" }, new() { Key = stringVectorFixture.GenerateNextKey(), Embedding = "[400, 1, 0]", Counter = counter2, Text = nameof(UpsertAsync_batch) + "2" } ]; var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); await collection.UpsertAsync(records).ConfigureAwait(false); await stringVectorFixture.TestStore.WaitForDataAsync(collection, 2, filter: r => (int)r.Counter == counter1 || (int)r.Counter == counter2); var result = await collection.SearchAsync(new ReadOnlyMemory([300, 1, 3]), top: 1).SingleAsync(); Assert.Equal(counter1, result.Record.Counter); result = await collection.SearchAsync(new ReadOnlyMemory([400, 1, 3]), top: 1).SingleAsync(); Assert.Equal(counter2, result.Record.Counter); } [ConditionalFact] public virtual async Task UpsertAsync_batch_dynamic() { var (counter1, counter2) = (stringVectorFixture.GenerateNextCounter(), stringVectorFixture.GenerateNextCounter()); Dictionary[] records = [ new() { [nameof(Record.Key)] = stringVectorFixture.GenerateNextKey(), [nameof(Record.Embedding)] = "[500, 1, 0]", [nameof(Record.Counter)] = counter1, [nameof(Record.Text)] = nameof(UpsertAsync_batch_dynamic) + "1" }, new() { [nameof(Record.Key)] = stringVectorFixture.GenerateNextKey(), [nameof(Record.Embedding)] = "[600, 1, 0]", [nameof(Record.Counter)] = counter2, [nameof(Record.Text)] = nameof(UpsertAsync_batch_dynamic) + "2" } ]; var collection = this.GetDynamicCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); await collection.UpsertAsync(records).ConfigureAwait(false); await stringVectorFixture.TestStore.WaitForDataAsync(collection, 2, filter: r => (int)r[nameof(Record.Counter)] == counter1 || (int)r[nameof(Record.Counter)] == counter2); var result = await collection.SearchAsync(new ReadOnlyMemory([500, 1, 3]), top: 1).SingleAsync(); Assert.Equal(counter1, result.Record[nameof(Record.Counter)]); result = await collection.SearchAsync(new ReadOnlyMemory([600, 1, 3]), top: 1).SingleAsync(); Assert.Equal(counter2, result.Record[nameof(Record.Counter)]); } #endregion Upsert #region IncludeVectors [ConditionalFact] public virtual async Task SearchAsync_with_IncludeVectors_throws() { var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); var exception = await Assert.ThrowsAsync(() => collection.SearchAsync("[1, 0, 0]", top: 1, new() { IncludeVectors = true }).ToListAsync().AsTask()); Assert.Equal("When an embedding generator is configured, `Include Vectors` cannot be enabled.", exception.Message); } [ConditionalFact] public virtual async Task GetAsync_with_IncludeVectors_throws() { var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); var exception = await Assert.ThrowsAsync(() => collection.GetAsync(stringVectorFixture.TestData[0].Key, new() { IncludeVectors = true })); Assert.Equal("When an embedding generator is configured, `Include Vectors` cannot be enabled.", exception.Message); } [ConditionalFact] public virtual async Task GetAsync_enumerable_with_IncludeVectors_throws() { var collection = this.GetCollection(storeGenerator: true, collectionGenerator: true, propertyGenerator: true); var exception = await Assert.ThrowsAsync(() => collection.GetAsync( [stringVectorFixture.TestData[0].Key, stringVectorFixture.TestData[1].Key], new() { IncludeVectors = true }) .ToListAsync().AsTask()); Assert.Equal("When an embedding generator is configured, `Include Vectors` cannot be enabled.", exception.Message); } #endregion IncludeVectors #region Support public class Record : TestRecord { public string? Embedding { get; set; } public int Counter { get; set; } public string? Text { get; set; } } public class RecordWithAttributes { [VectorStoreKey] public TKey Key { get; set; } = default!; [VectorStoreVector(Dimensions: 3)] public string? Embedding { get; set; } [VectorStoreData(IsIndexed = true)] public int Counter { get; set; } [VectorStoreData] public string? Text { get; set; } } public class RecordWithCustomerVectorProperty { public TKey Key { get; set; } = default!; public Customer? Embedding { get; set; } public int Counter { get; set; } public string? Text { get; set; } } public class RecordWithRomOfFloatVectorProperty : TestRecord { public ReadOnlyMemory Embedding { get; set; } public int Counter { get; set; } public string? Text { get; set; } } public class Customer { public string? FirstName { get; set; } public string? LastName { get; set; } } private VectorStoreCollection GetCollection( bool storeGenerator = false, bool collectionGenerator = false, bool propertyGenerator = false) where TRecord : class { var (vectorStore, recordDefinition) = this.GetStoreAndRecordDefinition(storeGenerator, collectionGenerator, propertyGenerator); return stringVectorFixture.GetCollection(vectorStore, stringVectorFixture.CollectionName, recordDefinition); } private VectorStoreCollection> GetDynamicCollection( bool storeGenerator = false, bool collectionGenerator = false, bool propertyGenerator = false) { var (vectorStore, recordDefinition) = this.GetStoreAndRecordDefinition(storeGenerator, collectionGenerator, propertyGenerator); return stringVectorFixture.GetDynamicCollection(vectorStore, stringVectorFixture.CollectionName, recordDefinition); } private (VectorStore, VectorStoreCollectionDefinition) GetStoreAndRecordDefinition( bool storeGenerator = false, bool collectionGenerator = false, bool propertyGenerator = false) { var recordDefinition = stringVectorFixture.CreateRecordDefinition(); if (propertyGenerator) { foreach (var vectorProperty in recordDefinition.Properties.OfType()) { vectorProperty.EmbeddingGenerator = new FakeEmbeddingGenerator(replaceLast: 3); } } if (collectionGenerator) { recordDefinition.EmbeddingGenerator = new FakeEmbeddingGenerator(replaceLast: 2); } var vectorStore = stringVectorFixture.CreateVectorStore(storeGenerator ? new FakeEmbeddingGenerator(replaceLast: 1) : null); return (vectorStore, recordDefinition); } public abstract class StringVectorFixture : VectorStoreCollectionFixture { private int _counter; protected override string CollectionNameBase => nameof(EmbeddingGenerationTests); public override VectorStoreCollectionDefinition CreateRecordDefinition() => new() { Properties = [ new VectorStoreKeyProperty(nameof(Record.Key), typeof(TKey)), new VectorStoreVectorProperty(nameof(Record.Embedding), typeof(string), dimensions: 3) { DistanceFunction = this.DefaultDistanceFunction, IndexKind = this.DefaultIndexKind }, new VectorStoreDataProperty(nameof(Record.Counter), typeof(int)) { IsIndexed = true }, new VectorStoreDataProperty(nameof(Record.Text), typeof(string)) ], EmbeddingGenerator = new FakeEmbeddingGenerator() }; protected override List BuildTestData() => [ new() { Key = this.GenerateNextKey(), Embedding = "[1, 1, 1]", Counter = this.GenerateNextCounter(), Text = "Store ([1, 1, 1])" }, new() { Key = this.GenerateNextKey(), Embedding = "[1, 1, 2]", Counter = this.GenerateNextCounter(), Text = "Collection ([1, 1, 2])" }, new() { Key = this.GenerateNextKey(), Embedding = "[1, 1, 3]", Counter = this.GenerateNextCounter(), Text = "Property ([1, 1, 3])" } ]; public virtual VectorStoreCollection GetCollection( VectorStore vectorStore, string collectionName, VectorStoreCollectionDefinition? recordDefinition = null) where TRecord : class => vectorStore.GetCollection(collectionName, recordDefinition); public virtual VectorStoreCollection> GetDynamicCollection( VectorStore vectorStore, string collectionName, VectorStoreCollectionDefinition recordDefinition) => vectorStore.GetDynamicCollection(collectionName, recordDefinition); public abstract VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator = null); public abstract Func[] DependencyInjectionStoreRegistrationDelegates { get; } public abstract Func[] DependencyInjectionCollectionRegistrationDelegates { get; } public virtual int GenerateNextCounter() => Interlocked.Increment(ref this._counter); } public abstract class RomOfFloatVectorFixture : VectorStoreCollectionFixture { private int _counter; protected override string CollectionNameBase => "SearchOnlyEmbeddingGenerationTests"; public override VectorStoreCollectionDefinition CreateRecordDefinition() => new() { Properties = [ new VectorStoreKeyProperty(nameof(RecordWithRomOfFloatVectorProperty.Key), typeof(TKey)), new VectorStoreVectorProperty(nameof(RecordWithRomOfFloatVectorProperty.Embedding), typeof(ReadOnlyMemory), dimensions: 3) { DistanceFunction = this.DefaultDistanceFunction, IndexKind = this.DefaultIndexKind }, new VectorStoreDataProperty(nameof(RecordWithRomOfFloatVectorProperty.Counter), typeof(int)) { IsIndexed = true }, new VectorStoreDataProperty(nameof(RecordWithRomOfFloatVectorProperty.Text), typeof(string)) ], EmbeddingGenerator = new FakeEmbeddingGenerator() }; protected override List BuildTestData() => [ new() { Key = this.GenerateNextKey(), Embedding = new ReadOnlyMemory([1, 1, 1]), Counter = this.GenerateNextCounter(), Text = "Store ([1, 1, 1])", }, new() { Key = this.GenerateNextKey(), Embedding = new ReadOnlyMemory([1, 1, 2]), Counter = this.GenerateNextCounter(), Text = "Collection ([1, 1, 2])" }, new() { Key = this.GenerateNextKey(), Embedding = new ReadOnlyMemory([1, 1, 3]), Counter = this.GenerateNextCounter(), Text = "Property ([1, 1, 3])" } ]; public virtual VectorStoreCollection GetCollection( VectorStore vectorStore, string collectionName, VectorStoreCollectionDefinition? recordDefinition = null) where TRecord : class => vectorStore.GetCollection(collectionName, recordDefinition); public virtual VectorStoreCollection> GetDynamicCollection( VectorStore vectorStore, string collectionName, VectorStoreCollectionDefinition recordDefinition) => vectorStore.GetDynamicCollection(collectionName, recordDefinition); public abstract VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator = null); public abstract Func[] DependencyInjectionStoreRegistrationDelegates { get; } public abstract Func[] DependencyInjectionCollectionRegistrationDelegates { get; } public virtual int GenerateNextCounter() => Interlocked.Increment(ref this._counter); } protected sealed class FakeEmbeddingGenerator(int? replaceLast = null) : IEmbeddingGenerator> { public Task>> GenerateAsync( IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) { var results = new GeneratedEmbeddings>(); foreach (var value in values) { var vector = value.TrimStart('[').TrimEnd(']').Split(',').Select(s => float.Parse(s.Trim())).ToArray(); if (replaceLast is not null) { vector[vector.Length - 1] = replaceLast.Value; } results.Add(new Embedding(vector)); } return Task.FromResult(results); } public object? GetService(Type serviceType, object? serviceKey = null) => null; public void Dispose() { } } private sealed class FakeCustomerEmbeddingGenerator(float[] embedding) : IEmbeddingGenerator> { public Task>> GenerateAsync(IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) => Task.FromResult(new GeneratedEmbeddings> { new(embedding) }); public object? GetService(Type serviceType, object? serviceKey = null) => null; public void Dispose() { } } #endregion Support }