chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace VectorData.ConformanceTests.ModelTests;
|
||||
|
||||
public abstract class BasicModelTests<TKey>(BasicModelTests<TKey>.Fixture fixture) : IAsyncLifetime
|
||||
where TKey : notnull
|
||||
{
|
||||
#region Get
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_single_record(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedRecord.Key, new() { IncludeVectors = includeVectors });
|
||||
|
||||
expectedRecord.AssertEqual(received, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_multiple_records(bool includeVectors)
|
||||
{
|
||||
var expectedRecords = fixture.TestData.Take(2);
|
||||
var ids = expectedRecords.Select(record => record.Key);
|
||||
|
||||
var received = await this.Collection.GetAsync(ids, new() { IncludeVectors = includeVectors }).ToArrayAsync();
|
||||
|
||||
foreach (var record in expectedRecords)
|
||||
{
|
||||
record.AssertEqual(
|
||||
received.Single(r => r.Key.Equals(record.Key)),
|
||||
includeVectors,
|
||||
fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_throws_for_null_key()
|
||||
{
|
||||
// Skip this test for value type keys
|
||||
if (default(TKey) is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException ex = await Assert.ThrowsAsync<ArgumentNullException>(() => this.Collection.GetAsync((TKey)default!));
|
||||
Assert.Equal("key", ex.ParamName);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_throws_for_null_keys()
|
||||
{
|
||||
ArgumentNullException ex = await Assert.ThrowsAsync<ArgumentNullException>(() => this.Collection.GetAsync(keys: null!).ToArrayAsync().AsTask());
|
||||
Assert.Equal("keys", ex.ParamName);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_returns_null_for_missing_key()
|
||||
{
|
||||
TKey key = fixture.GenerateNextKey<TKey>();
|
||||
|
||||
Assert.Null(await this.Collection.GetAsync(key));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_multiple_records_with_missing_keys_returns_only_existing()
|
||||
{
|
||||
var expectedRecords = fixture.TestData.Take(2).ToArray();
|
||||
var missingKey = fixture.GenerateNextKey<TKey>();
|
||||
var ids = expectedRecords.Select(record => record.Key).Append(missingKey).ToArray();
|
||||
|
||||
var received = await this.Collection.GetAsync(ids).ToListAsync();
|
||||
|
||||
Assert.Equal(2, received.Count);
|
||||
|
||||
foreach (var record in expectedRecords)
|
||||
{
|
||||
record.AssertEqual(
|
||||
received.Single(r => r.Key.Equals(record.Key)),
|
||||
includeVectors: false,
|
||||
fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_returns_empty_for_empty_keys()
|
||||
{
|
||||
Assert.Empty(await this.Collection.GetAsync([]).ToArrayAsync());
|
||||
}
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_with_filter(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var results = await this.Collection.GetAsync(
|
||||
r => r.Number == 1,
|
||||
top: 2,
|
||||
new() { IncludeVectors = includeVectors })
|
||||
.ToListAsync();
|
||||
|
||||
var receivedRecord = Assert.Single(results);
|
||||
expectedRecord.AssertEqual(receivedRecord, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_by_true()
|
||||
{
|
||||
Assert.True(fixture.TestData.Count < 100);
|
||||
|
||||
var count = await this.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
|
||||
Assert.Equal(fixture.TestData.Count, count);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_and_OrderBy()
|
||||
{
|
||||
var ascendingNumbers = fixture.TestData.Where(r => r.Number > 1).OrderBy(r => r.Number).Take(2).Select(r => r.Number).ToList();
|
||||
var descendingNumbers = fixture.TestData.Where(r => r.Number > 1).OrderByDescending(r => r.Number).Take(2).Select(r => r.Number).ToList();
|
||||
|
||||
// Make sure the actual results are different for ascending/descending, otherwise the test is meaningless
|
||||
Assert.NotEqual(ascendingNumbers, descendingNumbers);
|
||||
|
||||
var results = await this.Collection.GetAsync(
|
||||
r => r.Number > 1,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Ascending(r => r.Number) })
|
||||
.Select(r => r.Number)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(ascendingNumbers, results);
|
||||
|
||||
results = await this.Collection.GetAsync(
|
||||
r => r.Number > 1,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Descending(r => r.Number) })
|
||||
.Select(r => r.Number)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(descendingNumbers, results);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_and_multiple_OrderBys()
|
||||
{
|
||||
var ascendingNumbers = fixture.TestData
|
||||
.OrderByDescending(r => r.Text)
|
||||
.ThenBy(r => r.Number)
|
||||
.Take(2).Select(r => r.Number).ToList();
|
||||
var descendingNumbers = fixture.TestData
|
||||
.OrderByDescending(r => r.Text)
|
||||
.ThenByDescending(r => r.Number)
|
||||
.Take(2)
|
||||
.Select(r => r.Number)
|
||||
.ToList();
|
||||
|
||||
// Make sure the actual results are different for ascending/descending, otherwise the test is meaningless
|
||||
Assert.NotEqual(ascendingNumbers, descendingNumbers);
|
||||
|
||||
var results = await this.Collection.GetAsync(
|
||||
r => true,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Descending(r => r.Text).Ascending(r => r.Number) })
|
||||
.Select(r => r.Number)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(ascendingNumbers, results);
|
||||
|
||||
results = await this.Collection.GetAsync(
|
||||
r => true,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Descending(r => r.Text).Descending(r => r.Number) })
|
||||
.Select(r => r.Number)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(descendingNumbers, results);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_and_OrderBy_and_Skip()
|
||||
{
|
||||
var results = await this.Collection.GetAsync(
|
||||
r => r.Number > 1,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Ascending(r => r.Number), Skip = 1 })
|
||||
.Select(r => r.Number)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(
|
||||
fixture.TestData.Where(r => r.Number > 1).OrderBy(r => r.Number).Skip(1).Take(2).Select(r => r.Number),
|
||||
results);
|
||||
}
|
||||
|
||||
#endregion Get
|
||||
|
||||
#region Upsert
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_single_record()
|
||||
{
|
||||
TKey expectedKey = fixture.GenerateNextKey<TKey>();
|
||||
Record inserted = new()
|
||||
{
|
||||
Key = expectedKey,
|
||||
Text = "New record",
|
||||
Number = 123,
|
||||
Vector = new([10, 0, 0])
|
||||
};
|
||||
|
||||
Assert.Null(await this.Collection.GetAsync(expectedKey));
|
||||
await this.Collection.UpsertAsync(inserted);
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedKey, new() { IncludeVectors = true });
|
||||
inserted.AssertEqual(received, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
|
||||
await fixture.TestStore.WaitForDataAsync(this.Collection, recordCount: fixture.TestData.Count + 1);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Update_single_record()
|
||||
{
|
||||
var existingRecord = fixture.TestData[1];
|
||||
Record updated = new()
|
||||
{
|
||||
Key = existingRecord.Key,
|
||||
Text = "Updated record",
|
||||
Number = 456,
|
||||
Vector = new([10, 0, 0])
|
||||
};
|
||||
|
||||
Assert.NotNull(await this.Collection.GetAsync(existingRecord.Key));
|
||||
await this.Collection.UpsertAsync(updated);
|
||||
|
||||
var received = await this.Collection.GetAsync(existingRecord.Key, new() { IncludeVectors = true });
|
||||
updated.AssertEqual(received, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_multiple_records()
|
||||
{
|
||||
Record[] newRecords =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = fixture.GenerateNextKey<TKey>(),
|
||||
Number = 100,
|
||||
Text = "New record 1",
|
||||
Vector = new([10, 0, 1])
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = fixture.GenerateNextKey<TKey>(),
|
||||
Number = 101,
|
||||
Text = "New record 2",
|
||||
Vector = new([10, 0, 2])
|
||||
},
|
||||
];
|
||||
|
||||
var keys = newRecords.Select(record => record.Key).ToArray();
|
||||
Assert.Empty(await this.Collection.GetAsync(keys).ToArrayAsync());
|
||||
|
||||
await this.Collection.UpsertAsync(newRecords);
|
||||
|
||||
var received = await this.Collection.GetAsync(keys, new() { IncludeVectors = true }).ToArrayAsync();
|
||||
|
||||
Assert.Collection(
|
||||
received.OrderBy(r => r.Number),
|
||||
r => newRecords[0].AssertEqual(r, includeVectors: true, fixture.TestStore.VectorsComparable),
|
||||
r => newRecords[1].AssertEqual(r, includeVectors: true, fixture.TestStore.VectorsComparable));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Update_multiple_records()
|
||||
{
|
||||
Record[] existingRecords =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = fixture.TestData[0].Key,
|
||||
Number = 101,
|
||||
Text = "Updated record 1",
|
||||
Vector = new([10, 0, 1])
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = fixture.TestData[1].Key,
|
||||
Number = 102,
|
||||
Text = "Updated record 2",
|
||||
Vector = new([10, 0, 2])
|
||||
}
|
||||
];
|
||||
|
||||
await this.Collection.UpsertAsync(existingRecords);
|
||||
|
||||
var keys = existingRecords.Select(record => record.Key).ToArray();
|
||||
var received = await this.Collection.GetAsync(keys, new() { IncludeVectors = true }).ToArrayAsync();
|
||||
|
||||
Assert.Collection(
|
||||
received.OrderBy(r => r.Number),
|
||||
r => existingRecords[0].AssertEqual(r, includeVectors: true, fixture.TestStore.VectorsComparable),
|
||||
r => existingRecords[1].AssertEqual(r, includeVectors: true, fixture.TestStore.VectorsComparable));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_and_update_in_same_batch()
|
||||
{
|
||||
Record[] records =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = fixture.GenerateNextKey<TKey>(),
|
||||
Number = 101,
|
||||
Text = "New record",
|
||||
Vector = new([10, 0, 1])
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = fixture.TestData[0].Key,
|
||||
Number = 102,
|
||||
Text = "Updated record",
|
||||
Vector = new([10, 0, 2])
|
||||
},
|
||||
];
|
||||
|
||||
await this.Collection.UpsertAsync(records);
|
||||
|
||||
var keys = records.Select(record => record.Key).ToArray();
|
||||
var received = await this.Collection.GetAsync(keys, new() { IncludeVectors = true }).ToArrayAsync();
|
||||
|
||||
Assert.Collection(
|
||||
received.OrderBy(r => r.Number),
|
||||
r => records[0].AssertEqual(r, includeVectors: true, fixture.TestStore.VectorsComparable),
|
||||
r => records[1].AssertEqual(r, includeVectors: true, fixture.TestStore.VectorsComparable));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task UpsertAsync_throws_for_null_batch()
|
||||
{
|
||||
ArgumentNullException ex = await Assert.ThrowsAsync<ArgumentNullException>(() => this.Collection.UpsertAsync(records: null!));
|
||||
Assert.Equal("records", ex.ParamName);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task UpsertAsync_does_nothing_for_empty_batch()
|
||||
{
|
||||
Assert.True(fixture.TestData.Count < 100);
|
||||
|
||||
var beforeCount = await this.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
await this.Collection.UpsertAsync([]);
|
||||
var afterCount = await this.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
|
||||
Assert.Equal(afterCount, beforeCount);
|
||||
}
|
||||
|
||||
#endregion Upsert
|
||||
|
||||
#region Delete
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Delete_single_record()
|
||||
{
|
||||
var keyToRemove = fixture.TestData[0].Key;
|
||||
|
||||
await this.Collection.DeleteAsync(keyToRemove);
|
||||
Assert.Null(await this.Collection.GetAsync(keyToRemove));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Delete_multiple_records()
|
||||
{
|
||||
TKey[] keysToRemove = [fixture.TestData[0].Key, fixture.TestData[1].Key];
|
||||
|
||||
await this.Collection.DeleteAsync(keysToRemove);
|
||||
Assert.Empty(await this.Collection.GetAsync(keysToRemove).ToArrayAsync());
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task DeleteAsync_does_nothing_for_non_existing_key()
|
||||
{
|
||||
var beforeCount = await this.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
await this.Collection.DeleteAsync(fixture.GenerateNextKey<TKey>());
|
||||
var afterCount = await this.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
|
||||
Assert.Equal(afterCount, beforeCount);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task DeleteAsync_does_nothing_for_empty_batch()
|
||||
{
|
||||
var beforeCount = await this.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
await this.Collection.DeleteAsync([]);
|
||||
var afterCount = await this.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
|
||||
Assert.Equal(afterCount, beforeCount);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task DeleteAsync_throws_for_null_keys()
|
||||
{
|
||||
ArgumentNullException ex = await Assert.ThrowsAsync<ArgumentNullException>(() => this.Collection.DeleteAsync(keys: null!));
|
||||
Assert.Equal("keys", ex.ParamName);
|
||||
}
|
||||
|
||||
#endregion Delete
|
||||
|
||||
#region Search
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task SearchAsync(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var result = await this.Collection
|
||||
.SearchAsync(
|
||||
expectedRecord.Vector,
|
||||
top: 1,
|
||||
new() { IncludeVectors = includeVectors })
|
||||
.SingleAsync();
|
||||
|
||||
expectedRecord.AssertEqual(result.Record, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task SearchAsync_with_Skip()
|
||||
{
|
||||
var result = await this.Collection
|
||||
.SearchAsync(
|
||||
fixture.TestData[0].Vector,
|
||||
top: 1,
|
||||
new() { Skip = 1 })
|
||||
.SingleAsync();
|
||||
|
||||
fixture.TestData[1].AssertEqual(result.Record, includeVectors: false, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task SearchAsync_with_Filter()
|
||||
{
|
||||
var result = await this.Collection
|
||||
.SearchAsync(
|
||||
fixture.TestData[0].Vector,
|
||||
top: 1,
|
||||
new() { Filter = r => r.Number == 2 })
|
||||
.SingleAsync();
|
||||
|
||||
fixture.TestData[1].AssertEqual(result.Record, includeVectors: false, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
// For ScoreThreshold, see DistanceFunctionTests (to ensure we tests thresholds for each and every function)
|
||||
|
||||
#endregion Search
|
||||
|
||||
protected VectorStoreCollection<TKey, Record> Collection => fixture.Collection;
|
||||
|
||||
public abstract class Fixture : VectorStoreCollectionFixture<TKey, Record>
|
||||
{
|
||||
protected override string CollectionNameBase => nameof(BasicModelTests<int>);
|
||||
|
||||
protected override List<Record> BuildTestData() =>
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Number = 1,
|
||||
Text = "foo",
|
||||
Vector = new([1, 2, 3])
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Number = 2,
|
||||
Text = "bar",
|
||||
Vector = new([1, 2, 4])
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Number = 3,
|
||||
Text = "foo", // identical text as above
|
||||
Vector = new([1, 2, 5])
|
||||
}
|
||||
];
|
||||
|
||||
public override VectorStoreCollectionDefinition CreateRecordDefinition()
|
||||
=> new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(nameof(Record.Key), typeof(TKey)),
|
||||
new VectorStoreVectorProperty(nameof(Record.Vector), typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
DistanceFunction = this.DistanceFunction,
|
||||
IndexKind = this.IndexKind
|
||||
},
|
||||
|
||||
new VectorStoreDataProperty(nameof(Record.Number), typeof(int)) { IsIndexed = true },
|
||||
new VectorStoreDataProperty(nameof(Record.Text), typeof(string)) { IsIndexed = true },
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class Record : TestRecord<TKey>
|
||||
{
|
||||
[VectorStoreData(StorageName = "text")]
|
||||
public string? Text { get; set; }
|
||||
|
||||
[VectorStoreData(StorageName = "number")]
|
||||
public int Number { get; set; }
|
||||
|
||||
[VectorStoreVector(Dimensions: 3, StorageName = "vector")]
|
||||
public ReadOnlyMemory<float> Vector { get; set; }
|
||||
|
||||
public void AssertEqual(Record? other, bool includeVectors, bool compareVectors)
|
||||
{
|
||||
Assert.NotNull(other);
|
||||
Assert.Equal(this.Key, other.Key);
|
||||
Assert.Equal(this.Text, other.Text);
|
||||
Assert.Equal(this.Number, other.Number);
|
||||
|
||||
if (includeVectors)
|
||||
{
|
||||
Assert.Equal(this.Vector.Span.Length, other.Vector.Span.Length);
|
||||
|
||||
if (compareVectors)
|
||||
{
|
||||
Assert.Equal(this.Vector.ToArray(), other.Vector.ToArray());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(0, other.Vector.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"Key: {this.Key}, Text: {this.Text}";
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> fixture.ReseedAsync();
|
||||
|
||||
public Task DisposeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public static readonly TheoryData<bool> IncludeVectorsData = [false, true];
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace VectorData.ConformanceTests.ModelTests;
|
||||
|
||||
public abstract class DynamicModelTests<TKey>(DynamicModelTests<TKey>.Fixture fixture) : IAsyncLifetime
|
||||
where TKey : notnull
|
||||
{
|
||||
#region Get
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_single_record(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var received = await fixture.Collection.GetAsync(
|
||||
(TKey)expectedRecord[KeyPropertyName]!,
|
||||
new() { IncludeVectors = includeVectors });
|
||||
|
||||
AssertEquivalent(expectedRecord, received, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_multiple_records(bool includeVectors)
|
||||
{
|
||||
var expectedRecords = fixture.TestData.Take(2);
|
||||
var ids = expectedRecords.Select(record => record[KeyPropertyName]!);
|
||||
|
||||
var received = await fixture.Collection.GetAsync(ids, new() { IncludeVectors = includeVectors }).ToArrayAsync();
|
||||
|
||||
foreach (var record in expectedRecords)
|
||||
{
|
||||
AssertEquivalent(
|
||||
record,
|
||||
received.Single(r => r[KeyPropertyName]!.Equals(record[KeyPropertyName])),
|
||||
includeVectors,
|
||||
fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_throws_for_null_key()
|
||||
{
|
||||
// Skip this test for value type keys
|
||||
if (default(TKey) is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException ex = await Assert.ThrowsAsync<ArgumentNullException>(() => fixture.Collection.GetAsync((TKey)default!));
|
||||
Assert.Equal("key", ex.ParamName);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_throws_for_null_keys()
|
||||
{
|
||||
ArgumentNullException ex = await Assert.ThrowsAsync<ArgumentNullException>(() => fixture.Collection.GetAsync(keys: null!).ToArrayAsync().AsTask());
|
||||
Assert.Equal("keys", ex.ParamName);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_returns_null_for_missing_key()
|
||||
{
|
||||
TKey key = fixture.GenerateNextKey<TKey>();
|
||||
|
||||
Assert.Null(await fixture.Collection.GetAsync(key));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_returns_empty_for_empty_keys()
|
||||
{
|
||||
Assert.Empty(await fixture.Collection.GetAsync([]).ToArrayAsync());
|
||||
}
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_with_filter(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var results = await fixture.Collection.GetAsync(
|
||||
r => (int)r[IntegerPropertyName]! == 1,
|
||||
top: 2,
|
||||
new() { IncludeVectors = includeVectors })
|
||||
.ToListAsync();
|
||||
|
||||
var receivedRecord = Assert.Single(results);
|
||||
AssertEquivalent(expectedRecord, receivedRecord, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_by_true()
|
||||
{
|
||||
var count = await fixture.Collection.GetAsync(r => true, top: 100).CountAsync();
|
||||
Assert.Equal(fixture.TestData.Count, count);
|
||||
Assert.True(count < 100);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_and_OrderBy()
|
||||
{
|
||||
var ascendingNumbers = fixture.TestData
|
||||
.Where(r => (int)r[IntegerPropertyName]! > 1)
|
||||
.OrderBy(r => r[IntegerPropertyName])
|
||||
.Take(2)
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToList();
|
||||
|
||||
var descendingNumbers = fixture.TestData
|
||||
.Where(r => (int)r[IntegerPropertyName]! > 1)
|
||||
.OrderByDescending(r => r[IntegerPropertyName])
|
||||
.Take(2)
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToList();
|
||||
|
||||
// Make sure the actual results are different for ascending/descending, otherwise the test is meaningless
|
||||
Assert.NotEqual(ascendingNumbers, descendingNumbers);
|
||||
|
||||
// Finally, query once with ascending and once with descending, comparing against the expected results above.
|
||||
var results = await fixture.Collection.GetAsync(
|
||||
r => (int)r[IntegerPropertyName]! > 1,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Ascending(r => r[IntegerPropertyName]) })
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(ascendingNumbers, results);
|
||||
|
||||
results = await fixture.Collection.GetAsync(
|
||||
r => (int)r[IntegerPropertyName]! > 1,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Descending(r => r[IntegerPropertyName]) })
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(descendingNumbers, results);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_and_multiple_OrderBys()
|
||||
{
|
||||
var ascendingNumbers = fixture.TestData
|
||||
.OrderByDescending(r => r[StringPropertyName])
|
||||
.ThenBy(r => r[IntegerPropertyName])
|
||||
.Take(2)
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToList();
|
||||
|
||||
var descendingNumbers = fixture.TestData
|
||||
.OrderByDescending(r => r[StringPropertyName])
|
||||
.ThenByDescending(r => r[IntegerPropertyName])
|
||||
.Take(2)
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToList();
|
||||
|
||||
// Make sure the actual results are different for ascending/descending, otherwise the test is meaningless
|
||||
Assert.NotEqual(ascendingNumbers, descendingNumbers);
|
||||
|
||||
var results = await fixture.Collection.GetAsync(
|
||||
r => true,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Descending(r => r[StringPropertyName]).Ascending(r => r[IntegerPropertyName]) })
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(ascendingNumbers, results);
|
||||
|
||||
results = await fixture.Collection.GetAsync(
|
||||
r => true,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Descending(r => r[StringPropertyName]).Descending(r => r[IntegerPropertyName]) })
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(descendingNumbers, results);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task GetAsync_with_filter_and_OrderBy_and_Skip()
|
||||
{
|
||||
var results = await fixture.Collection.GetAsync(
|
||||
r => (int)r[IntegerPropertyName]! > 1,
|
||||
top: 2,
|
||||
new() { OrderBy = o => o.Ascending(r => r[IntegerPropertyName]), Skip = 1 })
|
||||
.Select(r => (int)r[IntegerPropertyName]!)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(
|
||||
fixture.TestData
|
||||
.Where(r => (int)r[IntegerPropertyName]! > 1)
|
||||
.OrderBy(r => r[IntegerPropertyName])
|
||||
.Skip(1)
|
||||
.Take(2)
|
||||
.Select(r => (int)r[IntegerPropertyName]!),
|
||||
results);
|
||||
}
|
||||
|
||||
#endregion Get
|
||||
|
||||
#region Upsert
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_single_record()
|
||||
{
|
||||
TKey expectedKey = fixture.GenerateNextKey<TKey>();
|
||||
var inserted = new Dictionary<string, object?>
|
||||
{
|
||||
[KeyPropertyName] = expectedKey,
|
||||
[StringPropertyName] = "some",
|
||||
[IntegerPropertyName] = 123,
|
||||
[VectorPropertyName] = new ReadOnlyMemory<float>([10, 0, 0])
|
||||
};
|
||||
|
||||
Assert.Null(await this.Collection.GetAsync(expectedKey));
|
||||
await this.Collection.UpsertAsync(inserted);
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedKey, new() { IncludeVectors = true });
|
||||
AssertEquivalent(inserted, received, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Update_single_record()
|
||||
{
|
||||
var existingRecord = fixture.TestData[1];
|
||||
var updated = new Dictionary<string, object?>
|
||||
{
|
||||
[KeyPropertyName] = existingRecord[KeyPropertyName],
|
||||
[StringPropertyName] = "different",
|
||||
[IntegerPropertyName] = 456,
|
||||
[VectorPropertyName] = new ReadOnlyMemory<float>(Enumerable.Repeat(0.7f, 3).ToArray())
|
||||
};
|
||||
|
||||
Assert.NotNull(await this.Collection.GetAsync((TKey)existingRecord[KeyPropertyName]!));
|
||||
await this.Collection.UpsertAsync(updated);
|
||||
|
||||
var received = await this.Collection.GetAsync((TKey)existingRecord[KeyPropertyName]!, new() { IncludeVectors = true });
|
||||
AssertEquivalent(updated, received, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_multiple_records()
|
||||
{
|
||||
Dictionary<string, object?>[] newRecords =
|
||||
[
|
||||
new()
|
||||
{
|
||||
[KeyPropertyName] = fixture.GenerateNextKey<TKey>(),
|
||||
[IntegerPropertyName] = 100,
|
||||
[StringPropertyName] = "New record 1",
|
||||
[VectorPropertyName] = new ReadOnlyMemory<float>([10, 0, 1])
|
||||
},
|
||||
new()
|
||||
{
|
||||
[KeyPropertyName] = fixture.GenerateNextKey<TKey>(),
|
||||
[IntegerPropertyName] = 101,
|
||||
[StringPropertyName] = "New record 2",
|
||||
[VectorPropertyName] = new ReadOnlyMemory<float>([10, 0, 2])
|
||||
},
|
||||
];
|
||||
|
||||
var keys = newRecords.Select(record => record[KeyPropertyName]!).ToArray();
|
||||
Assert.Empty(await this.Collection.GetAsync(keys).ToArrayAsync());
|
||||
|
||||
await this.Collection.UpsertAsync(newRecords);
|
||||
|
||||
var received = await this.Collection.GetAsync(keys, new() { IncludeVectors = true }).ToArrayAsync();
|
||||
|
||||
Assert.Collection(
|
||||
received.OrderBy(r => r[IntegerPropertyName]),
|
||||
r => AssertEquivalent(newRecords[0], r, includeVectors: true, fixture.TestStore.VectorsComparable),
|
||||
r => AssertEquivalent(newRecords[1], r, includeVectors: true, fixture.TestStore.VectorsComparable));
|
||||
}
|
||||
|
||||
#endregion Upsert
|
||||
|
||||
#region Delete
|
||||
|
||||
[ConditionalFact]
|
||||
public async Task Delete_single_record()
|
||||
{
|
||||
var recordToRemove = fixture.TestData[2];
|
||||
|
||||
Assert.NotNull(await fixture.Collection.GetAsync((TKey)recordToRemove[KeyPropertyName]!));
|
||||
await fixture.Collection.DeleteAsync((TKey)recordToRemove[KeyPropertyName]!);
|
||||
Assert.Null(await fixture.Collection.GetAsync((TKey)recordToRemove[KeyPropertyName]!));
|
||||
}
|
||||
|
||||
// TODO: https://github.com/microsoft/semantic-kernel/issues/13303
|
||||
// [ConditionalFact]
|
||||
// public virtual async Task Delete_multiple_records()
|
||||
// {
|
||||
// TKey[] keysToRemove = [(TKey)fixture.TestData[0][KeyPropertyName]!, (TKey)fixture.TestData[1][KeyPropertyName]!];
|
||||
|
||||
// await this.Collection.DeleteAsync(keysToRemove);
|
||||
// Assert.Empty(await this.Collection.GetAsync(keysToRemove).ToArrayAsync());
|
||||
|
||||
// Assert.Equal(fixture.TestData.Count - 2, await this.GetRecordCount());
|
||||
// }
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task DeleteAsync_does_nothing_for_non_existing_key()
|
||||
{
|
||||
TKey key = fixture.GenerateNextKey<TKey>();
|
||||
|
||||
await fixture.Collection.DeleteAsync(key);
|
||||
}
|
||||
|
||||
#endregion Delete
|
||||
|
||||
#region Search
|
||||
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task SearchAsync(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var result = await this.Collection
|
||||
.SearchAsync(
|
||||
expectedRecord[VectorPropertyName]!,
|
||||
top: 1,
|
||||
new() { IncludeVectors = includeVectors })
|
||||
.SingleAsync();
|
||||
|
||||
AssertEquivalent(expectedRecord, result.Record, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task SearchAsync_with_Skip()
|
||||
{
|
||||
var result = await this.Collection
|
||||
.SearchAsync(
|
||||
fixture.TestData[0][VectorPropertyName]!,
|
||||
top: 1,
|
||||
new() { Skip = 1 })
|
||||
.SingleAsync();
|
||||
|
||||
AssertEquivalent(fixture.TestData[1], result.Record, includeVectors: false, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task SearchAsync_with_Filter()
|
||||
{
|
||||
var result = await this.Collection
|
||||
.SearchAsync(
|
||||
fixture.TestData[0][VectorPropertyName]!,
|
||||
top: 1,
|
||||
new() { Filter = r => (int)r[IntegerPropertyName]! == 2 })
|
||||
.SingleAsync();
|
||||
|
||||
AssertEquivalent(fixture.TestData[1], result.Record, includeVectors: false, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
#endregion Search
|
||||
|
||||
protected static void AssertEquivalent(Dictionary<string, object?> expected, Dictionary<string, object?>? actual, bool includeVectors, bool compareVectors)
|
||||
{
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal(expected[KeyPropertyName], actual[KeyPropertyName]);
|
||||
|
||||
Assert.Equal(expected[StringPropertyName], actual[StringPropertyName]);
|
||||
Assert.Equal(expected[IntegerPropertyName], actual[IntegerPropertyName]);
|
||||
|
||||
if (includeVectors)
|
||||
{
|
||||
Assert.Equal(
|
||||
((ReadOnlyMemory<float>)expected[VectorPropertyName]!).Length,
|
||||
((ReadOnlyMemory<float>)actual[VectorPropertyName]!).Length);
|
||||
|
||||
if (compareVectors)
|
||||
{
|
||||
Assert.Equal(
|
||||
((ReadOnlyMemory<float>)expected[VectorPropertyName]!).ToArray(),
|
||||
((ReadOnlyMemory<float>)actual[VectorPropertyName]!).ToArray());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.False(actual.ContainsKey(VectorPropertyName));
|
||||
}
|
||||
}
|
||||
|
||||
public const string KeyPropertyName = "key";
|
||||
public const string StringPropertyName = "text";
|
||||
public const string IntegerPropertyName = "integer";
|
||||
public const string VectorPropertyName = "vector";
|
||||
|
||||
protected VectorStoreCollection<object, Dictionary<string, object?>> Collection => fixture.Collection;
|
||||
|
||||
public abstract class Fixture : DynamicVectorStoreCollectionFixture<TKey>
|
||||
{
|
||||
protected override string CollectionNameBase => nameof(DynamicModelTests<int>);
|
||||
|
||||
protected override string KeyPropertyName => DynamicModelTests<TKey>.KeyPropertyName;
|
||||
|
||||
protected override VectorStoreCollection<object, Dictionary<string, object?>> GetCollection()
|
||||
=> this.TestStore.CreateDynamicCollection(this.CollectionName, this.CreateRecordDefinition());
|
||||
|
||||
public override VectorStoreCollectionDefinition CreateRecordDefinition()
|
||||
=> new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(this.KeyPropertyName, typeof(TKey)),
|
||||
new VectorStoreDataProperty(StringPropertyName, typeof(string)) { IsIndexed = true},
|
||||
new VectorStoreDataProperty(IntegerPropertyName, typeof(int)) { IsIndexed = true },
|
||||
new VectorStoreVectorProperty(VectorPropertyName, typeof(ReadOnlyMemory<float>), dimensions: 3)
|
||||
{
|
||||
DistanceFunction = this.DistanceFunction,
|
||||
IndexKind = this.IndexKind
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
protected override List<Dictionary<string, object?>> BuildTestData() =>
|
||||
[
|
||||
new()
|
||||
{
|
||||
[this.KeyPropertyName] = this.GenerateNextKey<TKey>(),
|
||||
[StringPropertyName] = "foo",
|
||||
[IntegerPropertyName] = 1,
|
||||
[VectorPropertyName] = new ReadOnlyMemory<float>([1, 2, 3])
|
||||
},
|
||||
new()
|
||||
{
|
||||
[this.KeyPropertyName] = this.GenerateNextKey<TKey>(),
|
||||
[StringPropertyName] = "bar",
|
||||
[IntegerPropertyName] = 2,
|
||||
[VectorPropertyName] = new ReadOnlyMemory<float>([1, 2, 4])
|
||||
},
|
||||
new()
|
||||
{
|
||||
[this.KeyPropertyName] = this.GenerateNextKey<TKey>(),
|
||||
[StringPropertyName] = "foo", // identical text as above
|
||||
[IntegerPropertyName] = 3,
|
||||
[VectorPropertyName] = new ReadOnlyMemory<float>([1, 2, 5])
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> fixture.ReseedAsync();
|
||||
|
||||
public Task DisposeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public static readonly TheoryData<bool> IncludeVectorsData = [false, true];
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace VectorData.ConformanceTests.ModelTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests using a model with multiple vectors.
|
||||
/// </summary>
|
||||
public class MultiVectorModelTests<TKey>(MultiVectorModelTests<TKey>.Fixture fixture) : IAsyncLifetime
|
||||
where TKey : notnull
|
||||
{
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_single_record(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedRecord.Key, new() { IncludeVectors = includeVectors });
|
||||
|
||||
expectedRecord.AssertEqual(received, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_single_record()
|
||||
{
|
||||
TKey expectedKey = fixture.GenerateNextKey<TKey>();
|
||||
MultiVectorRecord inserted = new()
|
||||
{
|
||||
Key = expectedKey,
|
||||
Number = 10,
|
||||
Vector1 = new([10, 0, 0]),
|
||||
Vector2 = new([10, 0, 0]),
|
||||
};
|
||||
|
||||
Assert.Null(await this.Collection.GetAsync(expectedKey));
|
||||
await this.Collection.UpsertAsync(inserted);
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedKey, new() { IncludeVectors = true });
|
||||
inserted.AssertEqual(received, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Delete_single_record()
|
||||
{
|
||||
var keyToRemove = fixture.TestData[0].Key;
|
||||
|
||||
await this.Collection.DeleteAsync(keyToRemove);
|
||||
Assert.Null(await this.Collection.GetAsync(keyToRemove));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task SearchAsync_with_multiple_vector_properties()
|
||||
{
|
||||
var result = await this.Collection
|
||||
.SearchAsync(new ReadOnlyMemory<float>([1, 2, 3]), top: 1, new() { VectorProperty = r => r.Vector1, IncludeVectors = true })
|
||||
.SingleAsync();
|
||||
fixture.TestData[0].AssertEqual(result.Record, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
|
||||
result = await this.Collection
|
||||
.SearchAsync(new ReadOnlyMemory<float>([10, 2, 6]), top: 1, new() { VectorProperty = r => r.Vector2, IncludeVectors = true })
|
||||
.SingleAsync();
|
||||
fixture.TestData[1].AssertEqual(result.Record, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Search_without_explicitly_specified_vector_property_fails()
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await this.Collection.SearchAsync(new ReadOnlyMemory<float>([1, 2, 3]), top: 1).ToListAsync());
|
||||
|
||||
Assert.Equal($"The '{nameof(MultiVectorRecord)}' type has multiple vector properties, please specify your chosen property via options.", exception.Message);
|
||||
}
|
||||
|
||||
protected VectorStoreCollection<TKey, MultiVectorRecord> Collection => fixture.Collection;
|
||||
|
||||
public abstract class Fixture : VectorStoreCollectionFixture<TKey, MultiVectorRecord>
|
||||
{
|
||||
protected override string CollectionNameBase => "MultiVectorModelTests";
|
||||
|
||||
protected override List<MultiVectorRecord> BuildTestData() =>
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Number = 1,
|
||||
Vector1 = new([1, 2, 3]),
|
||||
Vector2 = new([10, 2, 4])
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Number = 2,
|
||||
Vector1 = new([1, 2, 5]),
|
||||
Vector2 = new([10, 2, 6])
|
||||
}
|
||||
];
|
||||
|
||||
public override VectorStoreCollectionDefinition CreateRecordDefinition()
|
||||
=> new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(nameof(MultiVectorRecord.Key), typeof(TKey)),
|
||||
new VectorStoreDataProperty(nameof(MultiVectorRecord.Number), typeof(int)),
|
||||
|
||||
new VectorStoreVectorProperty(nameof(MultiVectorRecord.Vector1), typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
DistanceFunction = this.DistanceFunction,
|
||||
IndexKind = this.IndexKind
|
||||
},
|
||||
|
||||
new VectorStoreVectorProperty(nameof(MultiVectorRecord.Vector2), typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
DistanceFunction = this.DistanceFunction,
|
||||
IndexKind = this.IndexKind
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
protected override Task WaitForDataAsync()
|
||||
=> this.TestStore.WaitForDataAsync(this.Collection, recordCount: this.TestData.Count, vectorProperty: r => r.Vector1);
|
||||
}
|
||||
|
||||
public sealed class MultiVectorRecord : TestRecord<TKey>
|
||||
{
|
||||
public int Number { get; set; }
|
||||
|
||||
public ReadOnlyMemory<float> Vector1 { get; set; }
|
||||
public ReadOnlyMemory<float> Vector2 { get; set; }
|
||||
|
||||
public void AssertEqual(MultiVectorRecord? other, bool includeVectors, bool compareVectors)
|
||||
{
|
||||
Assert.NotNull(other);
|
||||
|
||||
Assert.Equal(this.Key, other.Key);
|
||||
Assert.Equal(this.Number, other.Number);
|
||||
|
||||
if (includeVectors)
|
||||
{
|
||||
Assert.Equal(this.Vector1.Span.Length, other.Vector1.Span.Length);
|
||||
Assert.Equal(this.Vector2.Span.Length, other.Vector2.Span.Length);
|
||||
|
||||
if (compareVectors)
|
||||
{
|
||||
Assert.True(this.Vector1.Span.SequenceEqual(other.Vector1.Span));
|
||||
Assert.True(this.Vector2.Span.SequenceEqual(other.Vector2.Span));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> fixture.ReseedAsync();
|
||||
|
||||
public Task DisposeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public static readonly TheoryData<bool> IncludeVectorsData = [false, true];
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace VectorData.ConformanceTests.ModelTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests using a model without data fields, only a key and an embedding.
|
||||
/// </summary>
|
||||
public class NoDataModelTests<TKey>(NoDataModelTests<TKey>.Fixture fixture) : IAsyncLifetime
|
||||
where TKey : notnull
|
||||
{
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_single_record(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedRecord.Key, new() { IncludeVectors = includeVectors });
|
||||
|
||||
expectedRecord.AssertEqual(received, includeVectors, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_single_record()
|
||||
{
|
||||
TKey expectedKey = fixture.GenerateNextKey<TKey>();
|
||||
NoDataRecord inserted = new()
|
||||
{
|
||||
Key = expectedKey,
|
||||
Floats = new([10, 0, 0])
|
||||
};
|
||||
|
||||
Assert.Null(await this.Collection.GetAsync(expectedKey));
|
||||
await this.Collection.UpsertAsync(inserted);
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedKey, new() { IncludeVectors = true });
|
||||
inserted.AssertEqual(received, includeVectors: true, fixture.TestStore.VectorsComparable);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Delete_single_record()
|
||||
{
|
||||
var keyToRemove = fixture.TestData[0].Key;
|
||||
|
||||
await this.Collection.DeleteAsync(keyToRemove);
|
||||
Assert.Null(await this.Collection.GetAsync(keyToRemove));
|
||||
}
|
||||
|
||||
protected VectorStoreCollection<TKey, NoDataRecord> Collection => fixture.Collection;
|
||||
|
||||
public abstract class Fixture : VectorStoreCollectionFixture<TKey, NoDataRecord>
|
||||
{
|
||||
protected override string CollectionNameBase => nameof(NoDataModelTests<int>);
|
||||
|
||||
protected override List<NoDataRecord> BuildTestData() =>
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Floats = new([1, 2, 3])
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Floats = new([1, 2, 4])
|
||||
}
|
||||
];
|
||||
|
||||
public override VectorStoreCollectionDefinition CreateRecordDefinition()
|
||||
=> new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(nameof(NoDataRecord.Key), typeof(TKey)),
|
||||
new VectorStoreVectorProperty(nameof(NoDataRecord.Floats), typeof(ReadOnlyMemory<float>), 3)
|
||||
{
|
||||
IndexKind = this.IndexKind
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class NoDataRecord : TestRecord<TKey>
|
||||
{
|
||||
[VectorStoreVector(Dimensions: 3, StorageName = "embedding")]
|
||||
public ReadOnlyMemory<float> Floats { get; set; }
|
||||
|
||||
public void AssertEqual(NoDataRecord? other, bool includeVectors, bool compareVectors)
|
||||
{
|
||||
Assert.NotNull(other);
|
||||
Assert.Equal(this.Key, other.Key);
|
||||
|
||||
if (includeVectors)
|
||||
{
|
||||
Assert.Equal(this.Floats.Span.Length, other.Floats.Span.Length);
|
||||
|
||||
if (compareVectors)
|
||||
{
|
||||
Assert.True(this.Floats.Span.SequenceEqual(other.Floats.Span));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> fixture.ReseedAsync();
|
||||
|
||||
public Task DisposeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public static readonly TheoryData<bool> IncludeVectorsData = [false, true];
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using VectorData.ConformanceTests.Support;
|
||||
using VectorData.ConformanceTests.Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace VectorData.ConformanceTests.ModelTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests operations using a model without a vector.
|
||||
/// This is only supported by a subset of databases so only extend if applicable for your database.
|
||||
/// </summary>
|
||||
public class NoVectorModelTests<TKey>(NoVectorModelTests<TKey>.Fixture fixture) : IAsyncLifetime
|
||||
where TKey : notnull
|
||||
{
|
||||
[ConditionalTheory, MemberData(nameof(IncludeVectorsData))]
|
||||
public virtual async Task GetAsync_single_record(bool includeVectors)
|
||||
{
|
||||
var expectedRecord = fixture.TestData[0];
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedRecord.Key, new() { IncludeVectors = includeVectors });
|
||||
|
||||
expectedRecord.AssertEqual(received);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Insert_single_record()
|
||||
{
|
||||
TKey expectedKey = fixture.GenerateNextKey<TKey>();
|
||||
NoVectorRecord inserted = new()
|
||||
{
|
||||
Key = expectedKey,
|
||||
Text = "New record"
|
||||
};
|
||||
|
||||
Assert.Null(await this.Collection.GetAsync(expectedKey));
|
||||
await this.Collection.UpsertAsync(inserted);
|
||||
|
||||
var received = await this.Collection.GetAsync(expectedKey, new() { IncludeVectors = true });
|
||||
inserted.AssertEqual(received);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public virtual async Task Delete_single_record()
|
||||
{
|
||||
var keyToRemove = fixture.TestData[0].Key;
|
||||
|
||||
await this.Collection.DeleteAsync(keyToRemove);
|
||||
Assert.Null(await this.Collection.GetAsync(keyToRemove));
|
||||
}
|
||||
|
||||
protected VectorStoreCollection<TKey, NoVectorRecord> Collection => fixture.Collection;
|
||||
|
||||
public abstract class Fixture : VectorStoreCollectionFixture<TKey, NoVectorRecord>
|
||||
{
|
||||
protected override string CollectionNameBase => nameof(NoVectorModelTests<int>);
|
||||
|
||||
protected override List<NoVectorRecord> BuildTestData() =>
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Text = "foo",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = this.GenerateNextKey<TKey>(),
|
||||
Text = "bar",
|
||||
}
|
||||
];
|
||||
|
||||
public override VectorStoreCollectionDefinition CreateRecordDefinition()
|
||||
=> new()
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
new VectorStoreKeyProperty(nameof(NoVectorRecord.Key), typeof(TKey)),
|
||||
new VectorStoreDataProperty(nameof(NoVectorRecord.Text), typeof(string)) { IsIndexed = true }
|
||||
]
|
||||
};
|
||||
|
||||
// The default implementation of WaitForDataAsync uses SearchAsync, but our model has no vectors.
|
||||
protected override async Task WaitForDataAsync()
|
||||
{
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
var results = await this.Collection.GetAsync([this.TestData[0].Key, this.TestData[1].Key]).ToArrayAsync();
|
||||
if (results.Length == this.TestData.Count && results.All(r => r != null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Data did not appear in the collection within the expected time.");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class NoVectorRecord : TestRecord<TKey>
|
||||
{
|
||||
[VectorStoreData(StorageName = "text")]
|
||||
public string? Text { get; set; }
|
||||
|
||||
public void AssertEqual(NoVectorRecord? other)
|
||||
{
|
||||
Assert.NotNull(other);
|
||||
Assert.Equal(this.Key, other.Key);
|
||||
Assert.Equal(this.Text, other.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> fixture.ReseedAsync();
|
||||
|
||||
public Task DisposeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public static readonly TheoryData<bool> IncludeVectorsData = [false, true];
|
||||
}
|
||||
Reference in New Issue
Block a user