diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchKeywordVectorizedHybridSearchTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchKeywordVectorizedHybridSearchTests.cs deleted file mode 100644 index d42561ac8ee6..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/Memory/AzureAISearch/AzureAISearchKeywordVectorizedHybridSearchTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.VectorData; -using Microsoft.SemanticKernel.Connectors.AzureAISearch; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.Memory.AzureAISearch; - -/// -/// Inherits common integration tests that should pass for any . -/// -/// Azure AI Search setup and teardown. -[Collection("AzureAISearchVectorStoreCollection")] -[AzureAISearchConfigCondition] -public class AzureAISearchKeywordVectorizedHybridSearchTests(AzureAISearchVectorStoreFixture fixture) : BaseKeywordVectorizedHybridSearchTests -{ - protected override string Key1 => "1"; - protected override string Key2 => "2"; - protected override string Key3 => "3"; - protected override string Key4 => "4"; - protected override int DelayAfterUploadInMilliseconds => 2000; - - protected override IVectorStoreRecordCollection GetTargetRecordCollection(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - return new AzureAISearchVectorStoreRecordCollection(fixture.SearchIndexClient, recordCollectionName + AzureAISearchVectorStoreFixture.TestIndexPostfix, new() - { - VectorStoreRecordDefinition = vectorStoreRecordDefinition - }); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLKeywordVectorizedHybridSearchTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLKeywordVectorizedHybridSearchTests.cs deleted file mode 100644 index 3ce18873790f..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLKeywordVectorizedHybridSearchTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.VectorData; -using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.Memory.AzureCosmosDBNoSQL; - -/// -/// Inherits common integration tests that should pass for any . -/// -[Collection("AzureCosmosDBNoSQLVectorStoreCollection")] -[AzureCosmosDBNoSQLConnectionStringSetCondition] -public class AzureCosmosDBNoSQLKeywordVectorizedHybridSearchTests(AzureCosmosDBNoSQLVectorStoreFixture fixture) : BaseKeywordVectorizedHybridSearchTests -{ - protected override string Key1 => "1"; - protected override string Key2 => "2"; - protected override string Key3 => "3"; - protected override string Key4 => "4"; - protected override int DelayAfterUploadInMilliseconds => 2000; - protected override string? IndexKind { get; } = Microsoft.Extensions.VectorData.IndexKind.Flat; - - protected override IVectorStoreRecordCollection GetTargetRecordCollection(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - return new AzureCosmosDBNoSQLVectorStoreRecordCollection(fixture.Database!, recordCollectionName, new() - { - VectorStoreRecordDefinition = vectorStoreRecordDefinition - }); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/BaseKeywordVectorizedHybridSearchTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/BaseKeywordVectorizedHybridSearchTests.cs deleted file mode 100644 index 149159ad46c0..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/Memory/BaseKeywordVectorizedHybridSearchTests.cs +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.VectorData; -using SemanticKernel.IntegrationTests.Connectors.Memory.Xunit; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.Memory; - -/// -/// Base class for common integration tests that should pass for any . -/// -/// The type of key to use with the record collection. -public abstract class BaseKeywordVectorizedHybridSearchTests - where TKey : notnull -{ - protected abstract TKey Key1 { get; } - protected abstract TKey Key2 { get; } - protected abstract TKey Key3 { get; } - protected abstract TKey Key4 { get; } - - protected virtual int DelayAfterIndexCreateInMilliseconds { get; } = 0; - - protected virtual int DelayAfterUploadInMilliseconds { get; } = 0; - - protected virtual string? IndexKind { get; } = null; - - protected abstract IVectorStoreRecordCollection GetTargetRecordCollection(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition); - - [VectorStoreFact] - public async Task SearchShouldReturnExpectedResultsAsync() - { - // Arrange - var sut = this.GetTargetRecordCollection>( - "kwhybrid", - this.KeyWithVectorAndStringRecordDefinition); - - var hybridSearch = sut as IKeywordHybridSearch>; - - try - { - var vector = new ReadOnlyMemory([1, 0, 0, 0]); - await this.CreateCollectionAndAddDataAsync(sut, vector); - - // Act - // All records have the same vector, but the third contains Grapes, so searching for - // Grapes should return the third record first. - var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["Grapes"]); - - // Assert - var results = await searchResult.Results.ToListAsync(); - Assert.Equal(3, results.Count); - - Assert.Equal(this.Key3, results[0].Record.Key); - } - finally - { - // Cleanup - await sut.DeleteCollectionAsync(); - } - } - - [VectorStoreFact] - public async Task SearchWithFilterShouldReturnExpectedResultsAsync() - { - // Arrange - var sut = this.GetTargetRecordCollection>( - "kwfilteredhybrid", - this.KeyWithVectorAndStringRecordDefinition); - - var hybridSearch = sut as IKeywordHybridSearch>; - - try - { - var vector = new ReadOnlyMemory([1, 0, 0, 0]); - await this.CreateCollectionAndAddDataAsync(sut, vector); - - // Act - // All records have the same vector, but the second contains Oranges, however - // adding the filter should limit the results to only the first. -#pragma warning disable CS0618 // Type or member is obsolete - var options = new HybridSearchOptions> - { - OldFilter = new VectorSearchFilter().EqualTo("Code", 1) - }; -#pragma warning restore CS0618 // Type or member is obsolete - var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["Oranges"], options); - - // Assert - var results = await searchResult.Results.ToListAsync(); - Assert.Single(results); - - Assert.Equal(this.Key1, results[0].Record.Key); - } - finally - { - // Cleanup - await sut.DeleteCollectionAsync(); - } - } - - [VectorStoreFact] - public async Task SearchWithTopShouldReturnExpectedResultsAsync() - { - // Arrange - var sut = this.GetTargetRecordCollection>( - "kwtophybrid", - this.KeyWithVectorAndStringRecordDefinition); - - var hybridSearch = sut as IKeywordHybridSearch>; - - try - { - var vector = new ReadOnlyMemory([1, 0, 0, 0]); - await this.CreateCollectionAndAddDataAsync(sut, vector); - - // Act - // All records have the same vector, but the second contains Oranges, so the - // second should be returned first. - var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["Oranges"], new() { Top = 1 }); - - // Assert - var results = await searchResult.Results.ToListAsync(); - Assert.Single(results); - - Assert.Equal(this.Key2, results[0].Record.Key); - } - finally - { - // Cleanup - await sut.DeleteCollectionAsync(); - } - } - - [VectorStoreFact] - public async Task SearchWithSkipShouldReturnExpectedResultsAsync() - { - // Arrange - var sut = this.GetTargetRecordCollection>( - "kwskiphybrid", - this.KeyWithVectorAndStringRecordDefinition); - - var hybridSearch = sut as IKeywordHybridSearch>; - - try - { - var vector = new ReadOnlyMemory([1, 0, 0, 0]); - await this.CreateCollectionAndAddDataAsync(sut, vector); - - // Act - // All records have the same vector, but the first and third contain healthy, - // so when skipping the first two results, we should get the second record. - var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["healthy"], new() { Skip = 2 }); - - // Assert - var results = await searchResult.Results.ToListAsync(); - Assert.Single(results); - - Assert.Equal(this.Key2, results[0].Record.Key); - } - finally - { - // Cleanup - await sut.DeleteCollectionAsync(); - } - } - - [VectorStoreFact] - public async Task SearchWithMultipleKeywordsShouldRankMatchedKeywordsHigherAsync() - { - // Arrange - var sut = this.GetTargetRecordCollection>( - "kwmultikeywordhybrid", - this.KeyWithVectorAndStringRecordDefinition); - - var hybridSearch = sut as IKeywordHybridSearch>; - - try - { - var vector = new ReadOnlyMemory([1, 0, 0, 0]); - await this.CreateCollectionAndAddDataAsync(sut, vector); - - // Act - var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["tangy", "nourishing"]); - - // Assert - var results = await searchResult.Results.ToListAsync(); - Assert.Equal(3, results.Count); - - Assert.True(results[0].Record.Key.Equals(this.Key1) || results[0].Record.Key.Equals(this.Key2)); - Assert.True(results[1].Record.Key.Equals(this.Key1) || results[1].Record.Key.Equals(this.Key2)); - Assert.Equal(this.Key3, results[2].Record.Key); - } - finally - { - // Cleanup - await sut.DeleteCollectionAsync(); - } - } - - [VectorStoreFact] - public async Task SearchWithMultiTextRecordSearchesRequestedFieldAsync() - { - // Arrange - var sut = this.GetTargetRecordCollection>( - "kwmultitexthybrid", - this.MultiSearchStringRecordDefinition); - - var hybridSearch = sut as IKeywordHybridSearch>; - - try - { - var vector = new ReadOnlyMemory([1, 0, 0, 0]); - await this.CreateCollectionAndAddDataAsync(sut, vector); - - // Act - var searchResult1 = await hybridSearch!.HybridSearchAsync(vector, ["Apples"], new() { AdditionalProperty = r => r.Text2 }); - var searchResult2 = await hybridSearch!.HybridSearchAsync(vector, ["Oranges"], new() { AdditionalProperty = r => r.Text2 }); - - // Assert - var results1 = await searchResult1.Results.ToListAsync(); - Assert.Equal(2, results1.Count); - - Assert.Equal(this.Key2, results1[0].Record.Key); - Assert.Equal(this.Key1, results1[1].Record.Key); - - var results2 = await searchResult2.Results.ToListAsync(); - Assert.Equal(2, results2.Count); - - Assert.Equal(this.Key1, results2[0].Record.Key); - Assert.Equal(this.Key2, results2[1].Record.Key); - } - finally - { - // Cleanup - await sut.DeleteCollectionAsync(); - } - } - - private async Task CreateCollectionAndAddDataAsync(IVectorStoreRecordCollection> sut, ReadOnlyMemory vector) - { - await sut.CreateCollectionIfNotExistsAsync(); - await Task.Delay(this.DelayAfterIndexCreateInMilliseconds); - - var record1 = new KeyWithVectorAndStringRecord - { - Key = this.Key1, - Text = "Apples are a healthy and nourishing snack", - Vector = vector, - Code = 1 - }; - var record2 = new KeyWithVectorAndStringRecord - { - Key = this.Key2, - Text = "Oranges are tangy and contain vitamin c", - Vector = vector, - Code = 2 - }; - var record3 = new KeyWithVectorAndStringRecord - { - Key = this.Key3, - Text = "Grapes are healthy, sweet and juicy", - Vector = vector, - Code = 3 - }; - - await sut.UpsertBatchAsync([record1, record2, record3]).ToListAsync(); - await Task.Delay(this.DelayAfterUploadInMilliseconds); - } - - private async Task CreateCollectionAndAddDataAsync(IVectorStoreRecordCollection> sut, ReadOnlyMemory vector) - { - await sut.CreateCollectionIfNotExistsAsync(); - await Task.Delay(this.DelayAfterIndexCreateInMilliseconds); - - var record1 = new MultiSearchStringRecord - { - Key = this.Key1, - Text1 = "Apples", - Text2 = "Oranges", - Vector = vector - }; - var record2 = new MultiSearchStringRecord - { - Key = this.Key2, - Text1 = "Oranges", - Text2 = "Apples", - Vector = vector - }; - - await sut.UpsertBatchAsync([record1, record2]).ToListAsync(); - await Task.Delay(this.DelayAfterUploadInMilliseconds); - } - - private VectorStoreRecordDefinition KeyWithVectorAndStringRecordDefinition => new() - { - Properties = new List() - { - new VectorStoreRecordKeyProperty("Key", typeof(TKey)), - new VectorStoreRecordDataProperty("Text", typeof(string)) { IsFullTextSearchable = true }, - new VectorStoreRecordDataProperty("Code", typeof(int)) { IsFilterable = true }, - new VectorStoreRecordVectorProperty("Vector", typeof(ReadOnlyMemory)) { Dimensions = 4, IndexKind = this.IndexKind }, - } - }; - - private sealed class KeyWithVectorAndStringRecord - { - public TRecordKey Key { get; set; } = default!; - - public string Text { get; set; } = string.Empty; - - public int Code { get; set; } - - public ReadOnlyMemory Vector { get; set; } - } - - private VectorStoreRecordDefinition MultiSearchStringRecordDefinition => new() - { - Properties = new List() - { - new VectorStoreRecordKeyProperty("Key", typeof(TKey)), - new VectorStoreRecordDataProperty("Text1", typeof(string)) { IsFullTextSearchable = true }, - new VectorStoreRecordDataProperty("Text2", typeof(string)) { IsFullTextSearchable = true }, - new VectorStoreRecordVectorProperty("Vector", typeof(ReadOnlyMemory)) { Dimensions = 4, IndexKind = this.IndexKind }, - } - }; - - private sealed class MultiSearchStringRecord - { - public TRecordKey Key { get; set; } = default!; - - public string Text1 { get; set; } = string.Empty; - - public string Text2 { get; set; } = string.Empty; - - public ReadOnlyMemory Vector { get; set; } - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBKeywordVectorizedHybridSearchTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBKeywordVectorizedHybridSearchTests.cs deleted file mode 100644 index c6e326a14f76..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/Memory/MongoDB/MongoDBKeywordVectorizedHybridSearchTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.VectorData; -using Microsoft.SemanticKernel.Connectors.MongoDB; -using SemanticKernel.IntegrationTests.Connectors.Memory; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.MongoDB; - -/// -/// Inherits common integration tests that should pass for any . -/// -[Collection("MongoDBVectorStoreCollection")] -public class MongoDBKeywordVectorizedHybridSearchTests(MongoDBVectorStoreFixture fixture) : BaseKeywordVectorizedHybridSearchTests -{ - protected override string Key1 => "1"; - protected override string Key2 => "2"; - protected override string Key3 => "3"; - protected override string Key4 => "4"; - protected override int DelayAfterUploadInMilliseconds => 1000; - - protected override IVectorStoreRecordCollection GetTargetRecordCollection(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - return new MongoDBVectorStoreRecordCollection(fixture.MongoDatabase, recordCollectionName, new() - { - VectorStoreRecordDefinition = vectorStoreRecordDefinition - }); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantNamedVectorsKeywordVectorizedHybridSearchTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantNamedVectorsKeywordVectorizedHybridSearchTests.cs deleted file mode 100644 index 20fd1097b957..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantNamedVectorsKeywordVectorizedHybridSearchTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.VectorData; -using Microsoft.SemanticKernel.Connectors.Qdrant; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.Memory.Qdrant; - -/// -/// Inherits common integration tests that should pass for any . -/// -[Collection("QdrantVectorStoreCollection")] -public class QdrantNamedVectorsKeywordVectorizedHybridSearchTests(QdrantVectorStoreFixture fixture) : BaseKeywordVectorizedHybridSearchTests -{ - protected override ulong Key1 => 1; - protected override ulong Key2 => 2; - protected override ulong Key3 => 3; - protected override ulong Key4 => 4; - - protected override IVectorStoreRecordCollection GetTargetRecordCollection(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - return new QdrantVectorStoreRecordCollection(fixture.QdrantClient, recordCollectionName, new() - { - HasNamedVectors = true, - VectorStoreRecordDefinition = vectorStoreRecordDefinition - }); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantSingleVectorKeywordVectorizedHybridSearchTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantSingleVectorKeywordVectorizedHybridSearchTests.cs deleted file mode 100644 index 4579e47b56ad..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Qdrant/QdrantSingleVectorKeywordVectorizedHybridSearchTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.VectorData; -using Microsoft.SemanticKernel.Connectors.Qdrant; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.Memory.Qdrant; - -/// -/// Inherits common integration tests that should pass for any . -/// -[Collection("QdrantVectorStoreCollection")] -public class QdrantSingleVectorKeywordVectorizedHybridSearchTests(QdrantVectorStoreFixture fixture) : BaseKeywordVectorizedHybridSearchTests -{ - protected override ulong Key1 => 1; - protected override ulong Key2 => 2; - protected override ulong Key3 => 3; - protected override ulong Key4 => 4; - - protected override IVectorStoreRecordCollection GetTargetRecordCollection(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - return new QdrantVectorStoreRecordCollection(fixture.QdrantClient, recordCollectionName, new() - { - HasNamedVectors = false, - VectorStoreRecordDefinition = vectorStoreRecordDefinition - }); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateKeywordVectorizedHybridSearchTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateKeywordVectorizedHybridSearchTests.cs deleted file mode 100644 index 2ec29b428053..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateKeywordVectorizedHybridSearchTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Extensions.VectorData; -using Microsoft.SemanticKernel.Connectors.Weaviate; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.Memory.Weaviate; - -/// -/// Inherits common integration tests that should pass for any . -/// -/// Weaviate setup and teardown. -[Collection("WeaviateVectorStoreCollection")] -public class WeaviateKeywordVectorizedHybridSearchTests(WeaviateVectorStoreFixture fixture) : BaseKeywordVectorizedHybridSearchTests -{ - protected override Guid Key1 => new("11111111-1111-1111-1111-111111111111"); - protected override Guid Key2 => new("22222222-2222-2222-2222-222222222222"); - protected override Guid Key3 => new("33333333-3333-3333-3333-333333333333"); - protected override Guid Key4 => new("44444444-4444-4444-4444-444444444444"); - protected override int DelayAfterUploadInMilliseconds => 1000; - - protected override IVectorStoreRecordCollection GetTargetRecordCollection(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - // Weaviate collection names must start with an upper case letter. - var recordCollectionNameChars = recordCollectionName.ToCharArray(); - recordCollectionNameChars[0] = char.ToUpperInvariant(recordCollectionNameChars[0]); - - return new WeaviateVectorStoreRecordCollection(fixture.HttpClient!, new string(recordCollectionNameChars), new() - { - VectorStoreRecordDefinition = vectorStoreRecordDefinition - }); - } -} diff --git a/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/HybridSearch/AzureAISearchKeywordVectorizedHybridSearchTests.cs b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/HybridSearch/AzureAISearchKeywordVectorizedHybridSearchTests.cs new file mode 100644 index 000000000000..3860489b9471 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/AzureAISearchIntegrationTests/HybridSearch/AzureAISearchKeywordVectorizedHybridSearchTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.RegularExpressions; +using AzureAISearchIntegrationTests.Support; +using Microsoft.Extensions.VectorData; +using VectorDataSpecificationTests.HybridSearch; +using VectorDataSpecificationTests.Support; +using Xunit; + +namespace AzureAISearchIntegrationTests.HybridSearch; + +/// +/// Inherits common integration tests that should pass for any . +/// +public class AzureAISearchKeywordVectorizedHybridSearchTests( + AzureAISearchKeywordVectorizedHybridSearchTests.VectorAndStringFixture vectorAndStringFixture, + AzureAISearchKeywordVectorizedHybridSearchTests.MultiTextFixture multiTextFixture) + : KeywordVectorizedHybridSearchComplianceTests(vectorAndStringFixture, multiTextFixture), + IClassFixture, + IClassFixture +{ +#pragma warning disable CA1308 // Normalize strings to uppercase + private static readonly string _testIndexPostfix = new Regex("[^a-zA-Z0-9]").Replace(Environment.MachineName.ToLowerInvariant(), ""); +#pragma warning restore CA1308 // Normalize strings to uppercase + + public new class VectorAndStringFixture : KeywordVectorizedHybridSearchComplianceTests.VectorAndStringFixture + { + public override TestStore TestStore => AzureAISearchTestStore.Instance; + + // Azure AI search only supports lowercase letters, digits or dashes. + protected override string CollectionName => "vecstring-hybrid-search-" + _testIndexPostfix; + } + + public new class MultiTextFixture : KeywordVectorizedHybridSearchComplianceTests.MultiTextFixture + { + public override TestStore TestStore => AzureAISearchTestStore.Instance; + + // Azure AI search only supports lowercase letters, digits or dashes. + protected override string CollectionName => "multitext-hybrid-search-" + _testIndexPostfix; + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj index 715b01505df7..7c60f6d74ebd 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj +++ b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/CosmosNoSQLIntegrationTests.csproj @@ -7,23 +7,25 @@ true false CosmosNoSQLIntegrationTests + b7762d10-e29b-4bb1-8b74-b6d69a667dd4 - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + - - + + diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/HybridSearch/CosmosNoSQLKeywordVectorizedHybridSearchTests.cs b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/HybridSearch/CosmosNoSQLKeywordVectorizedHybridSearchTests.cs new file mode 100644 index 000000000000..24935b4ffc2d --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/HybridSearch/CosmosNoSQLKeywordVectorizedHybridSearchTests.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using CosmosNoSQLIntegrationTests.Support; +using VectorDataSpecificationTests.HybridSearch; +using VectorDataSpecificationTests.Support; +using Xunit; + +namespace CosmosNoSQLIntegrationTests.HybridSearch; + +public class CosmosNoSQLKeywordVectorizedHybridSearchTests( + CosmosNoSQLKeywordVectorizedHybridSearchTests.VectorAndStringFixture vectorAndStringFixture, + CosmosNoSQLKeywordVectorizedHybridSearchTests.MultiTextFixture multiTextFixture) + : KeywordVectorizedHybridSearchComplianceTests(vectorAndStringFixture, multiTextFixture), + IClassFixture, + IClassFixture +{ + public new class VectorAndStringFixture : KeywordVectorizedHybridSearchComplianceTests.VectorAndStringFixture + { + public override TestStore TestStore => CosmosNoSqlTestStore.Instance; + } + + public new class MultiTextFixture : KeywordVectorizedHybridSearchComplianceTests.MultiTextFixture + { + public override TestStore TestStore => CosmosNoSqlTestStore.Instance; + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLTestEnvironment.cs b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLTestEnvironment.cs index bd2848a2cb8f..6f9db1179ca5 100644 --- a/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLTestEnvironment.cs +++ b/dotnet/src/VectorDataIntegrationTests/CosmosNoSQLIntegrationTests/Support/CosmosNoSQLTestEnvironment.cs @@ -18,6 +18,7 @@ static CosmosNoSQLTestEnvironment() .AddJsonFile(path: "testsettings.json", optional: true) .AddJsonFile(path: "testsettings.development.json", optional: true) .AddEnvironmentVariables() + .AddUserSecrets() .Build(); ConnectionString = configuration["AzureCosmosDBNoSQL:ConnectionString"]; diff --git a/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/HybridSearch/MongoDBKeywordVectorizedHybridSearchTests.cs b/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/HybridSearch/MongoDBKeywordVectorizedHybridSearchTests.cs new file mode 100644 index 000000000000..7e8259442a36 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/MongoDBIntegrationTests/HybridSearch/MongoDBKeywordVectorizedHybridSearchTests.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using MongoDBIntegrationTests.Support; +using VectorDataSpecificationTests.HybridSearch; +using VectorDataSpecificationTests.Support; +using Xunit; + +namespace MongoDBIntegrationTests.HybridSearch; + +public class MongoDBKeywordVectorizedHybridSearchTests( + MongoDBKeywordVectorizedHybridSearchTests.VectorAndStringFixture vectorAndStringFixture, + MongoDBKeywordVectorizedHybridSearchTests.MultiTextFixture multiTextFixture) + : KeywordVectorizedHybridSearchComplianceTests(vectorAndStringFixture, multiTextFixture), + IClassFixture, + IClassFixture +{ + public new class VectorAndStringFixture : KeywordVectorizedHybridSearchComplianceTests.VectorAndStringFixture + { + public override TestStore TestStore => MongoDBTestStore.Instance; + } + + public new class MultiTextFixture : KeywordVectorizedHybridSearchComplianceTests.MultiTextFixture + { + public override TestStore TestStore => MongoDBTestStore.Instance; + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/HybridSearch/QdrantKeywordVectorizedHybridSearchTests_NamedVectors.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/HybridSearch/QdrantKeywordVectorizedHybridSearchTests_NamedVectors.cs new file mode 100644 index 000000000000..86a878167626 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/HybridSearch/QdrantKeywordVectorizedHybridSearchTests_NamedVectors.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using QdrantIntegrationTests.Support; +using VectorDataSpecificationTests.HybridSearch; +using VectorDataSpecificationTests.Support; +using Xunit; + +namespace QdrantIntegrationTests.HybridSearch; + +public class QdrantKeywordVectorizedHybridSearchTests_NamedVectors( + QdrantKeywordVectorizedHybridSearchTests_NamedVectors.VectorAndStringFixture vectorAndStringFixture, + QdrantKeywordVectorizedHybridSearchTests_NamedVectors.MultiTextFixture multiTextFixture) + : KeywordVectorizedHybridSearchComplianceTests(vectorAndStringFixture, multiTextFixture), + IClassFixture, + IClassFixture +{ + public new class VectorAndStringFixture : KeywordVectorizedHybridSearchComplianceTests.VectorAndStringFixture + { + public override TestStore TestStore => QdrantTestStore.NamedVectorsInstance; + + // Qdrant doesn't support the default Flat index kind + protected override string IndexKind => Microsoft.Extensions.VectorData.IndexKind.Hnsw; + } + + public new class MultiTextFixture : KeywordVectorizedHybridSearchComplianceTests.MultiTextFixture + { + public override TestStore TestStore => QdrantTestStore.NamedVectorsInstance; + + // Qdrant doesn't support the default Flat index kind + protected override string IndexKind => Microsoft.Extensions.VectorData.IndexKind.Hnsw; + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/HybridSearch/QdrantKeywordVectorizedHybridSearchTests_UnnamedVectors.cs b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/HybridSearch/QdrantKeywordVectorizedHybridSearchTests_UnnamedVectors.cs new file mode 100644 index 000000000000..e9492cd7ef21 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/QdrantIntegrationTests/HybridSearch/QdrantKeywordVectorizedHybridSearchTests_UnnamedVectors.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using QdrantIntegrationTests.Support; +using VectorDataSpecificationTests.HybridSearch; +using VectorDataSpecificationTests.Support; +using Xunit; + +namespace QdrantIntegrationTests.HybridSearch; + +public class QdrantKeywordVectorizedHybridSearchTests_UnnamedVectors( + QdrantKeywordVectorizedHybridSearchTests_UnnamedVectors.VectorAndStringFixture vectorAndStringFixture, + QdrantKeywordVectorizedHybridSearchTests_UnnamedVectors.MultiTextFixture multiTextFixture) + : KeywordVectorizedHybridSearchComplianceTests(vectorAndStringFixture, multiTextFixture), + IClassFixture, + IClassFixture +{ + public new class VectorAndStringFixture : KeywordVectorizedHybridSearchComplianceTests.VectorAndStringFixture + { + public override TestStore TestStore => QdrantTestStore.UnnamedVectorInstance; + + // Qdrant doesn't support the default Flat index kind + protected override string IndexKind => Microsoft.Extensions.VectorData.IndexKind.Hnsw; + } + + public new class MultiTextFixture : KeywordVectorizedHybridSearchComplianceTests.MultiTextFixture + { + public override TestStore TestStore => QdrantTestStore.UnnamedVectorInstance; + + // Qdrant doesn't support the default Flat index kind + protected override string IndexKind => Microsoft.Extensions.VectorData.IndexKind.Hnsw; + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/HybridSearch/KeywordVectorizedHybridSearchComplianceTests.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/HybridSearch/KeywordVectorizedHybridSearchComplianceTests.cs new file mode 100644 index 000000000000..c25bb065ba74 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/HybridSearch/KeywordVectorizedHybridSearchComplianceTests.cs @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.VectorData; +using VectorDataSpecificationTests.Support; +using VectorDataSpecificationTests.Xunit; +using Xunit; + +namespace VectorDataSpecificationTests.HybridSearch; + +/// +/// Base class for common integration tests that should pass for any . +/// +/// The type of key to use with the record collection. +public abstract class KeywordVectorizedHybridSearchComplianceTests( + KeywordVectorizedHybridSearchComplianceTests.VectorAndStringFixture vectorAndStringFixture, + KeywordVectorizedHybridSearchComplianceTests.MultiTextFixture multiTextFixture) + where TKey : notnull +{ + protected virtual int DelayAfterIndexCreateInMilliseconds { get; } = 0; + + [ConditionalFact] + public async Task SearchShouldReturnExpectedResultsAsync() + { + // Arrange + var hybridSearch = vectorAndStringFixture.Collection as IKeywordHybridSearch>; + + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + // Act + // All records have the same vector, but the third contains Grapes, so searching for + // Grapes should return the third record first. + var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["Grapes"]); + + // Assert + var results = await searchResult.Results.ToListAsync(); + Assert.Equal(3, results.Count); + + Assert.Equal(3, results[0].Record.Code); + } + + [ConditionalFact] + public async Task SearchWithFilterShouldReturnExpectedResultsAsync() + { + // Arrange + var hybridSearch = vectorAndStringFixture.Collection as IKeywordHybridSearch>; + + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + // Act + // All records have the same vector, but the second contains Oranges, however + // adding the filter should limit the results to only the first. +#pragma warning disable CS0618 // Type or member is obsolete + var options = new HybridSearchOptions> + { + OldFilter = new VectorSearchFilter().EqualTo("Code", 1) + }; +#pragma warning restore CS0618 // Type or member is obsolete + var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["Oranges"], options); + + // Assert + var results = await searchResult.Results.ToListAsync(); + Assert.Single(results); + + Assert.Equal(1, results[0].Record.Code); + } + + [ConditionalFact] + public async Task SearchWithTopShouldReturnExpectedResultsAsync() + { + // Arrange + var hybridSearch = vectorAndStringFixture.Collection as IKeywordHybridSearch>; + + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + // Act + // All records have the same vector, but the second contains Oranges, so the + // second should be returned first. + var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["Oranges"], new() { Top = 1 }); + + // Assert + var results = await searchResult.Results.ToListAsync(); + Assert.Single(results); + + Assert.Equal(2, results[0].Record.Code); + } + + [ConditionalFact] + public async Task SearchWithSkipShouldReturnExpectedResultsAsync() + { + // Arrange + var hybridSearch = vectorAndStringFixture.Collection as IKeywordHybridSearch>; + + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + // Act + // All records have the same vector, but the first and third contain healthy, + // so when skipping the first two results, we should get the second record. + var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["healthy"], new() { Skip = 2 }); + + // Assert + var results = await searchResult.Results.ToListAsync(); + Assert.Single(results); + + Assert.Equal(2, results[0].Record.Code); + } + + [ConditionalFact] + public async Task SearchWithMultipleKeywordsShouldRankMatchedKeywordsHigherAsync() + { + // Arrange + var hybridSearch = vectorAndStringFixture.Collection as IKeywordHybridSearch>; + + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + // Act + var searchResult = await hybridSearch!.HybridSearchAsync(vector, ["tangy", "nourishing"]); + + // Assert + var results = await searchResult.Results.ToListAsync(); + Assert.Equal(3, results.Count); + + Assert.True(results[0].Record.Code.Equals(1) || results[0].Record.Code.Equals(2)); + Assert.True(results[1].Record.Code.Equals(1) || results[1].Record.Code.Equals(2)); + Assert.Equal(3, results[2].Record.Code); + } + + [ConditionalFact] + public async Task SearchWithMultiTextRecordSearchesRequestedFieldAsync() + { + // Arrange + var hybridSearch = multiTextFixture.Collection as IKeywordHybridSearch>; + + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + // Act + var searchResult1 = await hybridSearch!.HybridSearchAsync(vector, ["Apples"], new() { AdditionalProperty = r => r.Text2 }); + var searchResult2 = await hybridSearch!.HybridSearchAsync(vector, ["Oranges"], new() { AdditionalProperty = r => r.Text2 }); + + // Assert + var results1 = await searchResult1.Results.ToListAsync(); + Assert.Equal(2, results1.Count); + + Assert.Equal(2, results1[0].Record.Code); + Assert.Equal(1, results1[1].Record.Code); + + var results2 = await searchResult2.Results.ToListAsync(); + Assert.Equal(2, results2.Count); + + Assert.Equal(1, results2[0].Record.Code); + Assert.Equal(2, results2[1].Record.Code); + } + + public sealed class VectorAndStringRecord + { + public TRecordKey Key { get; set; } = default!; + + public string Text { get; set; } = string.Empty; + + public int Code { get; set; } + + public ReadOnlyMemory Vector { get; set; } + } + + public sealed class MultiTextStringRecord + { + public TRecordKey Key { get; set; } = default!; + + public string Text1 { get; set; } = string.Empty; + + public string Text2 { get; set; } = string.Empty; + + public int Code { get; set; } + + public ReadOnlyMemory Vector { get; set; } + } + + public abstract class VectorAndStringFixture : VectorStoreCollectionFixture> + { + protected override string CollectionName => "KeywordHybridSearch" + this.GetUniqueCollectionName(); + + protected override VectorStoreRecordDefinition GetRecordDefinition() + => new() + { + Properties = new List() + { + new VectorStoreRecordKeyProperty("Key", typeof(TKey)), + new VectorStoreRecordDataProperty("Text", typeof(string)) { IsFullTextSearchable = true }, + new VectorStoreRecordDataProperty("Code", typeof(int)) { IsFilterable = true }, + new VectorStoreRecordVectorProperty("Vector", typeof(ReadOnlyMemory)) { Dimensions = 4, IndexKind = this.IndexKind }, + } + }; + + protected override List> BuildTestData() + { + // All records have the same vector - this fixture is about testing the full text search portion of hybrid search + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + return + [ + new() + { + Key = this.GenerateNextKey(), + Text = "Apples are a healthy and nourishing snack", + Vector = vector, + Code = 1 + }, + new() + { + Key = this.GenerateNextKey(), + Text = "Oranges are tangy and contain vitamin c", + Vector = vector, + Code = 2 + }, + new() + { + Key = this.GenerateNextKey(), + Text = "Grapes are healthy, sweet and juicy", + Vector = vector, + Code = 3 + } + ]; + } + + // In some databases (Azure AI Search), the data shows up but the filtering index isn't yet updated, + // so filtered searches show empty results. Add a filter to the seed data check below. + protected override Task WaitForDataAsync() + => this.TestStore.WaitForDataAsync(this.Collection, recordCount: this.TestData.Count, vectorSize: 4); + } + + public abstract class MultiTextFixture : VectorStoreCollectionFixture> + { + protected override string CollectionName => "KeywordHybridSearch" + this.GetUniqueCollectionName(); + + protected override VectorStoreRecordDefinition GetRecordDefinition() + => new() + { + Properties = new List() + { + new VectorStoreRecordKeyProperty("Key", typeof(TKey)), + new VectorStoreRecordDataProperty("Text1", typeof(string)) { IsFullTextSearchable = true }, + new VectorStoreRecordDataProperty("Text2", typeof(string)) { IsFullTextSearchable = true }, + new VectorStoreRecordDataProperty("Code", typeof(int)) { IsFilterable = true }, + new VectorStoreRecordVectorProperty("Vector", typeof(ReadOnlyMemory)) { Dimensions = 4, IndexKind = this.IndexKind }, + } + }; + + protected override List> BuildTestData() + { + // All records have the same vector - this fixture is about testing the full text search portion of hybrid search + var vector = new ReadOnlyMemory([1, 0, 0, 0]); + + return + [ + new() + { + Key = this.GenerateNextKey(), + Text1 = "Apples", + Text2 = "Oranges", + Code = 1, + Vector = vector + }, + new() + { + Key = this.GenerateNextKey(), + Text1 = "Oranges", + Text2 = "Apples", + Code = 2, + Vector = vector + } + ]; + } + + // In some databases (Azure AI Search), the data shows up but the filtering index isn't yet updated, + // so filtered searches show empty results. Add a filter to the seed data check below. + protected override Task WaitForDataAsync() + => this.TestStore.WaitForDataAsync(this.Collection, recordCount: this.TestData.Count, vectorSize: 4); + } +} diff --git a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/TestStore.cs b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/TestStore.cs index bff2f583633e..cd92e070abf8 100644 --- a/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/TestStore.cs +++ b/dotnet/src/VectorDataIntegrationTests/VectorDataIntegrationTests/Support/TestStore.cs @@ -72,13 +72,20 @@ _ when typeof(TKey) == typeof(Guid) => (TKey)(object)new Guid($"00000000-0000-00 public virtual async Task WaitForDataAsync( IVectorStoreRecordCollection collection, int recordCount, - Expression>? filter = null) + Expression>? filter = null, + int vectorSize = 3) where TKey : notnull { + var vector = new float[vectorSize]; + for (var i = 0; i < vectorSize; i++) + { + vector[i] = 1.0f; + } + for (var i = 0; i < 20; i++) { var results = await collection.VectorizedSearchAsync( - new ReadOnlyMemory([1, 2, 3]), + new ReadOnlyMemory(vector), new() { Top = recordCount, diff --git a/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/HybridSearch/WeaviateKeywordVectorizedHybridSearchTests.cs b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/HybridSearch/WeaviateKeywordVectorizedHybridSearchTests.cs new file mode 100644 index 000000000000..30d6bc0516f5 --- /dev/null +++ b/dotnet/src/VectorDataIntegrationTests/WeaviateIntegrationTests/HybridSearch/WeaviateKeywordVectorizedHybridSearchTests.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using VectorDataSpecificationTests.HybridSearch; +using VectorDataSpecificationTests.Support; +using WeaviateIntegrationTests.Support; +using Xunit; + +namespace WeaviateIntegrationTests.HybridSearch; + +public class WeaviateKeywordVectorizedHybridSearchTests( + WeaviateKeywordVectorizedHybridSearchTests.VectorAndStringFixture vectorAndStringFixture, + WeaviateKeywordVectorizedHybridSearchTests.MultiTextFixture multiTextFixture) + : KeywordVectorizedHybridSearchComplianceTests(vectorAndStringFixture, multiTextFixture), + IClassFixture, + IClassFixture +{ + public new class VectorAndStringFixture : KeywordVectorizedHybridSearchComplianceTests.VectorAndStringFixture + { + public override TestStore TestStore => WeaviateTestStore.Instance; + + protected override string DistanceFunction => Microsoft.Extensions.VectorData.DistanceFunction.CosineDistance; + + protected override string CollectionName => "VectorAndStringHybridSearch"; + } + + public new class MultiTextFixture : KeywordVectorizedHybridSearchComplianceTests.MultiTextFixture + { + public override TestStore TestStore => WeaviateTestStore.Instance; + + protected override string DistanceFunction => Microsoft.Extensions.VectorData.DistanceFunction.CosineDistance; + + protected override string CollectionName => "MultiTextHybridSearch"; + } +}