From ba26a2887c22240723f9cf5f1b4464d605decf2b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Oct 2026 12:10:52 +0200 Subject: [PATCH] Guard hybrid search against IncludeVectors with embedding generation Qdrant, Weaviate and Azure AI Search hybrid search (and Azure AI Search vector search) lacked the check that SearchAsync performs in other providers. Generated vectors can't be mapped back onto the source property, so throw NotSupportedException instead of failing during deserialization. Bump the affected provider versions to 1.0.1 and add unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a00fadae-dd58-4131-8f39-3e1a3b6bc005 --- MEVD/src/AzureAISearch/AzureAISearch.csproj | 2 +- .../AzureAISearch/AzureAISearchCollection.cs | 10 +++ MEVD/src/Qdrant/Qdrant.csproj | 2 +- MEVD/src/Qdrant/QdrantCollection.cs | 5 ++ MEVD/src/Weaviate/Weaviate.csproj | 2 +- MEVD/src/Weaviate/WeaviateCollection.cs | 5 ++ .../AzureAISearch.UnitTests.csproj | 1 + ...rchHybridSearchEmbeddingGenerationTests.cs | 50 +++++++++++++++ .../Qdrant.UnitTests/Qdrant.UnitTests.csproj | 2 + ...antHybridSearchEmbeddingGenerationTests.cs | 48 ++++++++++++++ .../Weaviate.UnitTests.csproj | 2 + ...ateHybridSearchEmbeddingGenerationTests.cs | 63 +++++++++++++++++++ 12 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 MEVD/test/AzureAISearch.UnitTests/AzureAISearchHybridSearchEmbeddingGenerationTests.cs create mode 100644 MEVD/test/Qdrant.UnitTests/QdrantHybridSearchEmbeddingGenerationTests.cs create mode 100644 MEVD/test/Weaviate.UnitTests/WeaviateHybridSearchEmbeddingGenerationTests.cs diff --git a/MEVD/src/AzureAISearch/AzureAISearch.csproj b/MEVD/src/AzureAISearch/AzureAISearch.csproj index 63cf26c..980b870 100644 --- a/MEVD/src/AzureAISearch/AzureAISearch.csproj +++ b/MEVD/src/AzureAISearch/AzureAISearch.csproj @@ -1,6 +1,6 @@  - 1.0.0 + 1.0.1 CommunityToolkit.VectorData.AzureAISearch $(AssemblyName) net10.0;net8.0;netstandard2.0;net462 diff --git a/MEVD/src/AzureAISearch/AzureAISearchCollection.cs b/MEVD/src/AzureAISearch/AzureAISearchCollection.cs index e063aea..f458528 100644 --- a/MEVD/src/AzureAISearch/AzureAISearchCollection.cs +++ b/MEVD/src/AzureAISearch/AzureAISearchCollection.cs @@ -389,6 +389,11 @@ public override async IAsyncEnumerable> SearchAsync< Throw.IfLessThan(top, 1); options ??= s_defaultVectorSearchOptions; + if (options.IncludeVectors && _model.EmbeddingGenerationRequired) + { + throw new NotSupportedException(VectorDataStrings.IncludeVectorsNotSupportedWithEmbeddingGeneration); + } + var vectorProperty = _model.GetVectorPropertyOrSingle(options); var floatVector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false); @@ -429,6 +434,11 @@ public async IAsyncEnumerable> HybridSearchAsync(new() { VectorProperty = options.VectorProperty }); var floatVector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false); diff --git a/MEVD/src/Qdrant/Qdrant.csproj b/MEVD/src/Qdrant/Qdrant.csproj index 02fb8d4..0e61e28 100644 --- a/MEVD/src/Qdrant/Qdrant.csproj +++ b/MEVD/src/Qdrant/Qdrant.csproj @@ -1,7 +1,7 @@  - 1.0.0 + 1.0.1 CommunityToolkit.VectorData.Qdrant $(AssemblyName) net10.0;net8.0;netstandard2.0;net462 diff --git a/MEVD/src/Qdrant/QdrantCollection.cs b/MEVD/src/Qdrant/QdrantCollection.cs index f1aa9aa..0501341 100644 --- a/MEVD/src/Qdrant/QdrantCollection.cs +++ b/MEVD/src/Qdrant/QdrantCollection.cs @@ -669,6 +669,11 @@ public async IAsyncEnumerable> HybridSearchAsync(new() { VectorProperty = options.VectorProperty }); var vectorArray = await GetSearchVectorArrayAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false); var textDataProperty = _model.GetFullTextDataPropertyOrSingle(options.AdditionalProperty); diff --git a/MEVD/src/Weaviate/Weaviate.csproj b/MEVD/src/Weaviate/Weaviate.csproj index 1c8dffc..ef0fea6 100644 --- a/MEVD/src/Weaviate/Weaviate.csproj +++ b/MEVD/src/Weaviate/Weaviate.csproj @@ -2,7 +2,7 @@ - 1.0.0 + 1.0.1 CommunityToolkit.VectorData.Weaviate $(AssemblyName) net10.0;net8.0;netstandard2.0;net462 diff --git a/MEVD/src/Weaviate/WeaviateCollection.cs b/MEVD/src/Weaviate/WeaviateCollection.cs index 4599cd4..faa069d 100644 --- a/MEVD/src/Weaviate/WeaviateCollection.cs +++ b/MEVD/src/Weaviate/WeaviateCollection.cs @@ -410,6 +410,11 @@ public async IAsyncEnumerable> HybridSearchAsync(new() { VectorProperty = options.VectorProperty }); var vector = await GetSearchVectorAsync(searchValue, vectorProperty, cancellationToken).ConfigureAwait(false); var textDataProperty = _model.GetFullTextDataPropertyOrSingle(options.AdditionalProperty); diff --git a/MEVD/test/AzureAISearch.UnitTests/AzureAISearch.UnitTests.csproj b/MEVD/test/AzureAISearch.UnitTests/AzureAISearch.UnitTests.csproj index 61466ef..14107ae 100644 --- a/MEVD/test/AzureAISearch.UnitTests/AzureAISearch.UnitTests.csproj +++ b/MEVD/test/AzureAISearch.UnitTests/AzureAISearch.UnitTests.csproj @@ -23,6 +23,7 @@ + diff --git a/MEVD/test/AzureAISearch.UnitTests/AzureAISearchHybridSearchEmbeddingGenerationTests.cs b/MEVD/test/AzureAISearch.UnitTests/AzureAISearchHybridSearchEmbeddingGenerationTests.cs new file mode 100644 index 0000000..5bd1980 --- /dev/null +++ b/MEVD/test/AzureAISearch.UnitTests/AzureAISearchHybridSearchEmbeddingGenerationTests.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Azure.Search.Documents.Indexes; +using CommunityToolkit.VectorData.AzureAISearch; +using CommunityToolkit.VectorData.UnitTests; +using Microsoft.Extensions.VectorData; +using Moq; +using Xunit; + +namespace AzureAISearch.UnitTests; + +public sealed class AzureAISearchHybridSearchEmbeddingGenerationTests +{ + [Fact] + public async Task HybridSearchAsync_WithIncludeVectorsAndEmbeddingGeneration_ThrowsNotSupported() + { + using AzureAISearchCollection collection = CreateCollection(); + + await Assert.ThrowsAsync(async () => + await collection.HybridSearchAsync("search text", ["keyword"], top: 3, new() { IncludeVectors = true }).ToListAsync()); + } + + [Fact] + public async Task SearchAsync_WithIncludeVectorsAndEmbeddingGeneration_ThrowsNotSupported() + { + using AzureAISearchCollection collection = CreateCollection(); + + await Assert.ThrowsAsync(async () => + await collection.SearchAsync("search text", top: 3, new() { IncludeVectors = true }).ToListAsync()); + } + + private static AzureAISearchCollection CreateCollection() + => new(new Mock().Object, "collection", new() { EmbeddingGenerator = new FakeEmbeddingGenerator() }); + + public sealed class Record + { + [VectorStoreKey] + public string Id { get; set; } = string.Empty; + + [VectorStoreData(IsFullTextIndexed = true)] + public string Text { get; set; } = string.Empty; + + [VectorStoreVector(3)] + public string Embedding { get; set; } = string.Empty; + } +} diff --git a/MEVD/test/Qdrant.UnitTests/Qdrant.UnitTests.csproj b/MEVD/test/Qdrant.UnitTests/Qdrant.UnitTests.csproj index 863a56a..fc10072 100644 --- a/MEVD/test/Qdrant.UnitTests/Qdrant.UnitTests.csproj +++ b/MEVD/test/Qdrant.UnitTests/Qdrant.UnitTests.csproj @@ -26,6 +26,8 @@ Link="%(RecursiveDir)%(Filename)%(Extension)"/> + diff --git a/MEVD/test/Qdrant.UnitTests/QdrantHybridSearchEmbeddingGenerationTests.cs b/MEVD/test/Qdrant.UnitTests/QdrantHybridSearchEmbeddingGenerationTests.cs new file mode 100644 index 0000000..66fe5d6 --- /dev/null +++ b/MEVD/test/Qdrant.UnitTests/QdrantHybridSearchEmbeddingGenerationTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.VectorData.UnitTests; +using Microsoft.Extensions.VectorData; +using Moq; +using Xunit; + +namespace CommunityToolkit.VectorData.Qdrant.UnitTests; + +public sealed class QdrantHybridSearchEmbeddingGenerationTests +{ + [Fact] + public async Task HybridSearchAsync_WithIncludeVectorsAndEmbeddingGeneration_ThrowsNotSupported() + { + using QdrantCollection collection = CreateCollection(); + + await Assert.ThrowsAsync(async () => + await collection.HybridSearchAsync("search text", ["keyword"], top: 3, new() { IncludeVectors = true }).ToListAsync()); + } + + [Fact] + public async Task SearchAsync_WithIncludeVectorsAndEmbeddingGeneration_ThrowsNotSupported() + { + using QdrantCollection collection = CreateCollection(); + + await Assert.ThrowsAsync(async () => + await collection.SearchAsync("search text", top: 3, new() { IncludeVectors = true }).ToListAsync()); + } + + private static QdrantCollection CreateCollection() + => new(() => new Mock(MockBehavior.Strict).Object, "collection", new() { EmbeddingGenerator = new FakeEmbeddingGenerator() }); + + public sealed class Record + { + [VectorStoreKey] + public ulong Id { get; set; } + + [VectorStoreData(IsFullTextIndexed = true)] + public string Text { get; set; } = string.Empty; + + [VectorStoreVector(3)] + public string Embedding { get; set; } = string.Empty; + } +} diff --git a/MEVD/test/Weaviate.UnitTests/Weaviate.UnitTests.csproj b/MEVD/test/Weaviate.UnitTests/Weaviate.UnitTests.csproj index b76eeb9..e8b4db2 100644 --- a/MEVD/test/Weaviate.UnitTests/Weaviate.UnitTests.csproj +++ b/MEVD/test/Weaviate.UnitTests/Weaviate.UnitTests.csproj @@ -24,6 +24,8 @@ + diff --git a/MEVD/test/Weaviate.UnitTests/WeaviateHybridSearchEmbeddingGenerationTests.cs b/MEVD/test/Weaviate.UnitTests/WeaviateHybridSearchEmbeddingGenerationTests.cs new file mode 100644 index 0000000..064aa94 --- /dev/null +++ b/MEVD/test/Weaviate.UnitTests/WeaviateHybridSearchEmbeddingGenerationTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using CommunityToolkit.VectorData.UnitTests; +using CommunityToolkit.VectorData.Weaviate; +using Microsoft.Extensions.VectorData; +using Xunit; + +namespace Weaviate.UnitTests; + +public sealed class WeaviateHybridSearchEmbeddingGenerationTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub = new(); + private readonly HttpClient _httpClient; + + public WeaviateHybridSearchEmbeddingGenerationTests() + { + this._httpClient = new(this._messageHandlerStub, false) { BaseAddress = new Uri("http://default-endpoint") }; + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } + + [Fact] + public async Task HybridSearchAsync_WithIncludeVectorsAndEmbeddingGeneration_ThrowsNotSupported() + { + using WeaviateCollection collection = this.CreateCollection(); + + await Assert.ThrowsAsync(async () => + await collection.HybridSearchAsync("search text", ["keyword"], top: 3, new() { IncludeVectors = true }).ToListAsync()); + } + + [Fact] + public async Task SearchAsync_WithIncludeVectorsAndEmbeddingGeneration_ThrowsNotSupported() + { + using WeaviateCollection collection = this.CreateCollection(); + + await Assert.ThrowsAsync(async () => + await collection.SearchAsync("search text", top: 3, new() { IncludeVectors = true }).ToListAsync()); + } + + private WeaviateCollection CreateCollection() + => new(this._httpClient, "Collection", new() { EmbeddingGenerator = new FakeEmbeddingGenerator() }); + + public sealed class Record + { + [VectorStoreKey] + public Guid Id { get; set; } + + [VectorStoreData(IsFullTextIndexed = true)] + public string Text { get; set; } = string.Empty; + + [VectorStoreVector(3)] + public string Embedding { get; set; } = string.Empty; + } +}