Files
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

624 lines
25 KiB
C#

// 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<TKey>(EmbeddingGenerationTests<TKey>.StringVectorFixture stringVectorFixture, EmbeddingGenerationTests<TKey>.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<Record>(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<Record>(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<Record>(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<IEmbeddingGenerator>(new FakeEmbeddingGenerator(replaceLast: 1));
registrationDelegate(serviceCollection);
await using var serviceProvider = serviceCollection.BuildServiceProvider();
var vectorStore = serviceProvider.GetRequiredService<VectorStore>();
var collection = vectorStore.GetCollection<TKey, Record>(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<IEmbeddingGenerator>(new FakeEmbeddingGenerator(replaceLast: 1));
registrationDelegate(serviceCollection);
await using var serviceProvider = serviceCollection.BuildServiceProvider();
var collection = serviceProvider.GetRequiredService<VectorStoreCollection<TKey, RecordWithAttributes>>();
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<Customer>(nameof(Record.Embedding), dimensions: 3)
{
DistanceFunction = stringVectorFixture.DefaultDistanceFunction,
IndexKind = stringVectorFixture.DefaultIndexKind
}
: p)
.ToList()
};
var collection = stringVectorFixture.GetCollection<RecordWithCustomerVectorProperty>(
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<RecordWithRomOfFloatVectorProperty>(
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<RawRecord>(stringVectorFixture.TestStore.DefaultVectorStore, stringVectorFixture.CollectionName + "withoutgenerator");
var exception = await Assert.ThrowsAsync<NotSupportedException>(() => 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<float> Embedding { get; set; }
}
[ConditionalFact]
public virtual async Task SearchAsync_with_incompatible_generator_throws()
{
var collection = this.GetCollection<Record>(storeGenerator: true, collectionGenerator: true, propertyGenerator: true);
// We have a generator configured for string, not int.
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => 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<TKey>(),
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<Record>(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<float>([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<string, object?>
{
[nameof(Record.Key)] = stringVectorFixture.GenerateNextKey<TKey>(),
[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<float>([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<TKey>(),
Embedding = "[300, 1, 0]",
Counter = counter1,
Text = nameof(UpsertAsync_batch) + "1"
},
new()
{
Key = stringVectorFixture.GenerateNextKey<TKey>(),
Embedding = "[400, 1, 0]",
Counter = counter2,
Text = nameof(UpsertAsync_batch) + "2"
}
];
var collection = this.GetCollection<Record>(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<float>([300, 1, 3]), top: 1).SingleAsync();
Assert.Equal(counter1, result.Record.Counter);
result = await collection.SearchAsync(new ReadOnlyMemory<float>([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<string, object?>[] records =
[
new()
{
[nameof(Record.Key)] = stringVectorFixture.GenerateNextKey<TKey>(),
[nameof(Record.Embedding)] = "[500, 1, 0]",
[nameof(Record.Counter)] = counter1,
[nameof(Record.Text)] = nameof(UpsertAsync_batch_dynamic) + "1"
},
new()
{
[nameof(Record.Key)] = stringVectorFixture.GenerateNextKey<TKey>(),
[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<float>([500, 1, 3]), top: 1).SingleAsync();
Assert.Equal(counter1, result.Record[nameof(Record.Counter)]);
result = await collection.SearchAsync(new ReadOnlyMemory<float>([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<Record>(storeGenerator: true, collectionGenerator: true, propertyGenerator: true);
var exception = await Assert.ThrowsAsync<NotSupportedException>(() => 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<Record>(storeGenerator: true, collectionGenerator: true, propertyGenerator: true);
var exception = await Assert.ThrowsAsync<NotSupportedException>(() => 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<Record>(storeGenerator: true, collectionGenerator: true, propertyGenerator: true);
var exception = await Assert.ThrowsAsync<NotSupportedException>(() =>
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<TKey>
{
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<TKey>
{
public ReadOnlyMemory<float> 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<TKey, TRecord> GetCollection<TRecord>(
bool storeGenerator = false,
bool collectionGenerator = false,
bool propertyGenerator = false)
where TRecord : class
{
var (vectorStore, recordDefinition) = this.GetStoreAndRecordDefinition(storeGenerator, collectionGenerator, propertyGenerator);
return stringVectorFixture.GetCollection<TRecord>(vectorStore, stringVectorFixture.CollectionName, recordDefinition);
}
private VectorStoreCollection<object, Dictionary<string, object?>> 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<VectorStoreVectorProperty>())
{
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<TKey, Record>
{
private int _counter;
protected override string CollectionNameBase => nameof(EmbeddingGenerationTests<int>);
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<Record> BuildTestData() =>
[
new()
{
Key = this.GenerateNextKey<TKey>(),
Embedding = "[1, 1, 1]",
Counter = this.GenerateNextCounter(),
Text = "Store ([1, 1, 1])"
},
new()
{
Key = this.GenerateNextKey<TKey>(),
Embedding = "[1, 1, 2]",
Counter = this.GenerateNextCounter(),
Text = "Collection ([1, 1, 2])"
},
new()
{
Key = this.GenerateNextKey<TKey>(),
Embedding = "[1, 1, 3]",
Counter = this.GenerateNextCounter(),
Text = "Property ([1, 1, 3])"
}
];
public virtual VectorStoreCollection<TKey, TRecord> GetCollection<TRecord>(
VectorStore vectorStore,
string collectionName,
VectorStoreCollectionDefinition? recordDefinition = null)
where TRecord : class
=> vectorStore.GetCollection<TKey, TRecord>(collectionName, recordDefinition);
public virtual VectorStoreCollection<object, Dictionary<string, object?>> GetDynamicCollection(
VectorStore vectorStore,
string collectionName,
VectorStoreCollectionDefinition recordDefinition)
=> vectorStore.GetDynamicCollection(collectionName, recordDefinition);
public abstract VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator = null);
public abstract Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates { get; }
public abstract Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates { get; }
public virtual int GenerateNextCounter()
=> Interlocked.Increment(ref this._counter);
}
public abstract class RomOfFloatVectorFixture : VectorStoreCollectionFixture<TKey, RecordWithRomOfFloatVectorProperty>
{
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<float>), 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<RecordWithRomOfFloatVectorProperty> BuildTestData() =>
[
new()
{
Key = this.GenerateNextKey<TKey>(),
Embedding = new ReadOnlyMemory<float>([1, 1, 1]),
Counter = this.GenerateNextCounter(),
Text = "Store ([1, 1, 1])",
},
new()
{
Key = this.GenerateNextKey<TKey>(),
Embedding = new ReadOnlyMemory<float>([1, 1, 2]),
Counter = this.GenerateNextCounter(),
Text = "Collection ([1, 1, 2])"
},
new()
{
Key = this.GenerateNextKey<TKey>(),
Embedding = new ReadOnlyMemory<float>([1, 1, 3]),
Counter = this.GenerateNextCounter(),
Text = "Property ([1, 1, 3])"
}
];
public virtual VectorStoreCollection<TKey, TRecord> GetCollection<TRecord>(
VectorStore vectorStore,
string collectionName,
VectorStoreCollectionDefinition? recordDefinition = null)
where TRecord : class
=> vectorStore.GetCollection<TKey, TRecord>(collectionName, recordDefinition);
public virtual VectorStoreCollection<object, Dictionary<string, object?>> GetDynamicCollection(
VectorStore vectorStore,
string collectionName,
VectorStoreCollectionDefinition recordDefinition)
=> vectorStore.GetDynamicCollection(collectionName, recordDefinition);
public abstract VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGenerator = null);
public abstract Func<IServiceCollection, IServiceCollection>[] DependencyInjectionStoreRegistrationDelegates { get; }
public abstract Func<IServiceCollection, IServiceCollection>[] DependencyInjectionCollectionRegistrationDelegates { get; }
public virtual int GenerateNextCounter()
=> Interlocked.Increment(ref this._counter);
}
protected sealed class FakeEmbeddingGenerator(int? replaceLast = null) : IEmbeddingGenerator<string, Embedding<float>>
{
public Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
IEnumerable<string> values,
EmbeddingGenerationOptions? options = null,
CancellationToken cancellationToken = default)
{
var results = new GeneratedEmbeddings<Embedding<float>>();
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<float>(vector));
}
return Task.FromResult(results);
}
public object? GetService(Type serviceType, object? serviceKey = null)
=> null;
public void Dispose()
{
}
}
private sealed class FakeCustomerEmbeddingGenerator(float[] embedding) : IEmbeddingGenerator<Customer, Embedding<float>>
{
public Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerable<Customer> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new GeneratedEmbeddings<Embedding<float>> { new(embedding) });
public object? GetService(Type serviceType, object? serviceKey = null)
=> null;
public void Dispose()
{
}
}
#endregion Support
}