From b2f287442a7f2b004d1690c47b3f84d2eeb0db1b Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 14:16:49 +0200 Subject: [PATCH 01/15] Send empty vector index type on Weaviate 1.37.5+ Mirrors weaviate-python-client#2042 for the C# client. The server now applies DEFAULT_VECTOR_INDEX when vectorIndexType is omitted; injecting "hnsw" client-side is preserved for older servers. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Common/ServerVersions.cs | 8 + ...tionsClient.DefaultVectorIndexTypeTests.cs | 207 ++++++++++++++++++ src/Weaviate.Client/CollectionsClient.cs | 26 ++- src/Weaviate.Client/Extensions.cs | 2 +- src/Weaviate.Client/WeaviateClient.cs | 18 ++ 5 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs diff --git a/src/Weaviate.Client.Tests/Common/ServerVersions.cs b/src/Weaviate.Client.Tests/Common/ServerVersions.cs index fd301e3e..112f369c 100644 --- a/src/Weaviate.Client.Tests/Common/ServerVersions.cs +++ b/src/Weaviate.Client.Tests/Common/ServerVersions.cs @@ -10,4 +10,12 @@ internal static class ServerVersions /// Tests will skip if the running server reports a lower version. /// public const string MinSupported = "1.32.0"; + + /// + /// First Weaviate server version that applies a server-side default for + /// vectorIndexType when the client omits it. On this version and later, + /// the C# client must leave an unset VectorIndexType empty rather than + /// injecting "hnsw". + /// + public const string DefaultVectorIndexTypeServerSide = "1.37.5"; } diff --git a/src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs b/src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs new file mode 100644 index 00000000..5a7daae8 --- /dev/null +++ b/src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs @@ -0,0 +1,207 @@ +using System.Text.Json; +using Weaviate.Client.Models; +using Weaviate.Client.Tests.Common; +using Weaviate.Client.Tests.Unit.Mocks; +using Dto = Weaviate.Client.Rest.Dto; + +namespace Weaviate.Client.Tests.Unit; + +/// +/// Tests that mirror weaviate-python-client#2042: starting with Weaviate 1.37.5 the +/// server applies its own default for vectorIndexType when the client omits it. +/// The C# client should leave the field empty on 1.37.5+, but keep injecting +/// "hnsw" on older servers (or when the version is unknown). +/// +public class CollectionsClientDefaultVectorIndexTypeTests +{ + private const string LegacyServerVersion = "1.37.4"; + private const string DefaultIndexServerVersion = + ServerVersions.DefaultVectorIndexTypeServerSide; + + /// + /// Captures the JSON body of the POST to /v1/schema for a Create call. + /// + private static async Task CaptureCreateBody( + string? serverVersion, + CollectionCreateParams config + ) + { + var (client, handler) = MockWeaviateClient.CreateWithMockHandler( + serverVersion: serverVersion + ); + handler.AddJsonResponse(new Dto.Class { Class1 = config.Name }, "/v1/schema"); + + await client.Collections.Create(config, TestContext.Current.CancellationToken); + + Assert.NotNull(handler.LastRequest); + var body = await handler.LastRequest!.GetBodyAsString(); + return JsonDocument.Parse(body).RootElement; + } + + /// + /// Reads the named vector's vectorIndexType, or returns null if the + /// property is absent or explicitly serialized as JSON null. + /// + private static string? GetNamedVectorIndexType(JsonElement root, string vectorName) + { + if (!root.TryGetProperty("vectorConfig", out var vectorConfig)) + return null; + if (!vectorConfig.TryGetProperty(vectorName, out var entry)) + return null; + if (!entry.TryGetProperty("vectorIndexType", out var vit)) + return null; + return vit.ValueKind == JsonValueKind.Null ? null : vit.GetString(); + } + + /// + /// Builds a single named-vector config. When is null + /// the resulting VectorConfig.VectorIndexType is also null. + /// + private static CollectionCreateParams MakeCollection( + string name, + VectorIndexConfig? indexConfig + ) + { + return new CollectionCreateParams + { + Name = name, + Properties = [Property.Text("title")], + VectorConfig = new VectorConfig("default", new Vectorizer.SelfProvided(), indexConfig), + }; + } + + [Fact] + public async Task UserSet_Hnsw_IsEmittedOnLegacyServer() + { + var root = await CaptureCreateBody( + LegacyServerVersion, + MakeCollection("Hnsw_Legacy", new VectorIndex.HNSW()) + ); + + Assert.Equal("hnsw", GetNamedVectorIndexType(root, "default")); + } + + [Fact] + public async Task UserSet_Hnsw_IsEmittedOnNewServer() + { + var root = await CaptureCreateBody( + DefaultIndexServerVersion, + MakeCollection("Hnsw_New", new VectorIndex.HNSW()) + ); + + Assert.Equal("hnsw", GetNamedVectorIndexType(root, "default")); + } + + [Fact] + public async Task UserSet_Flat_IsEmittedOnLegacyServer() + { + var root = await CaptureCreateBody( + LegacyServerVersion, + MakeCollection("Flat_Legacy", new VectorIndex.Flat()) + ); + + Assert.Equal("flat", GetNamedVectorIndexType(root, "default")); + } + + [Fact] + public async Task UserSet_Flat_IsEmittedOnNewServer() + { + var root = await CaptureCreateBody( + DefaultIndexServerVersion, + MakeCollection("Flat_New", new VectorIndex.Flat()) + ); + + Assert.Equal("flat", GetNamedVectorIndexType(root, "default")); + } + + [Fact] + public async Task UnsetIndex_OnLegacyServer_InjectsHnsw() + { + var root = await CaptureCreateBody( + LegacyServerVersion, + MakeCollection("Unset_Legacy", indexConfig: null) + ); + + Assert.Equal("hnsw", GetNamedVectorIndexType(root, "default")); + } + + [Fact] + public async Task UnsetIndex_OnNewServer_IsOmitted() + { + var root = await CaptureCreateBody( + DefaultIndexServerVersion, + MakeCollection("Unset_New", indexConfig: null) + ); + + // On 1.37.5+ the client must leave vectorIndexType empty so the server applies + // its DEFAULT_VECTOR_INDEX. The DTO property is nullable; the serializer may + // omit it or write JSON null — either is acceptable as long as it is not "hnsw". + var value = GetNamedVectorIndexType(root, "default"); + Assert.Null(value); + } + + [Fact] + public async Task UnknownServerVersion_InjectsHnsw() + { + // serverVersion null in MockHelpers means no meta is pre-populated, so the + // client defaults InjectLegacyVectorIndexDefault to true (safe fallback). + var root = await CaptureCreateBody( + serverVersion: null, + MakeCollection("Unknown_Server", indexConfig: null) + ); + + Assert.Equal("hnsw", GetNamedVectorIndexType(root, "default")); + } + + [Fact] + public void InjectLegacyDefaultVectorIndexType_FillsTopLevelEmptyType() + { + var dto = new Dto.Class { Class1 = "Top", VectorIndexType = null }; + + CollectionsClient.InjectLegacyDefaultVectorIndexType(dto); + + Assert.Equal("hnsw", dto.VectorIndexType); + } + + [Fact] + public void InjectLegacyDefaultVectorIndexType_PreservesTopLevelExplicitType() + { + var dto = new Dto.Class { Class1 = "Top", VectorIndexType = "flat" }; + + CollectionsClient.InjectLegacyDefaultVectorIndexType(dto); + + Assert.Equal("flat", dto.VectorIndexType); + } + + [Fact] + public void InjectLegacyDefaultVectorIndexType_FillsNamedEmptyTypes() + { + var dto = new Dto.Class + { + Class1 = "Named", + VectorConfig = new Dictionary + { + ["a"] = new Dto.VectorConfig { VectorIndexType = null }, + ["b"] = new Dto.VectorConfig { VectorIndexType = "" }, + ["c"] = new Dto.VectorConfig { VectorIndexType = "flat" }, + }, + }; + + CollectionsClient.InjectLegacyDefaultVectorIndexType(dto); + + Assert.Equal("hnsw", dto.VectorConfig!["a"].VectorIndexType); + Assert.Equal("hnsw", dto.VectorConfig["b"].VectorIndexType); + Assert.Equal("flat", dto.VectorConfig["c"].VectorIndexType); + } + + [Fact] + public void InjectLegacyDefaultVectorIndexType_HandlesNullVectorConfigDictionary() + { + var dto = new Dto.Class { Class1 = "NoVectorConfig", VectorConfig = null }; + + // Should not throw on a null dictionary. + CollectionsClient.InjectLegacyDefaultVectorIndexType(dto); + + Assert.Equal("hnsw", dto.VectorIndexType); + } +} diff --git a/src/Weaviate.Client/CollectionsClient.cs b/src/Weaviate.Client/CollectionsClient.cs index fb8e6d02..80ecae9c 100644 --- a/src/Weaviate.Client/CollectionsClient.cs +++ b/src/Weaviate.Client/CollectionsClient.cs @@ -198,14 +198,33 @@ public async Task Create( var config = CollectionConfig.FromCollectionCreate(collection); + var dto = config.ToDto(); + if (_client.InjectLegacyVectorIndexDefault) + InjectLegacyDefaultVectorIndexType(dto); var jsonString = JsonSerializer.Serialize( - config.ToDto(), + dto, Rest.WeaviateRestClient.RestJsonSerializerOptions ); return await CreateFromJson(jsonString, cancellationToken); } + /// + /// Injects the legacy "hnsw" default into every empty VectorIndexType + /// slot on the given DTO. Only invoked when the connected server (or unknown server) + /// is older than 1.37.5, the first version that applies a server-side default. + /// Exposed as internal for direct unit testing. + /// + internal static void InjectLegacyDefaultVectorIndexType(Rest.Dto.Class dto) + { + if (dto.VectorConfig is not null) + foreach (var vc in dto.VectorConfig.Values) + if (string.IsNullOrEmpty(vc.VectorIndexType)) + vc.VectorIndexType = "hnsw"; + if (string.IsNullOrEmpty(dto.VectorIndexType)) + dto.VectorIndexType = "hnsw"; + } + private static readonly Version TextAnalyzerMinimumVersion = new(1, 37, 0); private async Task EnsureTextAnalyzerFeaturesSupported(CollectionCreateParams collection) @@ -353,8 +372,11 @@ private static bool PropertyUsesTextAnalyzer(Property property) var config = CollectionConfig.FromCollectionCreate(collection); + var dto = config.ToDto(); + if (_client.InjectLegacyVectorIndexDefault) + InjectLegacyDefaultVectorIndexType(dto); var jsonString = JsonSerializer.Serialize( - config.ToDto(), + dto, Rest.WeaviateRestClient.RestJsonSerializerOptions ); diff --git a/src/Weaviate.Client/Extensions.cs b/src/Weaviate.Client/Extensions.cs index ce6bc22b..9cbf9f0d 100644 --- a/src/Weaviate.Client/Extensions.cs +++ b/src/Weaviate.Client/Extensions.cs @@ -131,7 +131,7 @@ internal static Rest.Dto.Class ToDto(this CollectionConfig collection) VectorIndexConfig = e.VectorIndexConfig is not null ? _objectToDict(VectorIndexSerialization.ToDto(e.VectorIndexConfig)) : new Dictionary(), - VectorIndexType = e.VectorIndexType ?? "hnsw", + VectorIndexType = e.VectorIndexType, Vectorizer = e.Vectorizer?.ToDto(), } ); diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index 711a17d1..2abdf31b 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -64,6 +64,20 @@ public partial class WeaviateClient : IDisposable }; } + /// + /// The first Weaviate server version that applies a server-side default for + /// vectorIndexType when omitted by the client. + /// + private static readonly Version FirstServerDefaultVectorIndexVersion = new(1, 37, 5); + + /// + /// When true, the client injects "hnsw" for any empty VectorIndexType + /// before sending a class definition to the server. Older servers (< 1.37.5) require this + /// because they do not apply a server-side default. On 1.37.5+ this is left to the server. + /// Defaults to true so unit-test mocks and pre-initialization usages stay safe. + /// + internal bool InjectLegacyVectorIndexDefault { get; private set; } = true; + /// /// The meta cache /// @@ -273,6 +287,8 @@ internal WeaviateClient( RestClient = restClient; GrpcClient = grpcClientInstance; _metaCache = meta; + InjectLegacyVectorIndexDefault = + _metaCache is null || _metaCache.Value.Version < FirstServerDefaultVectorIndexVersion; Cluster = new ClusterClient(this); Collections = new CollectionsClient(this); @@ -388,6 +404,8 @@ private async Task PerformInitializationAsync(ClientConfiguration config) ?? new Version(0, 0), Modules = metaDto?.Modules?.ToDictionary() ?? [], }; + InjectLegacyVectorIndexDefault = + _metaCache is null || _metaCache.Value.Version < FirstServerDefaultVectorIndexVersion; // Log warning if connecting to a server older than 1.32.0 var minSupportedVersion = new Version(1, 32, 0); From be6bf21d2a924c901b98f9c9f3a3e9302d4cf792 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 14:53:15 +0200 Subject: [PATCH 02/15] Fix vector index type defaulting bugs surfaced by integration tests Two bugs found while testing against Weaviate 1.37.4 and 1.37.5-e0fe0d5 with DEFAULT_VECTOR_INDEX=flat: 1. Configure.Vector synthesized a fully-populated HNSW index when the user passed no `index` argument. This made every named-vector config land on the wire as `vectorIndexType: "hnsw"` even on 1.37.5, defeating the server-side DEFAULT_VECTOR_INDEX mechanism. Synthesis now only happens when the user provided a quantizer (which implies HNSW). 2. InjectLegacyDefaultVectorIndexType wrote a class-level VectorIndexType even when the class used named vectors (VectorConfig). The server rejects this combination ("creating a class with both a class level vector index and named vectors is forbidden"). Top-level injection is now guarded by VectorConfig being empty, mirroring the Go fix. Adds integration tests using Testcontainers .NET against both server versions to lock the behavior in place, and a focused unit test for the named-vectors + empty-top-level case. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DefaultVectorIndexTypeIntegrationTests.cs | 255 ++++++++++++++++++ ...tionsClient.DefaultVectorIndexTypeTests.cs | 22 ++ .../Weaviate.Client.Tests.csproj | 1 + src/Weaviate.Client.Tests/packages.lock.json | 93 +++++++ src/Weaviate.Client/CollectionsClient.cs | 8 +- src/Weaviate.Client/Configure/Vectorizer.cs | 11 +- 6 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs diff --git a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs new file mode 100644 index 00000000..e3fcfa8f --- /dev/null +++ b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs @@ -0,0 +1,255 @@ +using System.Runtime.InteropServices; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Weaviate.Client.Models; + +namespace Weaviate.Client.Tests.Integration; + +/// +/// End-to-end coverage for the C# client logic introduced in commit +/// Send empty vector index type on Weaviate 1.37.5+: +/// +/// +/// On Weaviate 1.37.4 (legacy server) the client must inject +/// "hnsw" for any omitted vectorIndexType, and the +/// server must store it as "hnsw". +/// +/// +/// On Weaviate 1.37.5+ with DEFAULT_VECTOR_INDEX=flat the client +/// must omit vectorIndexType, and the server must apply its own +/// default — in this case "flat". +/// +/// +/// A user-supplied VectorIndexType (e.g. flat) must be +/// preserved on both server versions — the inject helper is not allowed +/// to mutate explicit values. +/// +/// +/// Unlike , these tests start their own +/// Weaviate container per scenario so the two server versions can be +/// exercised in the same test run, with different env vars. +/// +/// Requires Docker. If Docker is not reachable, the tests will fail rather +/// than skip, so missing infrastructure is loud in CI. +/// +public sealed class DefaultVectorIndexTypeIntegrationTests : IAsyncLifetime +{ + /// + /// Last Weaviate version that does NOT apply a server-side default for + /// vectorIndexType. The client must inject "hnsw" here. + /// + private const string LegacyImage = "cr.weaviate.io/semitechnologies/weaviate:1.37.4"; + + /// + /// First Weaviate build that consumes DEFAULT_VECTOR_INDEX. + /// Two arch-specific tags are published; pick the right one at runtime. + /// + private static string NewImage => + RuntimeInformation.OSArchitecture == Architecture.Arm64 + ? "cr.weaviate.io/semitechnologies/weaviate:1.37.5-e0fe0d5.arm64" + : "cr.weaviate.io/semitechnologies/weaviate:1.37.5-e0fe0d5.amd64"; + + private readonly List _containers = new(); + + public ValueTask InitializeAsync() => ValueTask.CompletedTask; + + public async ValueTask DisposeAsync() + { + foreach (var container in _containers) + { + try + { + await container.DisposeAsync(); + } + catch + { + // Best-effort cleanup; container disposal failures should not mask + // the actual test outcome. + } + } + } + + /// + /// Builds and starts a single-node Weaviate container with anonymous auth + /// enabled, then constructs a pointed at it. + /// The client's _metaCache is populated by BuildAsync. + /// + private async Task StartWeaviateAsync( + string image, + IDictionary? extraEnv = null + ) + { + var builder = new ContainerBuilder(image) + .WithPortBinding(8080, assignRandomHostPort: true) + .WithPortBinding(50051, assignRandomHostPort: true) + .WithEnvironment("QUERY_DEFAULTS_LIMIT", "25") + .WithEnvironment("AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED", "true") + .WithEnvironment("PERSISTENCE_DATA_PATH", "/var/lib/weaviate") + .WithEnvironment("CLUSTER_HOSTNAME", "node1") + .WithEnvironment("DISABLE_TELEMETRY", "true") + .WithCommand("--host", "0.0.0.0", "--port", "8080", "--scheme", "http") + .WithWaitStrategy( + Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(req => + req.ForPort(8080).ForPath("/v1/.well-known/ready") + ) + ); + + if (extraEnv is not null) + { + foreach (var kvp in extraEnv) + { + builder = builder.WithEnvironment(kvp.Key, kvp.Value); + } + } + + var container = builder.Build(); + _containers.Add(container); + + await container.StartAsync(TestContext.Current.CancellationToken); + + var restPort = container.GetMappedPublicPort(8080); + var grpcPort = container.GetMappedPublicPort(50051); + + var client = await WeaviateClientBuilder + .Local(restPort: restPort, grpcPort: grpcPort) + .BuildAsync(); + + // Sanity check: client should know the server version after BuildAsync. + Assert.NotNull(client.WeaviateVersion); + + return client; + } + + /// + /// Creates a collection with a single named vector and no explicit + /// VectorIndexType, then returns the stored type on the server. + /// + private static async Task CreateAndReadIndexTypeAsync( + WeaviateClient client, + string collectionName, + VectorIndexConfig? index + ) + { + // Best-effort cleanup of any leftover collection with this name. + await client.Collections.Delete(collectionName); + + var create = new CollectionCreateParams + { + Name = collectionName, + Properties = [Property.Text("title")], + VectorConfig = Configure.Vector("default", v => v.SelfProvided(), index: index), + }; + + var collection = await client.Collections.Create( + create, + TestContext.Current.CancellationToken + ); + try + { + var config = await collection.Config.Get( + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.NotNull(config.VectorConfig); + Assert.True(config.VectorConfig.ContainsKey("default")); + return config.VectorConfig["default"].VectorIndexType; + } + finally + { + await client.Collections.Delete(collectionName); + } + } + + /// + /// 1.37.4 has no server-side default for vectorIndexType. The C# + /// client must inject "hnsw" in that case, and the server must + /// then store the collection with that type. + /// + [Fact] + public async Task LegacyServer_NoIndexType_GetsHnsw() + { + var client = await StartWeaviateAsync(LegacyImage); + + // Sanity check the version gate — must be strictly older than the + // first version that applies a server-side default. + Assert.True(client.WeaviateVersion! < new Version(1, 37, 5)); + + var stored = await CreateAndReadIndexTypeAsync( + client, + nameof(LegacyServer_NoIndexType_GetsHnsw), + index: null + ); + + Assert.Equal(VectorIndex.HNSW.TypeValue, stored); + } + + /// + /// 1.37.5+ with DEFAULT_VECTOR_INDEX=flat. The client must omit + /// vectorIndexType entirely so the server applies the configured + /// default — verifying both the client-side change AND the new server + /// behaviour. + /// + [Fact] + public async Task NewServer_DefaultVectorIndexFlat_NoIndexType_GetsFlat() + { + var client = await StartWeaviateAsync( + NewImage, + extraEnv: new Dictionary { ["DEFAULT_VECTOR_INDEX"] = "flat" } + ); + + // Sanity check: this build must be at or beyond the version gate. + Assert.True(client.WeaviateVersion! >= new Version(1, 37, 5)); + + var stored = await CreateAndReadIndexTypeAsync( + client, + nameof(NewServer_DefaultVectorIndexFlat_NoIndexType_GetsFlat), + index: null + ); + + Assert.Equal(VectorIndex.Flat.TypeValue, stored); + } + + /// + /// An explicit user choice (flat) must round-trip on the legacy + /// server unchanged — the inject helper must never overwrite a value the + /// user explicitly set. + /// + [Fact] + public async Task LegacyServer_ExplicitFlat_IsPreserved() + { + var client = await StartWeaviateAsync(LegacyImage); + + var stored = await CreateAndReadIndexTypeAsync( + client, + nameof(LegacyServer_ExplicitFlat_IsPreserved), + index: new VectorIndex.Flat() + ); + + Assert.Equal(VectorIndex.Flat.TypeValue, stored); + } + + /// + /// Same explicit-choice guarantee on a 1.37.5+ server with a different + /// server-side default — proves the client doesn't accidentally rely on + /// the server falling back to DEFAULT_VECTOR_INDEX. + /// + [Fact] + public async Task NewServer_ExplicitFlat_IsPreserved() + { + // Use hnsw as the server default so a passing test really is showing + // that the user-supplied "flat" survived end-to-end. + var client = await StartWeaviateAsync( + NewImage, + extraEnv: new Dictionary { ["DEFAULT_VECTOR_INDEX"] = "hnsw" } + ); + + var stored = await CreateAndReadIndexTypeAsync( + client, + nameof(NewServer_ExplicitFlat_IsPreserved), + index: new VectorIndex.Flat() + ); + + Assert.Equal(VectorIndex.Flat.TypeValue, stored); + } +} diff --git a/src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs b/src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs index 5a7daae8..6e8aa45a 100644 --- a/src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs +++ b/src/Weaviate.Client.Tests/Unit/CollectionsClient.DefaultVectorIndexTypeTests.cs @@ -204,4 +204,26 @@ public void InjectLegacyDefaultVectorIndexType_HandlesNullVectorConfigDictionary Assert.Equal("hnsw", dto.VectorIndexType); } + + [Fact] + public void InjectLegacyDefaultVectorIndexType_SkipsTopLevelWhenNamedVectorsPresent() + { + // The server rejects a class that has both a class-level VectorIndexType + // AND named vectors (VectorConfig). The inject helper must not create + // that invalid combination. + var dto = new Dto.Class + { + Class1 = "Mixed", + VectorIndexType = null, + VectorConfig = new Dictionary + { + ["main"] = new Dto.VectorConfig { VectorIndexType = null }, + }, + }; + + CollectionsClient.InjectLegacyDefaultVectorIndexType(dto); + + Assert.Null(dto.VectorIndexType); + Assert.Equal("hnsw", dto.VectorConfig!["main"].VectorIndexType); + } } diff --git a/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj b/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj index c6370270..f4bdaae8 100644 --- a/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj +++ b/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj @@ -37,6 +37,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Weaviate.Client.Tests/packages.lock.json b/src/Weaviate.Client.Tests/packages.lock.json index 62194ad4..01b37606 100644 --- a/src/Weaviate.Client.Tests/packages.lock.json +++ b/src/Weaviate.Client.Tests/packages.lock.json @@ -58,6 +58,19 @@ "resolved": "6.0.3", "contentHash": "hSHiq2m1ky7zUQgTp+/2h1K3lABIQ+GltRixoclHPg/Sc1vnfeS6g/Uy5moOVZKrZJdQiFPFZd6OobBp3tZcFg==" }, + "Testcontainers": { + "type": "Direct", + "requested": "[4.12.0, )", + "resolved": "4.12.0", + "contentHash": "PTZRdG1ZVkFMsFbc3cK/VUaOB5L3l4wYL+OkWAK33/cvgd/5FcmZlQ6NhMAl3PWBqYkpdWmeYmQW9U2OIXqtFA==", + "dependencies": { + "Docker.DotNet.Enhanced": "4.2.0", + "Docker.DotNet.Enhanced.X509": "4.2.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "SSH.NET": "2025.1.0", + "SharpZipLib": "1.4.2" + } + }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", @@ -73,6 +86,72 @@ "xunit.v3.mtp-v1": "[3.2.1]" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.6.2", + "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" + }, + "Docker.DotNet.Enhanced": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "tm2V/DpnaURbBhMQ7Z3orNR3u+H863KQuYfA/sgGjI5py07dEeV0I02f6pGrx2869KG9uNM/E96puf9i0gId2w==", + "dependencies": { + "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0", + "Docker.DotNet.Enhanced.LegacyHttp": "4.2.0", + "Docker.DotNet.Enhanced.NPipe": "4.2.0", + "Docker.DotNet.Enhanced.NativeHttp": "4.2.0", + "Docker.DotNet.Enhanced.Unix": "4.2.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3" + } + }, + "Docker.DotNet.Enhanced.Handler.Abstractions": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "cQNxpdadEPdNdfjFCl9vgoCQIK3aVHRn1Qlu36aZUFpp4xHfPrk4hRPNVLR/CpobIFJ+dAt8AceTKMlCfPSccw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.3" + } + }, + "Docker.DotNet.Enhanced.LegacyHttp": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "sfbMX1HBPUec3PEMoqlP5ak6skXclcTBmu4gG3aUJatP34J2DgvYMP13bvz/rfrjVkAhPqnIiDKiHAkBCokajg==", + "dependencies": { + "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" + } + }, + "Docker.DotNet.Enhanced.NativeHttp": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "/ll+2ePYm1qrsMdgMO5BzCQnbfTGmPJAc9SqXEManbliVBZvEpBKHXLugx/OeEca2oC/b4RV+UNPtue5u4jAuA==", + "dependencies": { + "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" + } + }, + "Docker.DotNet.Enhanced.NPipe": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "8wyYOD6VkvqRkITwsvkt3UbW/1WDl6NFypNAsIIDaMiglNRzFrQcK0nK9VUEZa6Oja8Bso3UYySDoL8qatatAA==", + "dependencies": { + "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" + } + }, + "Docker.DotNet.Enhanced.Unix": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "x0wNcbww1+p9nUfw8i+JvsSArBDGkoZ9GI2PZ1wPo85B2OiFrdzp89omounNhO2GKyaIRWAqAm5jYZyNg9EnxA==", + "dependencies": { + "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" + } + }, + "Docker.DotNet.Enhanced.X509": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "nMw+FHGwGZieDi7kBgpIVl+E8MzjzXeXHvMQpidLADT06fts2Gw6G+K+p0hMGv7liZULxyYiZnQ1UbE2B9NNQg==", + "dependencies": { + "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" + } + }, "Duende.IdentityModel": { "type": "Transitive", "resolved": "7.1.0", @@ -395,6 +474,20 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SSH.NET": { + "type": "Transitive", + "resolved": "2025.1.0", + "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==", + "dependencies": { + "BouncyCastle.Cryptography": "2.6.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3" + } + }, "System.Collections.Immutable": { "type": "Transitive", "resolved": "8.0.0", diff --git a/src/Weaviate.Client/CollectionsClient.cs b/src/Weaviate.Client/CollectionsClient.cs index 80ecae9c..3c60a605 100644 --- a/src/Weaviate.Client/CollectionsClient.cs +++ b/src/Weaviate.Client/CollectionsClient.cs @@ -221,7 +221,13 @@ internal static void InjectLegacyDefaultVectorIndexType(Rest.Dto.Class dto) foreach (var vc in dto.VectorConfig.Values) if (string.IsNullOrEmpty(vc.VectorIndexType)) vc.VectorIndexType = "hnsw"; - if (string.IsNullOrEmpty(dto.VectorIndexType)) + // Only inject a class-level VectorIndexType when the class is NOT + // using named vectors. Mixing class-level VectorIndexType with + // VectorConfig is rejected by the server. + if ( + string.IsNullOrEmpty(dto.VectorIndexType) + && (dto.VectorConfig is null || dto.VectorConfig.Count == 0) + ) dto.VectorIndexType = "hnsw"; } diff --git a/src/Weaviate.Client/Configure/Vectorizer.cs b/src/Weaviate.Client/Configure/Vectorizer.cs index 21dcbecc..7c03e2d9 100644 --- a/src/Weaviate.Client/Configure/Vectorizer.cs +++ b/src/Weaviate.Client/Configure/Vectorizer.cs @@ -70,7 +70,16 @@ params string[] sourceProperties ) { name ??= "default"; - index ??= new VectorIndex.HNSW(); + // Only synthesize an HNSW index when the user provided a quantizer but + // no explicit index — the quantizer needs an index to attach to and + // HNSW is the implied choice. If the user provided nothing, leave the + // index null so the server can apply its own DEFAULT_VECTOR_INDEX + // (introduced in 1.37.5); on older servers, the legacy "hnsw" string + // is injected as a wire-level default by CollectionsClient. + if (index is null && quantizer is not null) + { + index = new VectorIndex.HNSW(); + } return new( name: name, From 6dc3dcb00f2d390ecd04addfa26c4348518e9fcd Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 15:48:29 +0200 Subject: [PATCH 03/15] Make integration test version-aware via WEAVIATE_VERSION The integration test reads WEAVIATE_VERSION (existing convention) and adapts its assertions to the running server: on servers >= 1.37.5 we assert the server-side DEFAULT_VECTOR_INDEX=flat applies; on older servers we assert the client-injected "hnsw" landed. One container per run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DefaultVectorIndexTypeIntegrationTests.cs | 303 ++++++++++-------- 1 file changed, 170 insertions(+), 133 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs index e3fcfa8f..196cf28f 100644 --- a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs +++ b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs @@ -6,51 +6,99 @@ namespace Weaviate.Client.Tests.Integration; /// -/// End-to-end coverage for the C# client logic introduced in commit -/// Send empty vector index type on Weaviate 1.37.5+: +/// End-to-end coverage for the C# client logic introduced in commits +/// Send empty vector index type on Weaviate 1.37.5+ and +/// Fix vector index type defaulting bugs surfaced by integration tests. +/// +/// +/// One container per run. The target server version is read from the +/// WEAVIATE_VERSION environment variable (the existing convention used +/// by the unit tests, see TestHybridSearchInputSyntax.ServerVersionEnvVar), +/// defaulting to a 1.37.5+ build when unset. CI's matrix runs the suite once +/// per version, so coverage across the 1.37.5 cutoff happens automatically; +/// locally, set WEAVIATE_VERSION=1.37.4 (or any older tag) to exercise +/// the legacy branch. +/// +/// +/// +/// Assertions adapt at runtime based on whether the parsed version is +/// >= 1.37.5: /// /// -/// On Weaviate 1.37.4 (legacy server) the client must inject -/// "hnsw" for any omitted vectorIndexType, and the -/// server must store it as "hnsw". +/// On 1.37.5+ the server applies its own default — the container is +/// started with DEFAULT_VECTOR_INDEX=flat, so any user-omitted +/// vectorIndexType must land as "flat". /// /// -/// On Weaviate 1.37.5+ with DEFAULT_VECTOR_INDEX=flat the client -/// must omit vectorIndexType, and the server must apply its own -/// default — in this case "flat". +/// On older servers the client injects "hnsw" client-side, and the +/// server stores it verbatim. /// /// -/// A user-supplied VectorIndexType (e.g. flat) must be -/// preserved on both server versions — the inject helper is not allowed -/// to mutate explicit values. +/// A user-supplied explicit VectorIndexType (e.g. flat) must +/// round-trip unchanged on every version. /// /// -/// Unlike , these tests start their own -/// Weaviate container per scenario so the two server versions can be -/// exercised in the same test run, with different env vars. +/// +/// +/// +/// DEFAULT_VECTOR_INDEX=flat is set on every run: older servers ignore +/// unknown env vars, so it is harmless on 1.37.4-and-below and asserts the +/// new behaviour on 1.37.5+. +/// /// -/// Requires Docker. If Docker is not reachable, the tests will fail rather -/// than skip, so missing infrastructure is loud in CI. +/// Requires Docker. If Docker is not reachable, the tests fail rather than +/// skip, so missing infrastructure is loud in CI. /// public sealed class DefaultVectorIndexTypeIntegrationTests : IAsyncLifetime { /// - /// Last Weaviate version that does NOT apply a server-side default for - /// vectorIndexType. The client must inject "hnsw" here. + /// Name of the env var that picks the Weaviate server version. Matches the + /// existing convention in the unit-test suite. /// - private const string LegacyImage = "cr.weaviate.io/semitechnologies/weaviate:1.37.4"; + private const string ServerVersionEnvVar = "WEAVIATE_VERSION"; /// - /// First Weaviate build that consumes DEFAULT_VECTOR_INDEX. - /// Two arch-specific tags are published; pick the right one at runtime. + /// Default tag when is unset. The amd64 + /// variant is the canonical default; if the host is arm64 we swap to the + /// matching arm64 build. When the user explicitly sets the env var, they + /// own the suffix and we use the value verbatim. /// - private static string NewImage => - RuntimeInformation.OSArchitecture == Architecture.Arm64 - ? "cr.weaviate.io/semitechnologies/weaviate:1.37.5-e0fe0d5.arm64" - : "cr.weaviate.io/semitechnologies/weaviate:1.37.5-e0fe0d5.amd64"; + private const string DefaultServerVersion = "1.37.5-e0fe0d5.amd64"; + private const string ImageRepo = "cr.weaviate.io/semitechnologies/weaviate"; + + /// + /// First Weaviate version that applies a server-side default for + /// vectorIndexType when the client omits it. + /// + private static readonly Version ServerDefaultCutoff = new(1, 37, 5); + + private readonly string _imageTag; + private readonly bool _serverAppliesDefault; private readonly List _containers = new(); + public DefaultVectorIndexTypeIntegrationTests() + { + var envValue = Environment.GetEnvironmentVariable(ServerVersionEnvVar); + if (string.IsNullOrWhiteSpace(envValue)) + { + // Default kicked in — swap amd64 -> arm64 if needed so the test + // works locally on Apple Silicon without a manual override. + _imageTag = + RuntimeInformation.OSArchitecture == Architecture.Arm64 + ? DefaultServerVersion.Replace(".amd64", ".arm64") + : DefaultServerVersion; + } + else + { + // User explicitly set the version; respect the tag suffix exactly. + _imageTag = envValue; + } + + var parsedVersion = MetaInfo.ParseWeaviateVersion(_imageTag); + _serverAppliesDefault = parsedVersion is not null && parsedVersion >= ServerDefaultCutoff; + } + public ValueTask InitializeAsync() => ValueTask.CompletedTask; public async ValueTask DisposeAsync() @@ -63,23 +111,24 @@ public async ValueTask DisposeAsync() } catch { - // Best-effort cleanup; container disposal failures should not mask - // the actual test outcome. + // Best-effort cleanup; container disposal failures should not + // mask the actual test outcome. } } } /// - /// Builds and starts a single-node Weaviate container with anonymous auth - /// enabled, then constructs a pointed at it. - /// The client's _metaCache is populated by BuildAsync. + /// Builds and starts a single-node Weaviate container at the configured + /// version with anonymous auth enabled and DEFAULT_VECTOR_INDEX=flat + /// applied. The flat default is always set: older servers ignore it. + /// Returns a wired to the container; the + /// client's meta cache is populated by BuildAsync. /// - private async Task StartWeaviateAsync( - string image, - IDictionary? extraEnv = null - ) + private async Task StartWeaviateAsync() { - var builder = new ContainerBuilder(image) + var image = $"{ImageRepo}:{_imageTag}"; + + var container = new ContainerBuilder(image) .WithPortBinding(8080, assignRandomHostPort: true) .WithPortBinding(50051, assignRandomHostPort: true) .WithEnvironment("QUERY_DEFAULTS_LIMIT", "25") @@ -87,23 +136,16 @@ private async Task StartWeaviateAsync( .WithEnvironment("PERSISTENCE_DATA_PATH", "/var/lib/weaviate") .WithEnvironment("CLUSTER_HOSTNAME", "node1") .WithEnvironment("DISABLE_TELEMETRY", "true") + .WithEnvironment("DEFAULT_VECTOR_INDEX", "flat") .WithCommand("--host", "0.0.0.0", "--port", "8080", "--scheme", "http") .WithWaitStrategy( Wait.ForUnixContainer() .UntilHttpRequestIsSucceeded(req => req.ForPort(8080).ForPath("/v1/.well-known/ready") ) - ); - - if (extraEnv is not null) - { - foreach (var kvp in extraEnv) - { - builder = builder.WithEnvironment(kvp.Key, kvp.Value); - } - } + ) + .Build(); - var container = builder.Build(); _containers.Add(container); await container.StartAsync(TestContext.Current.CancellationToken); @@ -115,31 +157,27 @@ private async Task StartWeaviateAsync( .Local(restPort: restPort, grpcPort: grpcPort) .BuildAsync(); - // Sanity check: client should know the server version after BuildAsync. + // Sanity check: client should know the server version after BuildAsync, + // and it should agree with our env-derived gate so the test is + // self-consistent if a tag is mislabelled. Assert.NotNull(client.WeaviateVersion); + Assert.Equal(_serverAppliesDefault, client.WeaviateVersion! >= ServerDefaultCutoff); return client; } /// - /// Creates a collection with a single named vector and no explicit - /// VectorIndexType, then returns the stored type on the server. + /// Creates a collection on the connected server, reads the stored config + /// back, and returns the result of + /// applied to it. The collection is best-effort deleted before and after. /// - private static async Task CreateAndReadIndexTypeAsync( + private static async Task CreateAndReadAsync( WeaviateClient client, - string collectionName, - VectorIndexConfig? index + CollectionCreateParams create, + Func readStoredType ) { - // Best-effort cleanup of any leftover collection with this name. - await client.Collections.Delete(collectionName); - - var create = new CollectionCreateParams - { - Name = collectionName, - Properties = [Property.Text("title")], - VectorConfig = Configure.Vector("default", v => v.SelfProvided(), index: index), - }; + await client.Collections.Delete(create.Name); var collection = await client.Collections.Create( create, @@ -150,106 +188,105 @@ private async Task StartWeaviateAsync( var config = await collection.Config.Get( cancellationToken: TestContext.Current.CancellationToken ); - - Assert.NotNull(config.VectorConfig); - Assert.True(config.VectorConfig.ContainsKey("default")); - return config.VectorConfig["default"].VectorIndexType; + return readStoredType(config); } finally { - await client.Collections.Delete(collectionName); + await client.Collections.Delete(create.Name); } } /// - /// 1.37.4 has no server-side default for vectorIndexType. The C# - /// client must inject "hnsw" in that case, and the server must - /// then store the collection with that type. + /// Scenario A on the named-vector path: user omits VectorIndexType + /// when configuring a named vector. + /// + /// 1.37.5+: server applies DEFAULT_VECTOR_INDEX=flat. + /// Older: client injects "hnsw" and server stores it. + /// /// [Fact] - public async Task LegacyServer_NoIndexType_GetsHnsw() + public async Task NamedVector_NoIndexType_GetsServerOrInjectedDefault() { - var client = await StartWeaviateAsync(LegacyImage); + var client = await StartWeaviateAsync(); - // Sanity check the version gate — must be strictly older than the - // first version that applies a server-side default. - Assert.True(client.WeaviateVersion! < new Version(1, 37, 5)); + var create = new CollectionCreateParams + { + Name = nameof(NamedVector_NoIndexType_GetsServerOrInjectedDefault), + Properties = [Property.Text("title")], + VectorConfig = Configure.Vector("default", v => v.SelfProvided(), index: null), + }; - var stored = await CreateAndReadIndexTypeAsync( + var stored = await CreateAndReadAsync( client, - nameof(LegacyServer_NoIndexType_GetsHnsw), - index: null + create, + config => + { + Assert.NotNull(config.VectorConfig); + Assert.True(config.VectorConfig.ContainsKey("default")); + return config.VectorConfig["default"].VectorIndexType; + } ); - Assert.Equal(VectorIndex.HNSW.TypeValue, stored); + var expected = _serverAppliesDefault + ? VectorIndex.Flat.TypeValue + : VectorIndex.HNSW.TypeValue; + Assert.Equal(expected, stored); } /// - /// 1.37.5+ with DEFAULT_VECTOR_INDEX=flat. The client must omit - /// vectorIndexType entirely so the server applies the configured - /// default — verifying both the client-side change AND the new server - /// behaviour. + /// Scenario B on the named-vector path: user explicitly configures + /// VectorIndex.Flat. Must round-trip as "flat" on every + /// server version — the inject helper must never overwrite an explicit + /// value. /// [Fact] - public async Task NewServer_DefaultVectorIndexFlat_NoIndexType_GetsFlat() + public async Task NamedVector_ExplicitFlat_IsPreserved() { - var client = await StartWeaviateAsync( - NewImage, - extraEnv: new Dictionary { ["DEFAULT_VECTOR_INDEX"] = "flat" } - ); - - // Sanity check: this build must be at or beyond the version gate. - Assert.True(client.WeaviateVersion! >= new Version(1, 37, 5)); + var client = await StartWeaviateAsync(); - var stored = await CreateAndReadIndexTypeAsync( - client, - nameof(NewServer_DefaultVectorIndexFlat_NoIndexType_GetsFlat), - index: null - ); - - Assert.Equal(VectorIndex.Flat.TypeValue, stored); - } - - /// - /// An explicit user choice (flat) must round-trip on the legacy - /// server unchanged — the inject helper must never overwrite a value the - /// user explicitly set. - /// - [Fact] - public async Task LegacyServer_ExplicitFlat_IsPreserved() - { - var client = await StartWeaviateAsync(LegacyImage); + var create = new CollectionCreateParams + { + Name = nameof(NamedVector_ExplicitFlat_IsPreserved), + Properties = [Property.Text("title")], + VectorConfig = Configure.Vector( + "default", + v => v.SelfProvided(), + index: new VectorIndex.Flat() + ), + }; - var stored = await CreateAndReadIndexTypeAsync( + var stored = await CreateAndReadAsync( client, - nameof(LegacyServer_ExplicitFlat_IsPreserved), - index: new VectorIndex.Flat() + create, + config => + { + Assert.NotNull(config.VectorConfig); + Assert.True(config.VectorConfig.ContainsKey("default")); + return config.VectorConfig["default"].VectorIndexType; + } ); Assert.Equal(VectorIndex.Flat.TypeValue, stored); } - /// - /// Same explicit-choice guarantee on a 1.37.5+ server with a different - /// server-side default — proves the client doesn't accidentally rely on - /// the server falling back to DEFAULT_VECTOR_INDEX. - /// - [Fact] - public async Task NewServer_ExplicitFlat_IsPreserved() - { - // Use hnsw as the server default so a passing test really is showing - // that the user-supplied "flat" survived end-to-end. - var client = await StartWeaviateAsync( - NewImage, - extraEnv: new Dictionary { ["DEFAULT_VECTOR_INDEX"] = "hnsw" } - ); - - var stored = await CreateAndReadIndexTypeAsync( - client, - nameof(NewServer_ExplicitFlat_IsPreserved), - index: new VectorIndex.Flat() - ); - - Assert.Equal(VectorIndex.Flat.TypeValue, stored); - } + // The legacy single-vector path (CollectionCreateParams with no + // VectorConfig — the server stores the choice in the top-level + // `vectorIndexType` field) is intentionally NOT exercised here: + // + // * `CollectionCreateParams` does not expose a top-level + // `VectorIndexType` setter on the write side, so Scenario B + // (explicit flat) is not reachable from the public API. + // * On the read side, `Extensions.ToModel(Rest.Dto.Class)` does not + // copy the top-level `vectorIndexType` from the GET response into + // `CollectionConfig.VectorIndexType`; the field stays at its default + // regardless of what the server returned. End-to-end assertions on + // the top-level field therefore cannot pass until that parse path + // is fixed — a separate bug. + // + // The legacy top-level inject behaviour is locked in by the wire-level + // unit tests in + // `CollectionsClient.DefaultVectorIndexTypeTests.InjectLegacyDefaultVectorIndexType_*`, + // which assert directly on the outgoing DTO. The integration coverage + // here focuses on the named-vector path, which is the supported public + // API surface for picking an index type in modern code. } From 6bc72bc070b53e213f254d44a76158afe0fb4f84 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 15:50:27 +0200 Subject: [PATCH 04/15] Bump CI 1.37 matrix entry to 1.37.5-e0fe0d5.amd64 This is the first server build that exposes DEFAULT_VECTOR_INDEX, which the version-aware integration test in this branch verifies end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index dce33607..388101e9 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -132,7 +132,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["1.32.27", "1.33.18", "1.34.20", "1.35.16", "1.36.10", "1.37.2"] + version: ["1.32.27", "1.33.18", "1.34.20", "1.35.16", "1.36.10", "1.37.5-e0fe0d5.amd64"] uses: ./.github/workflows/test-on-weaviate-version.yml secrets: inherit with: From 8657ca7b77146386c7276f6049ab1d5f61ad4f29 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 15:57:03 +0200 Subject: [PATCH 05/15] Drive integration test DEFAULT_VECTOR_INDEX via env var The test reads DEFAULT_VECTOR_INDEX (default "hfresh") for both the testcontainer env and the expected stored type on servers >= 1.37.5. CI sets it explicitly to "hfresh" so the value used in CI is visible in the workflow file. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/main.yaml | 1 + .../DefaultVectorIndexTypeIntegrationTests.cs | 50 +++++++++++++------ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 388101e9..53083da3 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -17,6 +17,7 @@ env: DOTNET_VERSION: "9.x" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true + DEFAULT_VECTOR_INDEX: hfresh OKTA_CLIENT_SECRET: ${{ secrets.OKTA_CLIENT_SECRET }} OKTA_DUMMY_CI_PW: ${{ secrets.OKTA_DUMMY_CI_PW }} diff --git a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs index 196cf28f..59127198 100644 --- a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs +++ b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs @@ -26,8 +26,9 @@ namespace Weaviate.Client.Tests.Integration; /// /// /// On 1.37.5+ the server applies its own default — the container is -/// started with DEFAULT_VECTOR_INDEX=flat, so any user-omitted -/// vectorIndexType must land as "flat". +/// started with DEFAULT_VECTOR_INDEX set from the host env var +/// (default "hfresh"), so any user-omitted vectorIndexType +/// must land as that value. /// /// /// On older servers the client injects "hnsw" client-side, and the @@ -41,9 +42,10 @@ namespace Weaviate.Client.Tests.Integration; /// /// /// -/// DEFAULT_VECTOR_INDEX=flat is set on every run: older servers ignore -/// unknown env vars, so it is harmless on 1.37.4-and-below and asserts the -/// new behaviour on 1.37.5+. +/// DEFAULT_VECTOR_INDEX is read from the host env var (default +/// "hfresh") and forwarded to the container on every run: older +/// servers ignore unknown env vars, so it is harmless on 1.37.4-and-below +/// and asserts the new behaviour on 1.37.5+. /// /// /// Requires Docker. If Docker is not reachable, the tests fail rather than @@ -57,6 +59,20 @@ public sealed class DefaultVectorIndexTypeIntegrationTests : IAsyncLifetime /// private const string ServerVersionEnvVar = "WEAVIATE_VERSION"; + /// + /// Name of the env var that picks both the container's + /// DEFAULT_VECTOR_INDEX setting and the expected stored + /// vectorIndexType on servers >= 1.37.5. Defaults to + /// when unset so local runs match + /// the production recommended default. + /// + private const string DefaultVectorIndexEnvVar = "DEFAULT_VECTOR_INDEX"; + + /// + /// Fallback value when is unset. + /// + private const string DefaultVectorIndexFallback = "hfresh"; + /// /// Default tag when is unset. The amd64 /// variant is the canonical default; if the host is arm64 we swap to the @@ -75,10 +91,16 @@ public sealed class DefaultVectorIndexTypeIntegrationTests : IAsyncLifetime private readonly string _imageTag; private readonly bool _serverAppliesDefault; + private readonly string _defaultVectorIndex; private readonly List _containers = new(); public DefaultVectorIndexTypeIntegrationTests() { + var defaultVectorIndexEnv = Environment.GetEnvironmentVariable(DefaultVectorIndexEnvVar); + _defaultVectorIndex = string.IsNullOrWhiteSpace(defaultVectorIndexEnv) + ? DefaultVectorIndexFallback + : defaultVectorIndexEnv; + var envValue = Environment.GetEnvironmentVariable(ServerVersionEnvVar); if (string.IsNullOrWhiteSpace(envValue)) { @@ -119,10 +141,11 @@ public async ValueTask DisposeAsync() /// /// Builds and starts a single-node Weaviate container at the configured - /// version with anonymous auth enabled and DEFAULT_VECTOR_INDEX=flat - /// applied. The flat default is always set: older servers ignore it. - /// Returns a wired to the container; the - /// client's meta cache is populated by BuildAsync. + /// version with anonymous auth enabled and DEFAULT_VECTOR_INDEX + /// applied (value read from the host env var, default + /// ). Older servers ignore the + /// env var. Returns a wired to the + /// container; the client's meta cache is populated by BuildAsync. /// private async Task StartWeaviateAsync() { @@ -136,7 +159,7 @@ private async Task StartWeaviateAsync() .WithEnvironment("PERSISTENCE_DATA_PATH", "/var/lib/weaviate") .WithEnvironment("CLUSTER_HOSTNAME", "node1") .WithEnvironment("DISABLE_TELEMETRY", "true") - .WithEnvironment("DEFAULT_VECTOR_INDEX", "flat") + .WithEnvironment("DEFAULT_VECTOR_INDEX", _defaultVectorIndex) .WithCommand("--host", "0.0.0.0", "--port", "8080", "--scheme", "http") .WithWaitStrategy( Wait.ForUnixContainer() @@ -200,7 +223,8 @@ private async Task StartWeaviateAsync() /// Scenario A on the named-vector path: user omits VectorIndexType /// when configuring a named vector. /// - /// 1.37.5+: server applies DEFAULT_VECTOR_INDEX=flat. + /// 1.37.5+: server applies DEFAULT_VECTOR_INDEX from the + /// host env var (default ). /// Older: client injects "hnsw" and server stores it. /// /// @@ -227,9 +251,7 @@ public async Task NamedVector_NoIndexType_GetsServerOrInjectedDefault() } ); - var expected = _serverAppliesDefault - ? VectorIndex.Flat.TypeValue - : VectorIndex.HNSW.TypeValue; + var expected = _serverAppliesDefault ? _defaultVectorIndex : VectorIndex.HNSW.TypeValue; Assert.Equal(expected, stored); } From d70f004f0f82f2f99b69d8946e019e7aa9a86ad1 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 16:01:08 +0200 Subject: [PATCH 06/15] Require WEAVIATE_VERSION env var in default-vector-index integration test Drop the hardcoded fallback ("1.37.5-e0fe0d5.amd64") and the arch-detection branch. CI sets the version explicitly in the workflow env block; locally export it before running. Hardcoded version defaults in test code rot and can mask CI/local drift. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DefaultVectorIndexTypeIntegrationTests.cs | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs index 59127198..373fd368 100644 --- a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs +++ b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using Weaviate.Client.Models; @@ -14,10 +13,8 @@ namespace Weaviate.Client.Tests.Integration; /// One container per run. The target server version is read from the /// WEAVIATE_VERSION environment variable (the existing convention used /// by the unit tests, see TestHybridSearchInputSyntax.ServerVersionEnvVar), -/// defaulting to a 1.37.5+ build when unset. CI's matrix runs the suite once -/// per version, so coverage across the 1.37.5 cutoff happens automatically; -/// locally, set WEAVIATE_VERSION=1.37.4 (or any older tag) to exercise -/// the legacy branch. +/// and is required — CI sets it; locally export it (e.g. +/// WEAVIATE_VERSION=1.37.4 for the legacy branch) before running. /// /// /// @@ -73,14 +70,6 @@ public sealed class DefaultVectorIndexTypeIntegrationTests : IAsyncLifetime /// private const string DefaultVectorIndexFallback = "hfresh"; - /// - /// Default tag when is unset. The amd64 - /// variant is the canonical default; if the host is arm64 we swap to the - /// matching arm64 build. When the user explicitly sets the env var, they - /// own the suffix and we use the value verbatim. - /// - private const string DefaultServerVersion = "1.37.5-e0fe0d5.amd64"; - private const string ImageRepo = "cr.weaviate.io/semitechnologies/weaviate"; /// @@ -104,18 +93,11 @@ public DefaultVectorIndexTypeIntegrationTests() var envValue = Environment.GetEnvironmentVariable(ServerVersionEnvVar); if (string.IsNullOrWhiteSpace(envValue)) { - // Default kicked in — swap amd64 -> arm64 if needed so the test - // works locally on Apple Silicon without a manual override. - _imageTag = - RuntimeInformation.OSArchitecture == Architecture.Arm64 - ? DefaultServerVersion.Replace(".amd64", ".arm64") - : DefaultServerVersion; - } - else - { - // User explicitly set the version; respect the tag suffix exactly. - _imageTag = envValue; + throw new InvalidOperationException( + $"{ServerVersionEnvVar} env var is required for this integration test." + ); } + _imageTag = envValue; var parsedVersion = MetaInfo.ParseWeaviateVersion(_imageTag); _serverAppliesDefault = parsedVersion is not null && parsedVersion >= ServerDefaultCutoff; From 5293434b80862ad9518e038ac6759591d193929a Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 16:10:30 +0200 Subject: [PATCH 07/15] Scope DEFAULT_VECTOR_INDEX to integration test job instead of global env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The calling workflow's global env is not inherited by reusable workflows — the variable was silently absent from the test job. Moving it to a job-level env in test-on-weaviate-version.yml makes it available where the testcontainer integration test runs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/main.yaml | 1 - .github/workflows/test-on-weaviate-version.yml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 53083da3..388101e9 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -17,7 +17,6 @@ env: DOTNET_VERSION: "9.x" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true - DEFAULT_VECTOR_INDEX: hfresh OKTA_CLIENT_SECRET: ${{ secrets.OKTA_CLIENT_SECRET }} OKTA_DUMMY_CI_PW: ${{ secrets.OKTA_DUMMY_CI_PW }} diff --git a/.github/workflows/test-on-weaviate-version.yml b/.github/workflows/test-on-weaviate-version.yml index bcffc1a7..b811bcf2 100644 --- a/.github/workflows/test-on-weaviate-version.yml +++ b/.github/workflows/test-on-weaviate-version.yml @@ -28,6 +28,8 @@ jobs: contents: read checks: write runs-on: ubuntu-latest + env: + DEFAULT_VECTOR_INDEX: hfresh steps: - name: Checkout code From 48d4252b14a987308cc558ec2725b60c2d829867 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 22:27:26 +0200 Subject: [PATCH 08/15] Inject legacy hnsw default in AddVector path for servers < 1.37.5 The inject step in CollectionsClient only covered the create path. The AddVector path (CollectionConfigClient.AddVectorAsync) also needs it to avoid sending an empty VectorIndexType to old servers, which reject it with 422 "unsupported vector index type". Also extract ExpectedDefaultIndexType() helper in the integration test to avoid repeating the ternary inline. Co-Authored-By: Claude Sonnet 4.6 --- .../DefaultVectorIndexTypeIntegrationTests.cs | 15 +++++++++++++-- src/Weaviate.Client/CollectionConfigClient.cs | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs index 373fd368..7a3772de 100644 --- a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs +++ b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs @@ -201,6 +201,18 @@ private async Task StartWeaviateAsync() } } + /// + /// Returns the expected stored VectorIndexType for a vector that was + /// configured without an explicit index type. + /// + /// 1.37.5+: server applies DEFAULT_VECTOR_INDEX from the + /// container env var. + /// Older: client injects "hnsw". + /// + /// + private string ExpectedDefaultIndexType() => + _serverAppliesDefault ? _defaultVectorIndex : VectorIndex.HNSW.TypeValue; + /// /// Scenario A on the named-vector path: user omits VectorIndexType /// when configuring a named vector. @@ -233,8 +245,7 @@ public async Task NamedVector_NoIndexType_GetsServerOrInjectedDefault() } ); - var expected = _serverAppliesDefault ? _defaultVectorIndex : VectorIndex.HNSW.TypeValue; - Assert.Equal(expected, stored); + Assert.Equal(ExpectedDefaultIndexType(), stored); } /// diff --git a/src/Weaviate.Client/CollectionConfigClient.cs b/src/Weaviate.Client/CollectionConfigClient.cs index c02979fa..cd1ece26 100644 --- a/src/Weaviate.Client/CollectionConfigClient.cs +++ b/src/Weaviate.Client/CollectionConfigClient.cs @@ -145,6 +145,8 @@ await _client.Collections.Export(_collectionName, cancellationToken) collection.VectorConfig.Add(vector); var dto = collection.ToDto(); + if (_client.InjectLegacyVectorIndexDefault) + CollectionsClient.InjectLegacyDefaultVectorIndexType(dto); // 3. PUT to /schema await _client.RestClient.CollectionUpdate(_collectionName, dto, cancellationToken); From a2545bd6cac856050a7cd1692d620c2aa4bfa2ff Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 22:27:40 +0200 Subject: [PATCH 09/15] Remove MaxWorkers and AliveNodesCheckingFrequency from ReplicationAsyncConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both fields are no-ops on servers >= 1.37.3. Remove from the user-facing model, both directions of the DTO mapping, and unit tests. The generated DTO (Models.g.cs) still has the fields so old-server responses continue to deserialize cleanly — they are simply dropped during DTO-to-model mapping. Unit tests updated to use PropagationConcurrency and HashtreeHeight. Co-Authored-By: Claude Sonnet 4.6 --- .../Unit/TestCollection.cs | 18 +++++------------- src/Weaviate.Client/Extensions.cs | 4 ---- .../Models/ReplicationConfig.cs | 6 ------ 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/Weaviate.Client.Tests/Unit/TestCollection.cs b/src/Weaviate.Client.Tests/Unit/TestCollection.cs index e6c51d4c..f779d522 100644 --- a/src/Weaviate.Client.Tests/Unit/TestCollection.cs +++ b/src/Weaviate.Client.Tests/Unit/TestCollection.cs @@ -591,11 +591,9 @@ public void ReplicationConfig_WithAsyncConfig_MapsToDto() { var asyncConfig = new ReplicationAsyncConfig { - MaxWorkers = 4, HashtreeHeight = 16, Frequency = 1000, FrequencyWhilePropagating = 500, - AliveNodesCheckingFrequency = 30000, LoggingFrequency = 60, DiffBatchSize = 100, DiffPerNodeTimeout = 30, @@ -617,11 +615,9 @@ public void ReplicationConfig_WithAsyncConfig_MapsToDto() Assert.NotNull(dto.ReplicationConfig?.AsyncConfig); var ac = dto.ReplicationConfig!.AsyncConfig!; - Assert.Equal(4, ac.MaxWorkers); Assert.Equal(16, ac.HashtreeHeight); Assert.Equal(1000, ac.Frequency); Assert.Equal(500, ac.FrequencyWhilePropagating); - Assert.Equal(30000, ac.AliveNodesCheckingFrequency); Assert.Equal(60, ac.LoggingFrequency); Assert.Equal(100, ac.DiffBatchSize); Assert.Equal(30, ac.DiffPerNodeTimeout); @@ -641,9 +637,9 @@ public void ReplicationConfig_WithAsyncConfig_RoundTripsFromDto() { var dtoAsyncConfig = new Rest.Dto.ReplicationAsyncConfig { - MaxWorkers = 8, HashtreeHeight = 12, PropagationLimit = 5000, + PropagationConcurrency = 3, }; var dto = new Rest.Dto.Class @@ -656,9 +652,9 @@ public void ReplicationConfig_WithAsyncConfig_RoundTripsFromDto() Assert.NotNull(model.ReplicationConfig?.AsyncConfig); var ac = model.ReplicationConfig!.AsyncConfig!; - Assert.Equal(8, ac.MaxWorkers); Assert.Equal(12, ac.HashtreeHeight); Assert.Equal(5000, ac.PropagationLimit); + Assert.Equal(3, ac.PropagationConcurrency); // Unset fields are null Assert.Null(ac.Frequency); Assert.Null(ac.PropagationBatchSize); @@ -689,11 +685,9 @@ public void ReplicationConfig_WithAsyncConfig_RoundTripsAllFieldsFromDto() { var dtoAsyncConfig = new Rest.Dto.ReplicationAsyncConfig { - MaxWorkers = 1, HashtreeHeight = 2, Frequency = 3, FrequencyWhilePropagating = 4, - AliveNodesCheckingFrequency = 5, LoggingFrequency = 6, DiffBatchSize = 7, DiffPerNodeTimeout = 8, @@ -715,11 +709,9 @@ public void ReplicationConfig_WithAsyncConfig_RoundTripsAllFieldsFromDto() Assert.NotNull(model.ReplicationConfig?.AsyncConfig); var ac = model.ReplicationConfig!.AsyncConfig!; - Assert.Equal(1, ac.MaxWorkers); Assert.Equal(2, ac.HashtreeHeight); Assert.Equal(3, ac.Frequency); Assert.Equal(4, ac.FrequencyWhilePropagating); - Assert.Equal(5, ac.AliveNodesCheckingFrequency); Assert.Equal(6, ac.LoggingFrequency); Assert.Equal(7, ac.DiffBatchSize); Assert.Equal(8, ac.DiffPerNodeTimeout); @@ -742,11 +734,11 @@ public void ReplicationConfigUpdate_AsyncConfig_ForwardsToWrappedConfig() Assert.Null(update.AsyncConfig); - update.AsyncConfig = new ReplicationAsyncConfig { MaxWorkers = 42 }; + update.AsyncConfig = new ReplicationAsyncConfig { PropagationConcurrency = 42 }; Assert.NotNull(update.AsyncConfig); - Assert.Equal(42, update.AsyncConfig!.MaxWorkers); + Assert.Equal(42, update.AsyncConfig!.PropagationConcurrency); Assert.NotNull(replicationConfig.AsyncConfig); - Assert.Equal(42, replicationConfig.AsyncConfig!.MaxWorkers); + Assert.Equal(42, replicationConfig.AsyncConfig!.PropagationConcurrency); } } diff --git a/src/Weaviate.Client/Extensions.cs b/src/Weaviate.Client/Extensions.cs index 9cbf9f0d..2d2efb06 100644 --- a/src/Weaviate.Client/Extensions.cs +++ b/src/Weaviate.Client/Extensions.cs @@ -169,11 +169,9 @@ internal static Rest.Dto.Class ToDto(this CollectionConfig collection) AsyncConfig = rc.AsyncConfig is ReplicationAsyncConfig ac ? new Rest.Dto.ReplicationAsyncConfig { - MaxWorkers = ac.MaxWorkers, HashtreeHeight = ac.HashtreeHeight, Frequency = ac.Frequency, FrequencyWhilePropagating = ac.FrequencyWhilePropagating, - AliveNodesCheckingFrequency = ac.AliveNodesCheckingFrequency, LoggingFrequency = ac.LoggingFrequency, DiffBatchSize = ac.DiffBatchSize, DiffPerNodeTimeout = ac.DiffPerNodeTimeout, @@ -414,11 +412,9 @@ internal static CollectionConfigExport ToModel(this Rest.Dto.Class collection) AsyncConfig = rc.AsyncConfig is Rest.Dto.ReplicationAsyncConfig ac ? new ReplicationAsyncConfig { - MaxWorkers = ac.MaxWorkers, HashtreeHeight = ac.HashtreeHeight, Frequency = ac.Frequency, FrequencyWhilePropagating = ac.FrequencyWhilePropagating, - AliveNodesCheckingFrequency = ac.AliveNodesCheckingFrequency, LoggingFrequency = ac.LoggingFrequency, DiffBatchSize = ac.DiffBatchSize, DiffPerNodeTimeout = ac.DiffPerNodeTimeout, diff --git a/src/Weaviate.Client/Models/ReplicationConfig.cs b/src/Weaviate.Client/Models/ReplicationConfig.cs index d57b6b54..58dd10a3 100644 --- a/src/Weaviate.Client/Models/ReplicationConfig.cs +++ b/src/Weaviate.Client/Models/ReplicationConfig.cs @@ -28,9 +28,6 @@ public enum DeletionStrategy /// public record ReplicationAsyncConfig { - /// Maximum number of async replication workers. - public long? MaxWorkers { get; set; } - /// Height of the hashtree used for diffing. public long? HashtreeHeight { get; set; } @@ -40,9 +37,6 @@ public record ReplicationAsyncConfig /// Frequency in milliseconds at which async replication runs while propagation is active. public long? FrequencyWhilePropagating { get; set; } - /// Interval in milliseconds at which liveness of target nodes is checked. - public long? AliveNodesCheckingFrequency { get; set; } - /// Interval in seconds at which async replication logs its status. public long? LoggingFrequency { get; set; } From 84bbce0ccbb2b7bfbc5a4baaa6c78d557edcbe3e Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 22:32:43 +0200 Subject: [PATCH 10/15] Remove maxWorkers and aliveNodesCheckingFrequency from OpenAPI spec and regenerate DTO Removes the two no-op fields from openapi.json and regenerates Models.g.cs via NSwag so the DTO layer is consistent with the handwritten model and mapping code. Co-Authored-By: Claude Sonnet 4.6 --- src/Weaviate.Client/Rest/Dto/Models.g.cs | 16 ---------------- src/Weaviate.Client/Rest/Schema/openapi.json | 14 -------------- 2 files changed, 30 deletions(-) diff --git a/src/Weaviate.Client/Rest/Dto/Models.g.cs b/src/Weaviate.Client/Rest/Dto/Models.g.cs index 4137ae15..966bb969 100644 --- a/src/Weaviate.Client/Rest/Dto/Models.g.cs +++ b/src/Weaviate.Client/Rest/Dto/Models.g.cs @@ -2703,14 +2703,6 @@ internal partial record AsyncReplicationStatus [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.6.1.0 (NJsonSchema v11.5.1.0 (Newtonsoft.Json v13.0.0.0))")] internal partial record ReplicationAsyncConfig { - /// - /// Maximum number of async replication workers. - /// - - [System.Text.Json.Serialization.JsonPropertyName("maxWorkers")] - - public long? MaxWorkers { get; set; } = default!; - /// /// Height of the hashtree used for diffing. /// @@ -2735,14 +2727,6 @@ internal partial record ReplicationAsyncConfig public long? FrequencyWhilePropagating { get; set; } = default!; - /// - /// Interval in milliseconds at which liveness of target nodes is checked. - /// - - [System.Text.Json.Serialization.JsonPropertyName("aliveNodesCheckingFrequency")] - - public long? AliveNodesCheckingFrequency { get; set; } = default!; - /// /// Interval in seconds at which async replication logs its status. /// diff --git a/src/Weaviate.Client/Rest/Schema/openapi.json b/src/Weaviate.Client/Rest/Schema/openapi.json index deed4ef7..cdc34c4d 100644 --- a/src/Weaviate.Client/Rest/Schema/openapi.json +++ b/src/Weaviate.Client/Rest/Schema/openapi.json @@ -2303,13 +2303,6 @@ "description": "Configuration for asynchronous replication.", "type": "object", "properties": { - "maxWorkers": { - "description": "Maximum number of async replication workers.", - "type": "integer", - "format": "int64", - "x-nullable": true, - "x-omitempty": true - }, "hashtreeHeight": { "description": "Height of the hashtree used for diffing.", "type": "integer", @@ -2331,13 +2324,6 @@ "x-nullable": true, "x-omitempty": true }, - "aliveNodesCheckingFrequency": { - "description": "Interval in milliseconds at which liveness of target nodes is checked.", - "type": "integer", - "format": "int64", - "x-nullable": true, - "x-omitempty": true - }, "loggingFrequency": { "description": "Interval in seconds at which async replication logs its status.", "type": "integer", From 58585262e3cbd5f81fab39efe60648746b3c0285 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 22:36:09 +0200 Subject: [PATCH 11/15] Add changelog entry for MaxWorkers/AliveNodesCheckingFrequency removal Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3679b28..484f2996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- **`ReplicationAsyncConfig.MaxWorkers` and `ReplicationAsyncConfig.AliveNodesCheckingFrequency`** — Both fields have been no-ops on the server since Weaviate 1.37.3 and are now removed from the user-facing model, the OpenAPI spec, and the generated DTO. Existing code that sets these properties will not compile after upgrading; no behavioral change results from the removal. + --- ## [1.1.0] — 2026-05-11 From b9d8ec0ba7ab3412e197cecb8deca7f762165109 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 21 May 2026 22:53:09 +0200 Subject: [PATCH 12/15] Pass WEAVIATE_VERSION to all test steps via job-level env The integration test steps were missing WEAVIATE_VERSION, causing DefaultVectorIndexTypeIntegrationTests to throw on startup. Promote it to the job env alongside DEFAULT_VECTOR_INDEX so all steps (unit and integration) see it. Remove the now-redundant inline prefix from the unit test step. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test-on-weaviate-version.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-on-weaviate-version.yml b/.github/workflows/test-on-weaviate-version.yml index b811bcf2..52faab5c 100644 --- a/.github/workflows/test-on-weaviate-version.yml +++ b/.github/workflows/test-on-weaviate-version.yml @@ -30,6 +30,7 @@ jobs: runs-on: ubuntu-latest env: DEFAULT_VECTOR_INDEX: hfresh + WEAVIATE_VERSION: ${{ inputs.weaviate-version }} steps: - name: Checkout code @@ -57,7 +58,7 @@ jobs: echo "🏃 DRY-RUN MODE: Skipping actual unit tests" echo '' > ./test-results/test-unit-${{ inputs.weaviate-version }}.trx else - WEAVIATE_VERSION=${{ inputs.weaviate-version }} dotnet test --no-restore \ + dotnet test --no-restore \ --filter "FullyQualifiedName~Unit" \ --logger "trx;LogFileName=test-unit-${{ inputs.weaviate-version }}.trx" \ --results-directory ./test-results From a70d627295e43274853b82957783d592d98a471a Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sat, 23 May 2026 00:21:00 +0200 Subject: [PATCH 13/15] Add InjectLegacyVectorIndexDefault check to Update method and simplify integration tests - Add InjectLegacyVectorIndexDefault check to CollectionConfigClient.Update() to match Create() behavior - Simplify DefaultVectorIndexTypeIntegrationTests to use existing IntegrationTests infrastructure instead of Testcontainers - Remove Testcontainers dependency - Add DEFAULT_VECTOR_INDEX support to all docker-compose files for integration testing - Update ci/start_weaviate.sh to set DEFAULT_VECTOR_INDEX env var - Remove redundant env vars from test-on-weaviate-version.yml workflow This addresses the PR comment about needing the InjectLegacyVectorIndexDefault check in CollectionConfigClient for consistency. --- .../workflows/test-on-weaviate-version.yml | 3 - ci/docker-compose-cluster.yml | 3 + ci/docker-compose-okta-cc.yml | 1 + ci/docker-compose-okta-users.yml | 1 + ci/docker-compose-rbac.yml | 1 + ci/docker-compose.yml | 1 + ci/start_weaviate.sh | 2 + .../DefaultVectorIndexTypeIntegrationTests.cs | 279 ++---------------- .../Weaviate.Client.Tests.csproj | 1 - src/Weaviate.Client.Tests/packages.lock.json | 93 ------ src/Weaviate.Client/CollectionConfigClient.cs | 6 +- 11 files changed, 44 insertions(+), 347 deletions(-) diff --git a/.github/workflows/test-on-weaviate-version.yml b/.github/workflows/test-on-weaviate-version.yml index 52faab5c..4f9bcbe7 100644 --- a/.github/workflows/test-on-weaviate-version.yml +++ b/.github/workflows/test-on-weaviate-version.yml @@ -28,9 +28,6 @@ jobs: contents: read checks: write runs-on: ubuntu-latest - env: - DEFAULT_VECTOR_INDEX: hfresh - WEAVIATE_VERSION: ${{ inputs.weaviate-version }} steps: - name: Checkout code diff --git a/ci/docker-compose-cluster.yml b/ci/docker-compose-cluster.yml index 85ec28fd..788734b7 100644 --- a/ci/docker-compose-cluster.yml +++ b/ci/docker-compose-cluster.yml @@ -20,6 +20,7 @@ services: DEFAULT_VECTORIZER_MODULE: text2vec-transformers ENABLE_MODULES: text2vec-transformers AUTOSCHEMA_ENABLED: 'false' + DEFAULT_VECTOR_INDEX: '${DEFAULT_VECTOR_INDEX:-hfresh}' REPLICA_MOVEMENT_ENABLED: 'true' TRANSFORMERS_INFERENCE_API: http://transformers:8080 @@ -54,6 +55,7 @@ services: DEFAULT_VECTORIZER_MODULE: text2vec-transformers ENABLE_MODULES: text2vec-transformers AUTOSCHEMA_ENABLED: 'false' + DEFAULT_VECTOR_INDEX: '${DEFAULT_VECTOR_INDEX:-hfresh}' REPLICA_MOVEMENT_ENABLED: 'true' TRANSFORMERS_INFERENCE_API: http://transformers:8080 @@ -88,6 +90,7 @@ services: DEFAULT_VECTORIZER_MODULE: text2vec-transformers ENABLE_MODULES: text2vec-transformers AUTOSCHEMA_ENABLED: 'false' + DEFAULT_VECTOR_INDEX: '${DEFAULT_VECTOR_INDEX:-hfresh}' REPLICA_MOVEMENT_ENABLED: 'true' TRANSFORMERS_INFERENCE_API: http://transformers:8080 ... diff --git a/ci/docker-compose-okta-cc.yml b/ci/docker-compose-okta-cc.yml index 1fb6e68c..1904a7cb 100644 --- a/ci/docker-compose-okta-cc.yml +++ b/ci/docker-compose-okta-cc.yml @@ -22,5 +22,6 @@ services: AUTHENTICATION_OIDC_GROUPS_CLAIM: 'groups' AUTHORIZATION_ADMINLIST_ENABLED: 'true' AUTHORIZATION_ADMINLIST_USERS: '0oa7e9ipdkVZRUcxo5d7' + DEFAULT_VECTOR_INDEX: '${DEFAULT_VECTOR_INDEX:-hfresh}' DISABLE_TELEMETRY: 'true' ... diff --git a/ci/docker-compose-okta-users.yml b/ci/docker-compose-okta-users.yml index d167c59f..c57c1b28 100644 --- a/ci/docker-compose-okta-users.yml +++ b/ci/docker-compose-okta-users.yml @@ -22,6 +22,7 @@ services: AUTHENTICATION_OIDC_GROUPS_CLAIM: 'groups' AUTHORIZATION_ADMINLIST_ENABLED: 'true' AUTHORIZATION_ADMINLIST_USERS: 'test@test.de' + DEFAULT_VECTOR_INDEX: '${DEFAULT_VECTOR_INDEX:-hfresh}' AUTHENTICATION_OIDC_SCOPES: 'openid,email' DISABLE_TELEMETRY: 'true' ... diff --git a/ci/docker-compose-rbac.yml b/ci/docker-compose-rbac.yml index 7004dd74..7f40c2b9 100644 --- a/ci/docker-compose-rbac.yml +++ b/ci/docker-compose-rbac.yml @@ -27,6 +27,7 @@ services: AUTHORIZATION_ENABLE_RBAC: "true" AUTHENTICATION_DB_USERS_ENABLED: "true" AUTHENTICATION_OIDC_ENABLED: 'true' + DEFAULT_VECTOR_INDEX: '${DEFAULT_VECTOR_INDEX:-hfresh}' AUTHENTICATION_OIDC_CLIENT_ID: 'Peuc12y02UA0eAED1dqSjE5HtGUrpBsx' AUTHENTICATION_OIDC_ISSUER: 'https://auth.weaviate.cloud/Peuc12y02UA0eAED1dqSjE5HtGUrpBsx' AUTHENTICATION_OIDC_USERNAME_CLAIM: 'email' diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index 7a57427e..b197578c 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -31,6 +31,7 @@ services: CLUSTER_HOSTNAME: "singlenode" AUTOSCHEMA_ENABLED: 'false' DISABLE_TELEMETRY: 'true' + DEFAULT_VECTOR_INDEX: '${DEFAULT_VECTOR_INDEX:-hfresh}' DISABLE_LAZY_LOAD_SHARDS: 'true' OBJECTS_TTL_DELETE_SCHEDULE: "@every 1m" GRPC_MAX_MESSAGE_SIZE: 100000000 # 100mb diff --git a/ci/start_weaviate.sh b/ci/start_weaviate.sh index 4fe15e53..aa63d59e 100755 --- a/ci/start_weaviate.sh +++ b/ci/start_weaviate.sh @@ -4,6 +4,7 @@ set -eou pipefail DEFAULT_VERSION="1.34.0" MIN_SUPPORTED="1.31.0" +DEFAULT_VECTOR_INDEX_VALUE="hfresh" REQUESTED_VERSION="${1:-$DEFAULT_VERSION}" # Compare versions; ensure REQUESTED_VERSION >= MIN_SUPPORTED @@ -13,6 +14,7 @@ if [[ $(printf '%s\n' "$MIN_SUPPORTED" "$REQUESTED_VERSION" | sort -V | head -n1 fi export WEAVIATE_VERSION="$REQUESTED_VERSION" +export DEFAULT_VECTOR_INDEX="${DEFAULT_VECTOR_INDEX:-$DEFAULT_VECTOR_INDEX_VALUE}" cd "$(dirname "${BASH_SOURCE[0]}")" diff --git a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs index 7a3772de..9d9543db 100644 --- a/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs +++ b/src/Weaviate.Client.Tests/Integration/DefaultVectorIndexTypeIntegrationTests.cs @@ -1,218 +1,36 @@ -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; using Weaviate.Client.Models; +using Weaviate.Client.Tests.Common; namespace Weaviate.Client.Tests.Integration; /// -/// End-to-end coverage for the C# client logic introduced in commits -/// Send empty vector index type on Weaviate 1.37.5+ and -/// Fix vector index type defaulting bugs surfaced by integration tests. -/// -/// -/// One container per run. The target server version is read from the -/// WEAVIATE_VERSION environment variable (the existing convention used -/// by the unit tests, see TestHybridSearchInputSyntax.ServerVersionEnvVar), -/// and is required — CI sets it; locally export it (e.g. -/// WEAVIATE_VERSION=1.37.4 for the legacy branch) before running. -/// -/// -/// -/// Assertions adapt at runtime based on whether the parsed version is -/// >= 1.37.5: -/// -/// -/// On 1.37.5+ the server applies its own default — the container is -/// started with DEFAULT_VECTOR_INDEX set from the host env var -/// (default "hfresh"), so any user-omitted vectorIndexType -/// must land as that value. -/// -/// -/// On older servers the client injects "hnsw" client-side, and the -/// server stores it verbatim. -/// -/// -/// A user-supplied explicit VectorIndexType (e.g. flat) must -/// round-trip unchanged on every version. -/// -/// -/// -/// -/// -/// DEFAULT_VECTOR_INDEX is read from the host env var (default -/// "hfresh") and forwarded to the container on every run: older -/// servers ignore unknown env vars, so it is harmless on 1.37.4-and-below -/// and asserts the new behaviour on 1.37.5+. -/// -/// -/// Requires Docker. If Docker is not reachable, the tests fail rather than -/// skip, so missing infrastructure is loud in CI. +/// End-to-end coverage for default vector index behavior across server versions. +/// Assertions rely on the connected server version reported by the test client. /// -public sealed class DefaultVectorIndexTypeIntegrationTests : IAsyncLifetime +[Collection("DefaultVectorIndexTypeIntegrationTests")] +public sealed class DefaultVectorIndexTypeIntegrationTests : IntegrationTests { - /// - /// Name of the env var that picks the Weaviate server version. Matches the - /// existing convention in the unit-test suite. - /// - private const string ServerVersionEnvVar = "WEAVIATE_VERSION"; - - /// - /// Name of the env var that picks both the container's - /// DEFAULT_VECTOR_INDEX setting and the expected stored - /// vectorIndexType on servers >= 1.37.5. Defaults to - /// when unset so local runs match - /// the production recommended default. - /// private const string DefaultVectorIndexEnvVar = "DEFAULT_VECTOR_INDEX"; - - /// - /// Fallback value when is unset. - /// private const string DefaultVectorIndexFallback = "hfresh"; + private static readonly Version ServerDefaultCutoff = Version.Parse( + ServerVersions.DefaultVectorIndexTypeServerSide + ); - private const string ImageRepo = "cr.weaviate.io/semitechnologies/weaviate"; - - /// - /// First Weaviate version that applies a server-side default for - /// vectorIndexType when the client omits it. - /// - private static readonly Version ServerDefaultCutoff = new(1, 37, 5); - - private readonly string _imageTag; - private readonly bool _serverAppliesDefault; - private readonly string _defaultVectorIndex; - private readonly List _containers = new(); - - public DefaultVectorIndexTypeIntegrationTests() + private string ExpectedDefaultIndexType() { - var defaultVectorIndexEnv = Environment.GetEnvironmentVariable(DefaultVectorIndexEnvVar); - _defaultVectorIndex = string.IsNullOrWhiteSpace(defaultVectorIndexEnv) - ? DefaultVectorIndexFallback - : defaultVectorIndexEnv; + Assert.NotNull(_weaviate.WeaviateVersion); - var envValue = Environment.GetEnvironmentVariable(ServerVersionEnvVar); - if (string.IsNullOrWhiteSpace(envValue)) + var defaultVectorIndex = Environment.GetEnvironmentVariable(DefaultVectorIndexEnvVar); + if (string.IsNullOrWhiteSpace(defaultVectorIndex)) { - throw new InvalidOperationException( - $"{ServerVersionEnvVar} env var is required for this integration test." - ); + defaultVectorIndex = DefaultVectorIndexFallback; } - _imageTag = envValue; - var parsedVersion = MetaInfo.ParseWeaviateVersion(_imageTag); - _serverAppliesDefault = parsedVersion is not null && parsedVersion >= ServerDefaultCutoff; - } - - public ValueTask InitializeAsync() => ValueTask.CompletedTask; - - public async ValueTask DisposeAsync() - { - foreach (var container in _containers) - { - try - { - await container.DisposeAsync(); - } - catch - { - // Best-effort cleanup; container disposal failures should not - // mask the actual test outcome. - } - } + return _weaviate.WeaviateVersion! >= ServerDefaultCutoff + ? defaultVectorIndex + : VectorIndex.HNSW.TypeValue; } - /// - /// Builds and starts a single-node Weaviate container at the configured - /// version with anonymous auth enabled and DEFAULT_VECTOR_INDEX - /// applied (value read from the host env var, default - /// ). Older servers ignore the - /// env var. Returns a wired to the - /// container; the client's meta cache is populated by BuildAsync. - /// - private async Task StartWeaviateAsync() - { - var image = $"{ImageRepo}:{_imageTag}"; - - var container = new ContainerBuilder(image) - .WithPortBinding(8080, assignRandomHostPort: true) - .WithPortBinding(50051, assignRandomHostPort: true) - .WithEnvironment("QUERY_DEFAULTS_LIMIT", "25") - .WithEnvironment("AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED", "true") - .WithEnvironment("PERSISTENCE_DATA_PATH", "/var/lib/weaviate") - .WithEnvironment("CLUSTER_HOSTNAME", "node1") - .WithEnvironment("DISABLE_TELEMETRY", "true") - .WithEnvironment("DEFAULT_VECTOR_INDEX", _defaultVectorIndex) - .WithCommand("--host", "0.0.0.0", "--port", "8080", "--scheme", "http") - .WithWaitStrategy( - Wait.ForUnixContainer() - .UntilHttpRequestIsSucceeded(req => - req.ForPort(8080).ForPath("/v1/.well-known/ready") - ) - ) - .Build(); - - _containers.Add(container); - - await container.StartAsync(TestContext.Current.CancellationToken); - - var restPort = container.GetMappedPublicPort(8080); - var grpcPort = container.GetMappedPublicPort(50051); - - var client = await WeaviateClientBuilder - .Local(restPort: restPort, grpcPort: grpcPort) - .BuildAsync(); - - // Sanity check: client should know the server version after BuildAsync, - // and it should agree with our env-derived gate so the test is - // self-consistent if a tag is mislabelled. - Assert.NotNull(client.WeaviateVersion); - Assert.Equal(_serverAppliesDefault, client.WeaviateVersion! >= ServerDefaultCutoff); - - return client; - } - - /// - /// Creates a collection on the connected server, reads the stored config - /// back, and returns the result of - /// applied to it. The collection is best-effort deleted before and after. - /// - private static async Task CreateAndReadAsync( - WeaviateClient client, - CollectionCreateParams create, - Func readStoredType - ) - { - await client.Collections.Delete(create.Name); - - var collection = await client.Collections.Create( - create, - TestContext.Current.CancellationToken - ); - try - { - var config = await collection.Config.Get( - cancellationToken: TestContext.Current.CancellationToken - ); - return readStoredType(config); - } - finally - { - await client.Collections.Delete(create.Name); - } - } - - /// - /// Returns the expected stored VectorIndexType for a vector that was - /// configured without an explicit index type. - /// - /// 1.37.5+: server applies DEFAULT_VECTOR_INDEX from the - /// container env var. - /// Older: client injects "hnsw". - /// - /// - private string ExpectedDefaultIndexType() => - _serverAppliesDefault ? _defaultVectorIndex : VectorIndex.HNSW.TypeValue; - /// /// Scenario A on the named-vector path: user omits VectorIndexType /// when configuring a named vector. @@ -225,27 +43,20 @@ private string ExpectedDefaultIndexType() => [Fact] public async Task NamedVector_NoIndexType_GetsServerOrInjectedDefault() { - var client = await StartWeaviateAsync(); - var create = new CollectionCreateParams { - Name = nameof(NamedVector_NoIndexType_GetsServerOrInjectedDefault), + Name = MakeUniqueCollectionName( + nameof(NamedVector_NoIndexType_GetsServerOrInjectedDefault) + ), Properties = [Property.Text("title")], VectorConfig = Configure.Vector("default", v => v.SelfProvided(), index: null), }; - var stored = await CreateAndReadAsync( - client, - create, - config => - { - Assert.NotNull(config.VectorConfig); - Assert.True(config.VectorConfig.ContainsKey("default")); - return config.VectorConfig["default"].VectorIndexType; - } - ); - - Assert.Equal(ExpectedDefaultIndexType(), stored); + var collection = await CollectionFactory(create); + var config = await collection.Config.Get(TestContext.Current.CancellationToken); + Assert.NotNull(config.VectorConfig); + Assert.True(config.VectorConfig.ContainsKey("default")); + Assert.Equal(ExpectedDefaultIndexType(), config.VectorConfig["default"].VectorIndexType); } /// @@ -257,11 +68,9 @@ public async Task NamedVector_NoIndexType_GetsServerOrInjectedDefault() [Fact] public async Task NamedVector_ExplicitFlat_IsPreserved() { - var client = await StartWeaviateAsync(); - var create = new CollectionCreateParams { - Name = nameof(NamedVector_ExplicitFlat_IsPreserved), + Name = MakeUniqueCollectionName(nameof(NamedVector_ExplicitFlat_IsPreserved)), Properties = [Property.Text("title")], VectorConfig = Configure.Vector( "default", @@ -270,38 +79,10 @@ public async Task NamedVector_ExplicitFlat_IsPreserved() ), }; - var stored = await CreateAndReadAsync( - client, - create, - config => - { - Assert.NotNull(config.VectorConfig); - Assert.True(config.VectorConfig.ContainsKey("default")); - return config.VectorConfig["default"].VectorIndexType; - } - ); - - Assert.Equal(VectorIndex.Flat.TypeValue, stored); + var collection = await CollectionFactory(create); + var config = await collection.Config.Get(TestContext.Current.CancellationToken); + Assert.NotNull(config.VectorConfig); + Assert.True(config.VectorConfig.ContainsKey("default")); + Assert.Equal(VectorIndex.Flat.TypeValue, config.VectorConfig["default"].VectorIndexType); } - - // The legacy single-vector path (CollectionCreateParams with no - // VectorConfig — the server stores the choice in the top-level - // `vectorIndexType` field) is intentionally NOT exercised here: - // - // * `CollectionCreateParams` does not expose a top-level - // `VectorIndexType` setter on the write side, so Scenario B - // (explicit flat) is not reachable from the public API. - // * On the read side, `Extensions.ToModel(Rest.Dto.Class)` does not - // copy the top-level `vectorIndexType` from the GET response into - // `CollectionConfig.VectorIndexType`; the field stays at its default - // regardless of what the server returned. End-to-end assertions on - // the top-level field therefore cannot pass until that parse path - // is fixed — a separate bug. - // - // The legacy top-level inject behaviour is locked in by the wire-level - // unit tests in - // `CollectionsClient.DefaultVectorIndexTypeTests.InjectLegacyDefaultVectorIndexType_*`, - // which assert directly on the outgoing DTO. The integration coverage - // here focuses on the named-vector path, which is the supported public - // API surface for picking an index type in modern code. } diff --git a/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj b/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj index f4bdaae8..c6370270 100644 --- a/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj +++ b/src/Weaviate.Client.Tests/Weaviate.Client.Tests.csproj @@ -37,7 +37,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Weaviate.Client.Tests/packages.lock.json b/src/Weaviate.Client.Tests/packages.lock.json index 01b37606..62194ad4 100644 --- a/src/Weaviate.Client.Tests/packages.lock.json +++ b/src/Weaviate.Client.Tests/packages.lock.json @@ -58,19 +58,6 @@ "resolved": "6.0.3", "contentHash": "hSHiq2m1ky7zUQgTp+/2h1K3lABIQ+GltRixoclHPg/Sc1vnfeS6g/Uy5moOVZKrZJdQiFPFZd6OobBp3tZcFg==" }, - "Testcontainers": { - "type": "Direct", - "requested": "[4.12.0, )", - "resolved": "4.12.0", - "contentHash": "PTZRdG1ZVkFMsFbc3cK/VUaOB5L3l4wYL+OkWAK33/cvgd/5FcmZlQ6NhMAl3PWBqYkpdWmeYmQW9U2OIXqtFA==", - "dependencies": { - "Docker.DotNet.Enhanced": "4.2.0", - "Docker.DotNet.Enhanced.X509": "4.2.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.3", - "SSH.NET": "2025.1.0", - "SharpZipLib": "1.4.2" - } - }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", @@ -86,72 +73,6 @@ "xunit.v3.mtp-v1": "[3.2.1]" } }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.6.2", - "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" - }, - "Docker.DotNet.Enhanced": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "tm2V/DpnaURbBhMQ7Z3orNR3u+H863KQuYfA/sgGjI5py07dEeV0I02f6pGrx2869KG9uNM/E96puf9i0gId2w==", - "dependencies": { - "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0", - "Docker.DotNet.Enhanced.LegacyHttp": "4.2.0", - "Docker.DotNet.Enhanced.NPipe": "4.2.0", - "Docker.DotNet.Enhanced.NativeHttp": "4.2.0", - "Docker.DotNet.Enhanced.Unix": "4.2.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.3" - } - }, - "Docker.DotNet.Enhanced.Handler.Abstractions": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "cQNxpdadEPdNdfjFCl9vgoCQIK3aVHRn1Qlu36aZUFpp4xHfPrk4hRPNVLR/CpobIFJ+dAt8AceTKMlCfPSccw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.3" - } - }, - "Docker.DotNet.Enhanced.LegacyHttp": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "sfbMX1HBPUec3PEMoqlP5ak6skXclcTBmu4gG3aUJatP34J2DgvYMP13bvz/rfrjVkAhPqnIiDKiHAkBCokajg==", - "dependencies": { - "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" - } - }, - "Docker.DotNet.Enhanced.NativeHttp": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "/ll+2ePYm1qrsMdgMO5BzCQnbfTGmPJAc9SqXEManbliVBZvEpBKHXLugx/OeEca2oC/b4RV+UNPtue5u4jAuA==", - "dependencies": { - "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" - } - }, - "Docker.DotNet.Enhanced.NPipe": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "8wyYOD6VkvqRkITwsvkt3UbW/1WDl6NFypNAsIIDaMiglNRzFrQcK0nK9VUEZa6Oja8Bso3UYySDoL8qatatAA==", - "dependencies": { - "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" - } - }, - "Docker.DotNet.Enhanced.Unix": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "x0wNcbww1+p9nUfw8i+JvsSArBDGkoZ9GI2PZ1wPo85B2OiFrdzp89omounNhO2GKyaIRWAqAm5jYZyNg9EnxA==", - "dependencies": { - "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" - } - }, - "Docker.DotNet.Enhanced.X509": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "nMw+FHGwGZieDi7kBgpIVl+E8MzjzXeXHvMQpidLADT06fts2Gw6G+K+p0hMGv7liZULxyYiZnQ1UbE2B9NNQg==", - "dependencies": { - "Docker.DotNet.Enhanced.Handler.Abstractions": "4.2.0" - } - }, "Duende.IdentityModel": { "type": "Transitive", "resolved": "7.1.0", @@ -474,20 +395,6 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "SharpZipLib": { - "type": "Transitive", - "resolved": "1.4.2", - "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" - }, - "SSH.NET": { - "type": "Transitive", - "resolved": "2025.1.0", - "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==", - "dependencies": { - "BouncyCastle.Cryptography": "2.6.2", - "Microsoft.Extensions.Logging.Abstractions": "8.0.3" - } - }, "System.Collections.Immutable": { "type": "Transitive", "resolved": "8.0.0", diff --git a/src/Weaviate.Client/CollectionConfigClient.cs b/src/Weaviate.Client/CollectionConfigClient.cs index cd1ece26..967bba5a 100644 --- a/src/Weaviate.Client/CollectionConfigClient.cs +++ b/src/Weaviate.Client/CollectionConfigClient.cs @@ -174,10 +174,14 @@ await _client.Collections.Export(_collectionName, cancellationToken) // 2. Apply everything that is not null over a collection export c(new CollectionUpdate(collection)); + var dto = collection.ToDto(); + if (_client.InjectLegacyVectorIndexDefault) + CollectionsClient.InjectLegacyDefaultVectorIndexType(dto); + // 3. PUT to /schema var result = await _client.RestClient.CollectionUpdate( _collectionName, - collection.ToDto(), + dto, cancellationToken ); From 972873dcc0147c391ae592729e4d672b9300527d Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sat, 23 May 2026 00:46:25 +0200 Subject: [PATCH 14/15] Fix integration tests: explicitly set HNSW index for tests that assert HNSW-specific values On Weaviate 1.37.5+, the server defaults vectorIndexType to 'hfresh' instead of 'hnsw' when no explicit index is provided. Tests that assert HNSW-specific properties (cleanupIntervalSeconds, quantizer, etc.) must now explicitly create collections with VectorIndex.HNSW to ensure consistent behavior. --- .../Integration/TestCollections.cs | 10 ++++++---- .../Integration/TestMultiVector.cs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/TestCollections.cs b/src/Weaviate.Client.Tests/Integration/TestCollections.cs index 2b3fd969..f66f7694 100644 --- a/src/Weaviate.Client.Tests/Integration/TestCollections.cs +++ b/src/Weaviate.Client.Tests/Integration/TestCollections.cs @@ -196,7 +196,7 @@ public async Task Test_Collections_Export() name: "MyOwnSuffix", description: "My own description too", properties: [Property.Text("Name")], - vectorConfig: Configure.Vector(v => v.SelfProvided()) + vectorConfig: Configure.Vector(v => v.SelfProvided(), index: new VectorIndex.HNSW()) ); var export = await _weaviate.Collections.Export( @@ -327,7 +327,8 @@ public async Task Test_Collections_Export_NonDefaultValues_Sharding() collectionNamePartSeparator: "", vectorConfig: Configure.Vector( "nondefault", - v => v.Text2VecTransformers(vectorizeCollectionName: false) + v => v.Text2VecTransformers(vectorizeCollectionName: false), + index: new VectorIndex.HNSW() ), invertedIndexConfig: new() { @@ -485,7 +486,8 @@ public async Task Test_Collections_Export_NonDefaultValues_MultiTenacy() collectionNamePartSeparator: "", vectorConfig: Configure.Vector( "nondefault", - v => v.Text2VecTransformers(vectorizeCollectionName: false) + v => v.Text2VecTransformers(vectorizeCollectionName: false), + index: new VectorIndex.HNSW() ), multiTenancyConfig: new() { @@ -736,7 +738,7 @@ public async Task Test_Collection_Config_Update() // Arrange var collection = await CollectionFactory( name: "TestCollectionUpdate", - vectorConfig: Configure.Vector(t => t.SelfProvided()), + vectorConfig: Configure.Vector(t => t.SelfProvided(), index: new VectorIndex.HNSW()), properties: [Property.Text("name"), Property.Int("age")], multiTenancyConfig: new() { diff --git a/src/Weaviate.Client.Tests/Integration/TestMultiVector.cs b/src/Weaviate.Client.Tests/Integration/TestMultiVector.cs index a4960481..8116ef14 100644 --- a/src/Weaviate.Client.Tests/Integration/TestMultiVector.cs +++ b/src/Weaviate.Client.Tests/Integration/TestMultiVector.cs @@ -82,7 +82,7 @@ public async Task Test_Should_Get_Config_Of_Created_Collection() name: "TestMultiVectorCollectionConfig", vectorConfig: new[] { - Configure.Vector("regular", v => v.SelfProvided()), + Configure.Vector("regular", v => v.SelfProvided(), index: new VectorIndex.HNSW()), Configure.MultiVector("colbert", v => v.SelfProvided()), } ); From 668d23e2ea205fc348176b09b5b9a897ea417ab5 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sat, 23 May 2026 01:21:01 +0200 Subject: [PATCH 15/15] Increase timeout for flaky batch test in CI ServerSideBatch_TaskHandleUuid_MatchesInserted passes locally (761ms) but times out in CI with resource constraints. Increase timeout from 10s to 30s to match other batch tests that also need more time in CI environments. --- src/Weaviate.Client.Tests/Integration/TestServerSideBatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Weaviate.Client.Tests/Integration/TestServerSideBatch.cs b/src/Weaviate.Client.Tests/Integration/TestServerSideBatch.cs index ec2d5f8c..c4f0cd3d 100644 --- a/src/Weaviate.Client.Tests/Integration/TestServerSideBatch.cs +++ b/src/Weaviate.Client.Tests/Integration/TestServerSideBatch.cs @@ -458,7 +458,7 @@ public async Task ServerSideBatch_InsertManyWithOptions_Success() /// /// Tests that InsertManyAsync returns proper UUIDs in handles /// - [Fact(Timeout = 10000)] + [Fact(Timeout = 30000)] public async Task ServerSideBatch_TaskHandleUuid_MatchesInserted() { // Arrange