From 55b0693e6bbad650e624ad79739cd3e389528607 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:07:14 +0200 Subject: [PATCH 01/14] fix: Multi2VecGoogleGemini emitted a module name no server has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi2VecGoogleGemini declared [Vectorizer("multi2vec-google-gemini")]. No such module exists: modules/multi2vec-google/module.go names the module "multi2vec-google" with the single alias "multi2vec-palm", and grepping the server for "multi2vec-google-gemini" returns nothing. A live 1.39.0 rejects it outright: 422 {"error":[{"message":"target vector \"default\": vectorizer: no module with name \"multi2vec-google-gemini\" present"}]} So the factory could not create a collection at all. Gemini is reached through the one Google multimodal module by pointing apiEndpoint at generativelanguage.googleapis.com — what python does with a single _Multi2VecGoogleConfig and a switched endpoint, and what this client already does on the text side in Text2VecGoogleGemini. Multi2VecGoogle grows that ApiEndpoint property and VectorizerFactory.Multi2VecGoogleGemini stays as the ergonomic entry point, now returning a Multi2VecGoogle. ProjectId and Location drop `required` and become nullable, matching python's Optional[str]. The Gemini API is scoped to neither, so both are null and the REST serializer omits them. This also unbreaks readback: a required member absent from the server's JSON makes System.Text.Json throw, so a Gemini config could not have round-tripped even once creation worked. Folds in the related gap: the Gemini path had neither Dimensions nor VectorizeCollectionName, both of which Multi2VecGoogle has and python passes. The Vertex overloads gain an apiEndpoint parameter, which the repo's own WEAVIATE002 analyzer requires once the property exists, and which mirrors Text2VecGoogle. Source-breaking for anyone binding the record type, so Multi2VecGoogleGemini survives as an [Obsolete] shim over Multi2VecGoogle following the Multi2VecPalm/Text2VecPalm precedent. It deliberately carries no [Vectorizer] attribute: VectorizerRegistry keys types by identifier and last write wins, so a second type claiming "multi2vec-palm" would make deserialization of every Google multimodal config depend on reflection order. Tests: the two existing Gemini unit cases now pin the module name, the endpoint and the absent Vertex fields; the string-array case carries the new dimensions/vectorizeCollectionName and the weighted case stays without them so omit-when-unset stays covered. New integration coverage creates both a Gemini and a Vertex collection against a real server and asserts the round trip; a RequireModule gate skips when the module is absent, and CI now enables multi2vec-google so it runs there. --- ci/docker-compose.yml | 2 +- .../Integration/TestVectorizers.cs | 101 ++++++++++++++++++ .../Integration/_Integration.cs | 14 +++ .../Unit/TestVectorizers.cs | 24 ++++- .../Configure/VectorizerFactory.cs | 50 +++++++-- src/Weaviate.Client/Models/Vectorizer.cs | 81 +++++--------- src/Weaviate.Client/PublicAPI.Unshipped.txt | 30 ++++++ 7 files changed, 238 insertions(+), 64 deletions(-) create mode 100644 src/Weaviate.Client.Tests/Integration/TestVectorizers.cs diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index b197578c..20485dfb 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -22,7 +22,7 @@ services: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' PERSISTENCE_DATA_PATH: '/var/lib/weaviate' - ENABLE_MODULES: text2vec-transformers,text2vec-cohere,backup-filesystem,generative-dummy,generative-anyscale,reranker-dummy,reranker-cohere,text2vec-ollama,generative-ollama + ENABLE_MODULES: text2vec-transformers,text2vec-cohere,backup-filesystem,generative-dummy,generative-anyscale,reranker-dummy,reranker-cohere,text2vec-ollama,generative-ollama,multi2vec-google BACKUP_FILESYSTEM_PATH: "/tmp/backups" EXPORT_ENABLED: 'true' EXPORT_DEFAULT_PATH: "/tmp/exports" diff --git a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs new file mode 100644 index 00000000..00083d07 --- /dev/null +++ b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs @@ -0,0 +1,101 @@ +namespace Weaviate.Client.Tests.Integration; + +using Weaviate.Client.Models; + +/// +/// The vectorizer module tests class. Covers vectorizer configurations end to end, i.e. that the +/// module name and settings the client emits are ones a real server accepts. +/// +/// +public class TestVectorizers : IntegrationTests +{ + /// + /// Tests that the Google Gemini multimodal factory produces a collection the server accepts. + /// This is the whole point of the factory and it used to be impossible: the config emitted + /// the module name multi2vec-google-gemini, which no Weaviate build provides, so + /// creation failed with no module with name "multi2vec-google-gemini" present. Gemini + /// is reached through multi2vec-google (wire name multi2vec-palm) by pointing + /// apiEndpoint at the generative-language host, so the round trip must show that + /// module, that endpoint, and no Vertex-only project id or location. + /// + [Fact] + public async Task Test_Multi2VecGoogleGemini_Creates_Collection() + { + RequireModule("multi2vec-google"); + + var collection = await CollectionFactory( + name: "TestMulti2VecGoogleGemini", + properties: [Property.Text("text"), Property.Blob("image")], + vectorConfig: Configure.Vector( + "default", + v => + v.Multi2VecGoogleGemini( + imageFields: ["image"], + textFields: ["text"], + dimensions: 512, + vectorizeCollectionName: false + ) + ) + ); + + var config = await collection.Config.Get( + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.NotNull(config); + var vectorizer = config.VectorConfig["default"].Vectorizer; + var google = Assert.IsType(vectorizer); + + Assert.Equal("multi2vec-palm", google.Identifier); + Assert.Equal("generativelanguage.googleapis.com", google.ApiEndpoint); + Assert.Equal(512, google.Dimensions); + Assert.False(google.VectorizeCollectionName); + Assert.NotNull(google.ImageFields); + Assert.Equal(["image"], google.ImageFields); + Assert.NotNull(google.TextFields); + Assert.Equal(["text"], google.TextFields); + // Vertex-only settings: the Gemini API has neither, and the server must not echo them. + Assert.Null(google.ProjectId); + Assert.Null(google.Location); + } + + /// + /// Tests that the Vertex AI multimodal factory still produces a collection the server + /// accepts, and that project id and location survive the round trip. Kept alongside the + /// Gemini case because both now share one config type: a regression that dropped either + /// field would otherwise only show up on the Vertex path. + /// + [Fact] + public async Task Test_Multi2VecGoogle_Vertex_Creates_Collection() + { + RequireModule("multi2vec-google"); + + var collection = await CollectionFactory( + name: "TestMulti2VecGoogleVertex", + properties: [Property.Text("text"), Property.Blob("image")], + vectorConfig: Configure.Vector( + "default", + v => + v.Multi2VecGoogle( + projectId: "my-project", + location: "us-central1", + imageFields: ["image"], + textFields: ["text"] + ) + ) + ); + + var config = await collection.Config.Get( + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.NotNull(config); + var vectorizer = config.VectorConfig["default"].Vectorizer; + var google = Assert.IsType(vectorizer); + + Assert.Equal("multi2vec-palm", google.Identifier); + Assert.Equal("my-project", google.ProjectId); + Assert.Equal("us-central1", google.Location); + Assert.Null(google.ApiEndpoint); + } +} diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index 600927fc..2753b840 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -418,6 +418,20 @@ protected bool ServerVersionIsInRange(string minimumVersion, string? maximumVers return VersionIsInRange(_weaviate.WeaviateVersion, minimumVersion, maximumVersion); } + /// + /// Skips the test unless the connected server has the named module enabled. + /// A module the server does not load cannot be named in a collection config at all, so + /// tests that create such a collection are meaningless — not failing — without it. + /// + /// The module name, e.g. multi2vec-google. + protected void RequireModule(string moduleName) + { + if (_weaviate.Meta?.Modules.ContainsKey(moduleName) != true) + { + Assert.Skip($"Weaviate module '{moduleName}' is not enabled on the test server."); + } + } + /// /// Requires the version using the specified minimum version /// diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs index 33a74b33..e39dee09 100644 --- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs @@ -347,6 +347,10 @@ public void Test_Multi2VecGoogle_Serializes_AudioFields_WeightedFields() /// /// Tests that Multi2VecGoogleGemini maps each string-array modality to its own key, and /// that the unweighted overload emits no weights object. + /// Also pins the module name: there is no multi2vec-google-gemini module on any + /// server, so the Gemini factory must emit the multi2vec-google module (under its + /// multi2vec-palm wire name) and select Gemini with apiEndpoint instead — + /// and must send neither projectId nor location, which are Vertex-only. /// [Fact] [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -364,7 +368,9 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() imageFields: new[] { "image" }, textFields: new[] { "text" }, videoFields: new[] { "video" }, - audioFields: new[] { "audio" } + audioFields: new[] { "audio" }, + dimensions: 512, + vectorizeCollectionName: false ) ); @@ -380,10 +386,20 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() ); // Assert + Assert.Contains("\"multi2vec-palm\"", json); + Assert.DoesNotContain("multi2vec-google-gemini", json); + Assert.Contains("\"apiEndpoint\":\"generativelanguage.googleapis.com\"", json); + // Vertex-only settings the Gemini API has no equivalent of. This test serializes with a + // bare options object, so nulls are written out; the REST client's options drop them, so + // neither key reaches the wire. + Assert.Contains("\"projectId\":null", json); + Assert.Contains("\"location\":null", json); Assert.Contains("\"imageFields\":[\"image\"]", json); Assert.Contains("\"textFields\":[\"text\"]", json); Assert.Contains("\"videoFields\":[\"video\"]", json); Assert.Contains("\"audioFields\":[\"audio\"]", json); + Assert.Contains("\"dimensions\":512", json); + Assert.Contains("\"vectorizeClassName\":false", json); Assert.DoesNotContain("\"weights\"", json); } @@ -430,6 +446,9 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields() ); // Assert + Assert.Contains("\"multi2vec-palm\"", json); + Assert.DoesNotContain("multi2vec-google-gemini", json); + Assert.Contains("\"apiEndpoint\":\"generativelanguage.googleapis.com\"", json); Assert.Contains("\"imageFields\":[\"image\",\"thumbnail\"]", json); Assert.Contains("\"textFields\":[\"text\"]", json); Assert.Contains("\"videoFields\":[\"video\",\"clip\"]", json); @@ -439,6 +458,9 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields() + "\"textFields\":[0.23],\"videoFields\":[0.33,0.34]}", json ); + // Left unset by this case, so the omit-when-null path stays covered. + Assert.Contains("\"dimensions\":null", json); + Assert.Contains("\"vectorizeClassName\":null", json); Assert.DoesNotContain("depthFields", json); Assert.DoesNotContain("imuFields", json); Assert.DoesNotContain("thermalFields", json); diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs index bef93a37..2075a51c 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactory.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs @@ -321,6 +321,7 @@ public VectorizerConfig Multi2VecBind( /// The model /// The dimensions /// The vectorize collection name + /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( string projectId, @@ -332,12 +333,14 @@ public VectorizerConfig Multi2VecGoogle( int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, - bool? vectorizeCollectionName = null + bool? vectorizeCollectionName = null, + string? apiEndpoint = null ) => new Multi2VecGoogle { ProjectId = projectId, Location = location, + ApiEndpoint = apiEndpoint, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), @@ -371,6 +374,7 @@ public VectorizerConfig Multi2VecGoogle( /// The model /// The dimensions /// The vectorize collection name + /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( string projectId, @@ -382,12 +386,14 @@ public VectorizerConfig Multi2VecGoogle( int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, - bool? vectorizeCollectionName = null + bool? vectorizeCollectionName = null, + string? apiEndpoint = null ) => new Multi2VecGoogle { ProjectId = projectId, Location = location, + ApiEndpoint = apiEndpoint, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), @@ -399,7 +405,9 @@ public VectorizerConfig Multi2VecGoogle( }; /// - /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API) + /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API). + /// Emits the multi2vec-google module with the Gemini API endpoint; there is no + /// separate Gemini module, so no project id or location is sent. /// /// The image fields /// The text fields @@ -408,6 +416,8 @@ public VectorizerConfig Multi2VecGoogle( /// The API endpoint /// The video interval seconds /// The model + /// The dimensions + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecGoogleGemini( WeightedFields imageFields, @@ -416,17 +426,25 @@ public VectorizerConfig Multi2VecGoogleGemini( WeightedFields audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, - string? model = null + string? model = null, + int? dimensions = null, + bool? vectorizeCollectionName = null ) => - new Multi2VecGoogleGemini + new Multi2VecGoogle { + // The Gemini API is not scoped to a Vertex AI project or region; left null so both + // are omitted on the wire. + ProjectId = null, + Location = null, ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com", ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), AudioFields = ModalityFields.OrNull(audioFields), VideoIntervalSeconds = videoIntervalSeconds, - Model = model, + ModelId = model, + Dimensions = dimensions, + VectorizeCollectionName = vectorizeCollectionName, // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -440,7 +458,9 @@ public VectorizerConfig Multi2VecGoogleGemini( }; /// - /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API) + /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API). + /// Emits the multi2vec-google module with the Gemini API endpoint; there is no + /// separate Gemini module, so no project id or location is sent. /// /// The image fields /// The text fields @@ -449,6 +469,8 @@ public VectorizerConfig Multi2VecGoogleGemini( /// The API endpoint /// The video interval seconds /// The model + /// The dimensions + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecGoogleGemini( string[]? imageFields = null, @@ -457,17 +479,25 @@ public VectorizerConfig Multi2VecGoogleGemini( string[]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, - string? model = null + string? model = null, + int? dimensions = null, + bool? vectorizeCollectionName = null ) => - new Multi2VecGoogleGemini + new Multi2VecGoogle { + // The Gemini API is not scoped to a Vertex AI project or region; left null so both + // are omitted on the wire. + ProjectId = null, + Location = null, ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com", ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), AudioFields = ModalityFields.OrNull(audioFields), VideoIntervalSeconds = videoIntervalSeconds, - Model = model, + ModelId = model, + Dimensions = dimensions, + VectorizeCollectionName = vectorizeCollectionName, }; /// diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs index cf66c1bf..a6a8c771 100644 --- a/src/Weaviate.Client/Models/Vectorizer.cs +++ b/src/Weaviate.Client/Models/Vectorizer.cs @@ -380,14 +380,25 @@ public record Multi2VecGoogle : VectorizerConfig internal Multi2VecGoogle() { } /// - /// Gets or sets the value of the project id + /// Gets or sets the Google Cloud project id. + /// Required by Vertex AI; omitted when null, as it is for the Gemini + /// (generative-language) API, which is not scoped to a project. /// - public required string ProjectId { get; set; } + public string? ProjectId { get; set; } = null; /// - /// Gets or sets the value of the location + /// Gets or sets the Google Vertex AI region. + /// Required by Vertex AI; omitted when null, as it is for the Gemini + /// (generative-language) API, which has no region. /// - public required string Location { get; set; } + public string? Location { get; set; } = null; + + /// + /// Gets or sets the value of the api endpoint. + /// Set to generativelanguage.googleapis.com to call the Gemini API instead of + /// Vertex AI; when omitted the server applies its default. + /// + public string? ApiEndpoint { get; set; } = null; /// /// Gets or sets the value of the image fields @@ -452,61 +463,27 @@ internal Multi2VecPalm() { } } /// - /// The configuration for multi-media vectorization using the Google Gemini module. - /// See the documentation for detailed usage. + /// Deprecated. Use Multi2VecGoogle instead. + /// This type used to declare the module name multi2vec-google-gemini, which no server + /// has ever provided, so a collection configured with it could not be created. The Gemini API + /// is reached through the multi2vec-google module by setting + /// to generativelanguage.googleapis.com, + /// which is what + /// now returns. Kept only so existing source that names the type still compiles. /// - [Vectorizer("multi2vec-google-gemini")] - public record Multi2VecGoogleGemini : VectorizerConfig + [Obsolete( + "Multi2VecGoogleGemini declared the non-existent module 'multi2vec-google-gemini' and " + + "could never create a collection. VectorizerFactory.Multi2VecGoogleGemini(...) now " + + "returns a Multi2VecGoogle with ApiEndpoint = 'generativelanguage.googleapis.com'; " + + "bind the result as Multi2VecGoogle (or VectorizerConfig) instead." + )] + public record Multi2VecGoogleGemini : Multi2VecGoogle { /// /// Initializes a new instance of the class /// [JsonConstructor] internal Multi2VecGoogleGemini() { } - - /// - /// Gets or sets the value of the api endpoint - /// - public string? ApiEndpoint { get; set; } = "generativelanguage.googleapis.com"; - - /// - /// Gets or sets the value of the image fields - /// - public string[]? ImageFields { get; set; } = null; - - /// - /// Gets or sets the value of the text fields - /// - public string[]? TextFields { get; set; } = null; - - /// - /// Gets or sets the value of the video fields - /// - public string[]? VideoFields { get; set; } = null; - - /// - /// Gets or sets the value of the audio fields - /// - public string[]? AudioFields { get; set; } = null; - - /// - /// Gets or sets the value of the video interval seconds - /// - public int? VideoIntervalSeconds { get; set; } = null; - - /// - /// Gets or sets the value of the model - /// - [JsonPropertyName("modelId")] - public string? Model { get; set; } = null; - - /// - /// Gets or sets the per-modality weights (the weights object), omitted when null. - /// [JsonInclude] is required: the serializer skips internal properties by default. - /// - [JsonInclude] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - internal VectorizerWeights? Weights { get; set; } = null; } /// diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index 30fd71ba..d8ae5b26 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -313,3 +313,33 @@ Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.TextFields.get -> string![ Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.TextFields.set -> void Weaviate.Client.VectorizerFactory.Multi2VecTwelveLabs(string![]? imageFields = null, string![]? textFields = null, string? baseURL = null, string? model = null) -> Weaviate.Client.Models.VectorizerConfig! Weaviate.Client.VectorizerFactory.Multi2VecTwelveLabs(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, string? baseURL = null, string? model = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.Location.get -> string! +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ProjectId.get -> string! +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ApiEndpoint.get -> string? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ApiEndpoint.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.AudioFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.AudioFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ImageFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ImageFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Model.get -> string? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Model.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.TextFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.TextFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoIntervalSeconds.get -> int? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoIntervalSeconds.set -> void +*REMOVED*override sealed Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Equals(Weaviate.Client.Models.VectorizerConfig? other) -> bool +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null) -> Weaviate.Client.Models.VectorizerConfig! +override sealed Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Equals(Weaviate.Client.Models.Vectorizer.Multi2VecGoogle? other) -> bool +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ApiEndpoint.get -> string? +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ApiEndpoint.set -> void +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.Location.get -> string? +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ProjectId.get -> string? +Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null, string? apiEndpoint = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null, string? apiEndpoint = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! From 0e83cd9e7b6005afeed57146b539c661b55a801b Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:11:42 +0200 Subject: [PATCH 02/14] feat: send incremental_base_backup_id on backup create The generated DTO has carried Incremental_base_backup_id since the 1.37 spec (Rest/Dto/Models.g.cs) and the server reads it (entities/models/backup_create_request.go:51), but nothing in the client could set it: BackupCreateRequest had no such member and BuildBackupCreateRequest never populated one. Asking for an incremental backup was impossible; python has had it since 4.20.2 (weaviate/backup/executor.py). The version gate is on the field rather than on the operation. A [RequiresWeaviateVersion] attribute on Create would refuse every backup on a pre-1.37 server, which is a regression for plain backups, so this follows CollectionsClient.EnsureTextAnalyzerFeaturesSupported instead: check only when the caller actually supplied a base id, and throw the same WeaviateVersionMismatchException the rest of the client's gates throw. Python gates identically (executor.py:92-96). The base id is passed through verbatim rather than lowercased. Python lowers it, but this client does not lower request.Id either, and applying the rule to one id and not the other would be the surprising behaviour. Tests cover all four quadrants: the key reaches the wire when set, is absent when unset, a pre-1.37 server rejects an incremental request with the right RequiredVersion/ActualVersion, and a plain backup on that same old server still succeeds. --- .../Unit/TestBackupClient.cs | 152 ++++++++++++++++++ src/Weaviate.Client/BackupClient.cs | 39 +++++ src/Weaviate.Client/Models/Backup.cs | 14 +- src/Weaviate.Client/PublicAPI.Unshipped.txt | 6 + 4 files changed, 210 insertions(+), 1 deletion(-) diff --git a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs index f7daeb46..1b87d344 100644 --- a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs +++ b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs @@ -216,4 +216,156 @@ BackupStatus expected Assert.Equal(expected, backup.Status); } + + /// + /// A create response body, reused by the incremental-backup cases below. + /// + private const string CreateResponseJson = """ + { + "id": "my-backup", + "backend": "filesystem", + "status": "STARTED", + "path": "/backups" + } + """; + + /// + /// Create() must put IncrementalBaseBackupId on the wire under the spec's + /// incremental_base_backup_id key. Without it the client silently drops the + /// caller's request and takes a full backup instead of an incremental one. + /// + [Fact] + public async Task Create_SendsIncrementalBaseBackupId() + { + var (client, handler) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: req => + req.Method == HttpMethod.Post + ? new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + CreateResponseJson, + System.Text.Encoding.UTF8, + "application/json" + ), + } + : null!, + serverVersion: "1.37.0" + ); + + await client.Backup.Create( + new BackupCreateRequest( + "my-backup", + new FilesystemBackend("/backups"), + IncrementalBaseBackupId: "base-backup" + ), + TestContext.Current.CancellationToken + ); + + Assert.NotNull(handler.LastRequest); + var body = await handler.LastRequest!.Content!.ReadAsStringAsync( + TestContext.Current.CancellationToken + ); + Assert.Contains("\"incremental_base_backup_id\":\"base-backup\"", body); + } + + /// + /// A create request that asks for no base backup must not send the key at all, so a plain + /// backup is unchanged. + /// + [Fact] + public async Task Create_OmitsIncrementalBaseBackupId_WhenNotRequested() + { + var (client, handler) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: req => + req.Method == HttpMethod.Post + ? new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + CreateResponseJson, + System.Text.Encoding.UTF8, + "application/json" + ), + } + : null!, + serverVersion: "1.37.0" + ); + + await client.Backup.Create( + new BackupCreateRequest("my-backup", new FilesystemBackend("/backups")), + TestContext.Current.CancellationToken + ); + + Assert.NotNull(handler.LastRequest); + var body = await handler.LastRequest!.Content!.ReadAsStringAsync( + TestContext.Current.CancellationToken + ); + Assert.DoesNotContain("incremental_base_backup_id", body); + } + + /// + /// Incremental backups arrived in Weaviate 1.37.0, so asking for one against an older + /// server must fail in the client rather than silently producing a full backup. + /// + [Fact] + public async Task Create_Throws_WhenIncrementalRequestedOnOlderServer() + { + var (client, _) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: req => + req.Method == HttpMethod.Post + ? new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + CreateResponseJson, + System.Text.Encoding.UTF8, + "application/json" + ), + } + : null!, + serverVersion: "1.36.0" + ); + + var exception = await Assert.ThrowsAsync(async () => + await client.Backup.Create( + new BackupCreateRequest( + "my-backup", + new FilesystemBackend("/backups"), + IncrementalBaseBackupId: "base-backup" + ), + TestContext.Current.CancellationToken + ) + ); + + Assert.Equal(new Version(1, 37, 0), exception.RequiredVersion); + Assert.Equal(new Version(1, 36, 0), exception.ActualVersion); + } + + /// + /// The version gate is on the field, not the operation: a plain backup must still work on a + /// server older than 1.37.0. + /// + [Fact] + public async Task Create_PlainBackup_Succeeds_OnOlderServer() + { + var (client, handler) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: req => + req.Method == HttpMethod.Post + ? new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + CreateResponseJson, + System.Text.Encoding.UTF8, + "application/json" + ), + } + : null!, + serverVersion: "1.36.0" + ); + + await client.Backup.Create( + new BackupCreateRequest("my-backup", new FilesystemBackend("/backups")), + TestContext.Current.CancellationToken + ); + + Assert.NotNull(handler.LastRequest); + } } diff --git a/src/Weaviate.Client/BackupClient.cs b/src/Weaviate.Client/BackupClient.cs index 30c36234..e5a42076 100644 --- a/src/Weaviate.Client/BackupClient.cs +++ b/src/Weaviate.Client/BackupClient.cs @@ -56,6 +56,8 @@ public async Task Create( CancellationToken cancellationToken = default ) { + await EnsureIncrementalBackupSupported(request); + var restRequest = BuildBackupCreateRequest(request); var response = await _client.RestClient.BackupCreate( request.Backend.Provider, @@ -85,6 +87,42 @@ public async Task CreateSync( return await operation.WaitForCompletion(timeout, cancellationToken); } + /// + /// The first Weaviate server version that accepts a file-based incremental backup base. + /// + private static readonly Version IncrementalBackupMinimumVersion = new(1, 37, 0); + + /// + /// Throws when the request asks for an + /// incremental backup and the connected server predates support for it. Gated on the field + /// rather than on the whole operation, because plain backups still work on older servers. + /// + /// The request + /// + /// Thrown when is set and the + /// connected server version is below 1.37.0. + /// + private async Task EnsureIncrementalBackupSupported(BackupCreateRequest request) + { + if (string.IsNullOrEmpty(request.IncrementalBaseBackupId)) + return; + + await _client.EnsureInitializedAsync(); + + var serverVersion = _client.WeaviateVersion; + if (serverVersion is null) + return; + + if (serverVersion < IncrementalBackupMinimumVersion) + { + throw new WeaviateVersionMismatchException( + nameof(BackupCreateRequest.IncrementalBaseBackupId), + IncrementalBackupMinimumVersion, + serverVersion + ); + } + } + /// /// Builds the backup create request using the specified request /// @@ -101,6 +139,7 @@ private Rest.Dto.BackupCreateRequest BuildBackupCreateRequest(BackupCreateReques Id = request.Id, Include = request.IncludeCollections?.ToList(), Exclude = request.ExcludeCollections?.ToList(), + Incremental_base_backup_id = request.IncrementalBaseBackupId, Config = new Rest.Dto.BackupConfig { Bucket = bucket, diff --git a/src/Weaviate.Client/Models/Backup.cs b/src/Weaviate.Client/Models/Backup.cs index 95adcb39..1528bf82 100644 --- a/src/Weaviate.Client/Models/Backup.cs +++ b/src/Weaviate.Client/Models/Backup.cs @@ -300,13 +300,25 @@ public record Backup( /// /// Options for creating a backup /// +/// The identifier of the backup to create. +/// The storage backend to write the backup to. +/// The collections to include; all when null. +/// The collections to exclude; none when null. +/// The share of CPU the backup may use. +/// The compression level to write with. +/// +/// The identifier of an existing backup to use as the base for a file-based incremental backup. +/// Files unchanged since the base backup are not copied again and are restored from the base. +/// Requires Weaviate server version 1.37.0 or later. +/// public record BackupCreateRequest( string Id, BackupBackend Backend, AutoArray? IncludeCollections = null, AutoArray? ExcludeCollections = null, int? CPUPercentage = null, - BackupCompressionLevel? CompressionLevel = null + BackupCompressionLevel? CompressionLevel = null, + string? IncrementalBaseBackupId = null ); /// diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index d8ae5b26..1c5bc8f5 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -343,3 +343,9 @@ Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! loc Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null, string? apiEndpoint = null) -> Weaviate.Client.Models.VectorizerConfig! Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.Models.BackupCreateRequest.BackupCreateRequest(string! Id, Weaviate.Client.Models.BackupBackend! Backend, Weaviate.Client.Internal.AutoArray? IncludeCollections = null, Weaviate.Client.Internal.AutoArray? ExcludeCollections = null, int? CPUPercentage = null, Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel = null) -> void +*REMOVED*Weaviate.Client.Models.BackupCreateRequest.Deconstruct(out string! Id, out Weaviate.Client.Models.BackupBackend! Backend, out Weaviate.Client.Internal.AutoArray? IncludeCollections, out Weaviate.Client.Internal.AutoArray? ExcludeCollections, out int? CPUPercentage, out Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel) -> void +Weaviate.Client.Models.BackupCreateRequest.BackupCreateRequest(string! Id, Weaviate.Client.Models.BackupBackend! Backend, Weaviate.Client.Internal.AutoArray? IncludeCollections = null, Weaviate.Client.Internal.AutoArray? ExcludeCollections = null, int? CPUPercentage = null, Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel = null, string? IncrementalBaseBackupId = null) -> void +Weaviate.Client.Models.BackupCreateRequest.Deconstruct(out string! Id, out Weaviate.Client.Models.BackupBackend! Backend, out Weaviate.Client.Internal.AutoArray? IncludeCollections, out Weaviate.Client.Internal.AutoArray? ExcludeCollections, out int? CPUPercentage, out Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel, out string? IncrementalBaseBackupId) -> void +Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.get -> string? +Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.init -> void From 6c647932f5fc0aca52ce5b4169754aca50e9df41 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:14:34 +0200 Subject: [PATCH 03/14] fix: backup list dropped Size and the incremental base id it had parsed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ToModelListItem read the list payload and then threw two fields away. Size is the outright bug: the property exists on Backup, the create-status path fills it, and Anonymous3 carries it — but the list mapper never assigned it, so every listed backup reported Size == null whatever the server said. The incremental base id had nowhere to go, so Backup gains it. Python added the same field to BackupListReturn in 4.23.0. Anonymous3 is an nswag anonymous-schema name, so it was re-checked rather than assumed: Rest/Backup.cs decodes the list response as List, and that record's shape (id/classes/status/startedAt/completedAt/size/incremental_base_backup_id) matches the list item in the spec. The create-status response carries incremental_base_backup_id too, and the Backup model is shared between both paths, so that mapper sets it as well — otherwise the field would have been silently null on the status path, which is the same defect this commit removes from the list path. --- .../Unit/TestBackupClient.cs | 111 ++++++++++++++++++ src/Weaviate.Client/BackupClient.cs | 7 +- src/Weaviate.Client/Models/Backup.cs | 6 + src/Weaviate.Client/PublicAPI.Unshipped.txt | 2 + 4 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs index 1b87d344..8099822f 100644 --- a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs +++ b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs @@ -368,4 +368,115 @@ await client.Backup.Create( Assert.NotNull(handler.LastRequest); } + + /// + /// List() parses the list payload and then discarded two fields it had already read: Size + /// was left null for every listed backup even though the property exists and the create + /// status path fills it, and the incremental base id had nowhere to go at all. Both must + /// survive onto the model. + /// + [Fact] + public async Task List_PopulatesSizeAndIncrementalBaseBackupId() + { + var json = """ + [ + { + "id": "my-backup", + "classes": ["Article"], + "status": "SUCCESS", + "startedAt": "2026-08-14T10:00:00Z", + "completedAt": "2026-08-14T10:05:00Z", + "size": 2.5, + "incremental_base_backup_id": "base-backup" + } + ] + """; + + var (client, _) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"), + } + ); + + var backups = await client.Backup.List( + BackupStorageProvider.Filesystem, + TestContext.Current.CancellationToken + ); + + var backup = Assert.Single(backups); + Assert.Equal("my-backup", backup.Id); + Assert.Equal(2.5, backup.Size); + Assert.Equal("base-backup", backup.IncrementalBaseBackupId); + } + + /// + /// A full (non-incremental) backup has no base, and the server omits the key entirely, so + /// the model must report null rather than an empty string. + /// + [Fact] + public async Task List_IncrementalBaseBackupIdIsNull_ForFullBackup() + { + var json = """ + [ + { + "id": "my-backup", + "classes": ["Article"], + "status": "SUCCESS", + "size": 2.5 + } + ] + """; + + var (client, _) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"), + } + ); + + var backups = await client.Backup.List( + BackupStorageProvider.Filesystem, + TestContext.Current.CancellationToken + ); + + var backup = Assert.Single(backups); + Assert.Null(backup.IncrementalBaseBackupId); + Assert.Equal(2.5, backup.Size); + } + + /// + /// The create-status response carries the same field, and the model is shared with the list + /// path, so leaving it unset there would reintroduce the same silent null. + /// + [Fact] + public async Task GetStatus_PopulatesIncrementalBaseBackupId() + { + var json = """ + { + "id": "my-backup", + "status": "SUCCESS", + "path": "/backups", + "backend": "filesystem", + "size": 1.5, + "incremental_base_backup_id": "base-backup" + } + """; + + var (client, _) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"), + } + ); + + var backup = await client.Backup.GetStatus( + new FilesystemBackend("/backups"), + "my-backup", + TestContext.Current.CancellationToken + ); + + Assert.Equal("base-backup", backup.IncrementalBaseBackupId); + Assert.Equal(1.5, backup.Size); + } } diff --git a/src/Weaviate.Client/BackupClient.cs b/src/Weaviate.Client/BackupClient.cs index e5a42076..99cce289 100644 --- a/src/Weaviate.Client/BackupClient.cs +++ b/src/Weaviate.Client/BackupClient.cs @@ -435,6 +435,7 @@ private static Backup ToModel(Rest.Dto.BackupCreateStatusResponse dto, BackupBac ) { Size = dto.Size, + IncrementalBaseBackupId = dto.Incremental_base_backup_id, }; /// @@ -487,5 +488,9 @@ private static Backup ToModelListItem(Rest.Dto.Anonymous3 dto) => dto.StartedAt, dto.CompletedAt, null - ); + ) + { + Size = dto.Size, + IncrementalBaseBackupId = dto.Incremental_base_backup_id, + }; } diff --git a/src/Weaviate.Client/Models/Backup.cs b/src/Weaviate.Client/Models/Backup.cs index 1528bf82..f531168f 100644 --- a/src/Weaviate.Client/Models/Backup.cs +++ b/src/Weaviate.Client/Models/Backup.cs @@ -295,6 +295,12 @@ public record Backup( /// Gets the size of the backup in GiB. Available after completion. /// public double? Size { get; init; } + + /// + /// Gets the id of the base backup this backup was built on, or null when the backup is not + /// incremental. + /// + public string? IncrementalBaseBackupId { get; init; } } /// diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index 1c5bc8f5..ffc35323 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -349,3 +349,5 @@ Weaviate.Client.Models.BackupCreateRequest.BackupCreateRequest(string! Id, Weavi Weaviate.Client.Models.BackupCreateRequest.Deconstruct(out string! Id, out Weaviate.Client.Models.BackupBackend! Backend, out Weaviate.Client.Internal.AutoArray? IncludeCollections, out Weaviate.Client.Internal.AutoArray? ExcludeCollections, out int? CPUPercentage, out Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel, out string? IncrementalBaseBackupId) -> void Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.get -> string? Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.init -> void +Weaviate.Client.Models.Backup.IncrementalBaseBackupId.get -> string? +Weaviate.Client.Models.Backup.IncrementalBaseBackupId.init -> void From 4ef693c3c774b36b9edb60a567cbada5b7c31770 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:29:32 +0200 Subject: [PATCH 04/14] feat: add the generative-deepseek module (Weaviate 1.39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client had zero references to generative-deepseek. Adds the collection config (GenerativeConfig.Deepseek + GenerativeConfigFactory.Deepseek + the serialization arm) and the runtime provider (Providers.Deepseek + GenerativeProviderFactory.Deepseek + the Search.Builders mapping), following the Databricks precedent throughout. Wire keys are taken from the server, not from the C# property names: modules/generative-deepseek/config/class_settings.go spells the base url "baseURL", and the camelCase policy happens to produce exactly that, so no [JsonPropertyName] is needed. That is load-bearing rather than lucky — a baseUrl/baseURL slip is silent, since the server ignores an unknown key and quietly uses its own endpoint — so the unit test pins the literal string, and was confirmed to fail when the casing is forced the other way. Includes a vendored proto sync. src/Weaviate.Client/gRPC/proto/v1/ generative.proto was one feature behind upstream: GenerativeDeepseek was missing from the GenerativeProvider oneof (field 16) along with the message itself and GenerativeDeepseekMetadata, so the runtime provider could not be expressed at all. The four hunks are copied verbatim from Weaviate v1.39.0's grpc/proto/v1/generative.proto; the vendored file now differs from upstream only by the deliberate `option csharp_namespace` line, and no other drift was found in it. The metadata message is unused today — the client does not read GenerativeMetadata anywhere — but is included so the file stays a faithful copy and the next sync is a clean diff. Note for the integration test: on a class using named vectors, Weaviate 1.39.0 rejects any fractional float in this module's config, e.g. temperature 0.7 comes back as "Wrong temperature configuration, values are between 0.0 and 2.0". The client sends ordinary JSON numbers and the same body is accepted when the class uses a class-level vectorizer, so this is a server defect: the value arrives as a json.Number, getNumberValue's non-integer path returns the caller's defaultValue, and this module passes a -100 sentinel there. generative-openai fails the same way; generative-cohere does not. The integration test therefore uses integral floats so it exercises all eight keys against a real server, and the unit test carries the fractional values. --- ci/docker-compose.yml | 2 +- .../Integration/TestCollections.cs | 58 +++++++++++++ .../Unit/TestCollection.cs | 74 +++++++++++++++++ .../Unit/TestGenerativeShortcuts.cs | 83 +++++++++++++++++++ .../Configure/GenerativeConfig.cs | 34 ++++++++ .../Configure/GenerativeProvider.cs | 34 ++++++++ .../Models/Generative/Providers.cs | 46 ++++++++++ .../Models/GenerativeConfig.cs | 62 ++++++++++++++ .../Models/Serialization.GenerativeConfig.cs | 5 ++ src/Weaviate.Client/PublicAPI.Unshipped.txt | 60 ++++++++++++++ src/Weaviate.Client/gRPC/Search.Builders.cs | 13 +++ .../gRPC/proto/v1/generative.proto | 22 +++++ 12 files changed, 492 insertions(+), 1 deletion(-) diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index 20485dfb..72d71416 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -22,7 +22,7 @@ services: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' PERSISTENCE_DATA_PATH: '/var/lib/weaviate' - ENABLE_MODULES: text2vec-transformers,text2vec-cohere,backup-filesystem,generative-dummy,generative-anyscale,reranker-dummy,reranker-cohere,text2vec-ollama,generative-ollama,multi2vec-google + ENABLE_MODULES: text2vec-transformers,text2vec-cohere,backup-filesystem,generative-dummy,generative-anyscale,reranker-dummy,reranker-cohere,text2vec-ollama,generative-ollama,multi2vec-google,generative-deepseek BACKUP_FILESYSTEM_PATH: "/tmp/backups" EXPORT_ENABLED: 'true' EXPORT_DEFAULT_PATH: "/tmp/exports" diff --git a/src/Weaviate.Client.Tests/Integration/TestCollections.cs b/src/Weaviate.Client.Tests/Integration/TestCollections.cs index b81b6953..62075890 100644 --- a/src/Weaviate.Client.Tests/Integration/TestCollections.cs +++ b/src/Weaviate.Client.Tests/Integration/TestCollections.cs @@ -193,6 +193,64 @@ public async Task Collection_Creates_And_Retrieves_Generative_Config() Assert.IsType(collection.GenerativeConfig); } + /// + /// Tests that a generative-deepseek collection round-trips through a real server. The module + /// validates its own settings on create (temperature, both penalties and topP all have + /// ranges, and baseURL is parsed), so a misspelt key or a wrong type shows up here rather + /// than silently falling back to a default. + /// + /// + /// Every float here is deliberately integral. On a class that uses named vectors, Weaviate + /// 1.39.0 rejects any fractional float in this module's config — temperature: 0.7 + /// comes back as "Wrong temperature configuration, values are between 0.0 and 2.0". The + /// client sends plain JSON numbers and the same payload is accepted on a class-level + /// vectorizer, so this is a server-side defect, not a client one: the settings helper + /// receives the value as a json.Number, its non-integer path returns the caller's + /// "defaultValue", and generative-deepseek passes a -100 sentinel there + /// (usecases/modulecomponents/settings/class_settings_property_helper.go getNumberValue, + /// modules/generative-deepseek/config/class_settings.go getFloatProperty). + /// generative-openai fails identically. Fractional values and the exact wire keys are + /// covered by the unit test instead. + /// + [Fact] + public async Task Collection_Creates_And_Retrieves_GenerativeDeepseek_Config() + { + RequireModule("generative-deepseek"); + + // Arrange + var collectionClient = await CollectionFactory( + properties: [Property.Text("Name")], + generativeConfig: Configure.Generative.Deepseek( + model: "deepseek-chat", + temperature: 1, + maxTokens: 2048, + frequencyPenalty: 0, + presencePenalty: 0, + topP: 1, + baseURL: "https://api.deepseek.com", + stop: ["\n\n"] + ) + ); + + // Act + var collection = await _weaviate + .Collections.Use(collectionClient.Name) + .Config.Get(TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(collection); + var deepseek = Assert.IsType(collection.GenerativeConfig); + Assert.Equal("deepseek-chat", deepseek.Model); + Assert.Equal(1, deepseek.Temperature); + Assert.Equal(2048, deepseek.MaxTokens); + Assert.Equal(0, deepseek.FrequencyPenalty); + Assert.Equal(0, deepseek.PresencePenalty); + Assert.Equal(1, deepseek.TopP); + Assert.Equal("https://api.deepseek.com", deepseek.BaseURL); + Assert.NotNull(deepseek.Stop); + Assert.Equal(["\n\n"], deepseek.Stop); + } + /// /// Tests that test collections export /// diff --git a/src/Weaviate.Client.Tests/Unit/TestCollection.cs b/src/Weaviate.Client.Tests/Unit/TestCollection.cs index 7f5ca918..9cc8290b 100644 --- a/src/Weaviate.Client.Tests/Unit/TestCollection.cs +++ b/src/Weaviate.Client.Tests/Unit/TestCollection.cs @@ -388,6 +388,80 @@ public void Collection_GenerativeGoogleVertex_Serializes_And_RoundTrips_Location Assert.Equal("us-east4", typed.Location); } + /// + /// Tests that the DeepSeek generative config serializes every setting under the exact key + /// the module reads, omits what is unset, and round-trips. The keys are pinned literally + /// against modules/generative-deepseek/config/class_settings.go, in particular + /// baseURL: the camelCase policy happens to produce the right casing here, but a + /// baseUrl/baseURL slip is silent — the server just ignores the unknown key + /// and falls back to its default endpoint. + /// + [Fact] + public void Collection_GenerativeDeepseek_Serializes_And_RoundTrips() + { + // Arrange + var full = Assert.IsType( + Configure.Generative.Deepseek( + model: "deepseek-chat", + temperature: 0.7, + maxTokens: 2048, + frequencyPenalty: 0.5, + presencePenalty: 0.25, + topP: 0.9, + baseURL: "https://api.deepseek.com", + stop: ["\n\n"] + ) + ); + var empty = Assert.IsType(Configure.Generative.Deepseek()); + + // Act + var jsonFull = JsonSerializer.Serialize( + full, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ); + var jsonEmpty = JsonSerializer.Serialize( + empty, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ); + + // Assert + Assert.Equal("generative-deepseek", full.Type); + Assert.Contains("\"model\":\"deepseek-chat\"", jsonFull); + Assert.Contains("\"temperature\":0.7", jsonFull); + Assert.Contains("\"maxTokens\":2048", jsonFull); + Assert.Contains("\"frequencyPenalty\":0.5", jsonFull); + Assert.Contains("\"presencePenalty\":0.25", jsonFull); + Assert.Contains("\"topP\":0.9", jsonFull); + Assert.Contains("\"baseURL\":\"https://api.deepseek.com\"", jsonFull); + Assert.Contains("\"stop\":[\"\\n\\n\"]", jsonFull); + // Every generative config also writes its own "type": IGenerativeConfig.Type carries + // [JsonIgnore] but attributes on an interface member do not apply to the implementing + // property. Pre-existing for all providers and accepted by the server; asserted here so + // DeepSeek is shown to behave the same rather than differently. + Assert.Contains("\"type\":\"generative-deepseek\"", jsonFull); + + // Nothing set means nothing else sent, so the server keeps its own defaults. + Assert.Equal("{\"type\":\"generative-deepseek\"}", jsonEmpty); + + var roundTripped = GenerativeConfigSerialization.Factory( + GenerativeConfig.Deepseek.TypeValue, + JsonSerializer.Deserialize( + jsonFull, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ) + ); + var typed = Assert.IsType(roundTripped); + Assert.Equal("deepseek-chat", typed.Model); + Assert.Equal(0.7, typed.Temperature); + Assert.Equal(2048, typed.MaxTokens); + Assert.Equal(0.5, typed.FrequencyPenalty); + Assert.Equal(0.25, typed.PresencePenalty); + Assert.Equal(0.9, typed.TopP); + Assert.Equal("https://api.deepseek.com", typed.BaseURL); + Assert.NotNull(typed.Stop); + Assert.Equal(["\n\n"], typed.Stop); + } + /// /// Tests that collection rerank deserializes into i reranker config /// diff --git a/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs b/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs index 67ce6621..b3ab293e 100644 --- a/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs +++ b/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs @@ -310,6 +310,89 @@ await client Assert.Equal(0.5f, providerQuery.Mistral.Temperature); } + /// + /// Tests that a runtime DeepSeek provider reaches the request as GenerativeDeepseek with + /// every setting mapped to its own proto field. Unlike the collection config, the runtime + /// provider travels over gRPC, so this also pins that the vendored generative.proto carries + /// the deepseek member of the provider oneof. + /// + [Fact] + public async Task GenerateClient_FetchObjects_WithDeepseekProvider_MapsAllFields() + { + // Arrange + var provider = new Providers.Deepseek + { + BaseUrl = "https://api.deepseek.com", + Model = "deepseek-chat", + Temperature = 0.7, + MaxTokens = 2048, + FrequencyPenalty = 0.5, + PresencePenalty = 0.25, + TopP = 0.9, + Stop = ["\n\n"], + }; + var (client, getCapturedRequest) = CreateClientWithRequestCapture(); + + // Act + await client + .Collections.Use("TestCollection") + .Generate.FetchObjects( + limit: 10, + singlePrompt: "Summarize this", + provider: provider, + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var capturedRequest = getCapturedRequest(); + Assert.NotNull(capturedRequest); + var providerQuery = capturedRequest!.Generative!.Single!.Queries[0]; + Assert.NotNull(providerQuery.Deepseek); + Assert.Equal("https://api.deepseek.com", providerQuery.Deepseek.BaseUrl); + Assert.Equal("deepseek-chat", providerQuery.Deepseek.Model); + Assert.Equal(0.7, providerQuery.Deepseek.Temperature); + Assert.Equal(2048, providerQuery.Deepseek.MaxTokens); + Assert.Equal(0.5, providerQuery.Deepseek.FrequencyPenalty); + Assert.Equal(0.25, providerQuery.Deepseek.PresencePenalty); + Assert.Equal(0.9, providerQuery.Deepseek.TopP); + Assert.Equal(["\n\n"], providerQuery.Deepseek.Stop.Values); + } + + /// + /// Tests that an unset optional stays unset rather than travelling as a proto default, so + /// the server applies its own default instead of a client-invented zero. + /// + [Fact] + public async Task GenerateClient_FetchObjects_WithBareDeepseekProvider_LeavesOptionalsUnset() + { + // Arrange + var provider = new Providers.Deepseek { Model = "deepseek-reasoner" }; + var (client, getCapturedRequest) = CreateClientWithRequestCapture(); + + // Act + await client + .Collections.Use("TestCollection") + .Generate.FetchObjects( + limit: 10, + singlePrompt: "Summarize this", + provider: provider, + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var capturedRequest = getCapturedRequest(); + Assert.NotNull(capturedRequest); + var providerQuery = capturedRequest!.Generative!.Single!.Queries[0]; + Assert.NotNull(providerQuery.Deepseek); + Assert.Equal("deepseek-reasoner", providerQuery.Deepseek.Model); + Assert.False(providerQuery.Deepseek.HasTemperature); + Assert.False(providerQuery.Deepseek.HasMaxTokens); + Assert.False(providerQuery.Deepseek.HasFrequencyPenalty); + Assert.False(providerQuery.Deepseek.HasPresencePenalty); + Assert.False(providerQuery.Deepseek.HasTopP); + Assert.Null(providerQuery.Deepseek.Stop); + } + #endregion #region Helper Methods diff --git a/src/Weaviate.Client/Configure/GenerativeConfig.cs b/src/Weaviate.Client/Configure/GenerativeConfig.cs index 17ed0fad..9a87b4dd 100644 --- a/src/Weaviate.Client/Configure/GenerativeConfig.cs +++ b/src/Weaviate.Client/Configure/GenerativeConfig.cs @@ -201,6 +201,40 @@ public IGenerativeConfig Databricks( TopP = topP, }; + /// + /// Create a generative configuration for DeepSeek. + /// + /// The model to use. + /// The temperature. + /// The maximum number of tokens to generate. + /// The frequency penalty. + /// The presence penalty. + /// The top P. + /// The base URL. + /// The stop sequences. + /// A instance. + public IGenerativeConfig Deepseek( + string? model = null, + double? temperature = null, + int? maxTokens = null, + double? frequencyPenalty = null, + double? presencePenalty = null, + double? topP = null, + string? baseURL = null, + string[]? stop = null + ) => + new GenerativeConfig.Deepseek + { + Model = model, + Temperature = temperature, + MaxTokens = maxTokens, + FrequencyPenalty = frequencyPenalty, + PresencePenalty = presencePenalty, + TopP = topP, + BaseURL = baseURL, + Stop = stop, + }; + /// /// Create a generative configuration for FriendliAI. /// diff --git a/src/Weaviate.Client/Configure/GenerativeProvider.cs b/src/Weaviate.Client/Configure/GenerativeProvider.cs index f123210d..57e8b91a 100644 --- a/src/Weaviate.Client/Configure/GenerativeProvider.cs +++ b/src/Weaviate.Client/Configure/GenerativeProvider.cs @@ -316,6 +316,40 @@ public Providers.Databricks Databricks( TopP = topP, }; + /// + /// Deepseeks the base url + /// + /// The base url + /// The model + /// The temperature + /// The max tokens + /// The frequency penalty + /// The presence penalty + /// The top + /// The stop + /// The providers deepseek + public Providers.Deepseek Deepseek( + string? baseUrl = null, + string? model = null, + double? temperature = null, + long? maxTokens = null, + double? frequencyPenalty = null, + double? presencePenalty = null, + double? topP = null, + List? stop = null + ) => + new() + { + BaseUrl = baseUrl, + Model = model, + Temperature = temperature, + MaxTokens = maxTokens, + FrequencyPenalty = frequencyPenalty, + PresencePenalty = presencePenalty, + TopP = topP, + Stop = stop, + }; + /// /// Dummies this instance /// diff --git a/src/Weaviate.Client/Models/Generative/Providers.cs b/src/Weaviate.Client/Models/Generative/Providers.cs index 2c23cd1d..abab1bae 100644 --- a/src/Weaviate.Client/Models/Generative/Providers.cs +++ b/src/Weaviate.Client/Models/Generative/Providers.cs @@ -822,6 +822,52 @@ public record Databricks() : GenerativeProvider("databricks") public double? TopP { get; set; } } + /// + /// Configuration for DeepSeek generative AI provider. + /// + public record Deepseek() : GenerativeProvider("deepseek") + { + /// + /// Gets or sets the base URL for the DeepSeek API endpoint. + /// + public string? BaseUrl { get; set; } + + /// + /// Gets or sets the model identifier to use. + /// + public string? Model { get; set; } + + /// + /// Gets or sets the temperature for controlling randomness in generation. + /// + public double? Temperature { get; set; } + + /// + /// Gets or sets the maximum number of tokens to generate. + /// + public long? MaxTokens { get; set; } + + /// + /// Gets or sets the frequency penalty to reduce repetition. + /// + public double? FrequencyPenalty { get; set; } + + /// + /// Gets or sets the presence penalty to encourage topic diversity. + /// + public double? PresencePenalty { get; set; } + + /// + /// Gets or sets the top-p (nucleus) sampling parameter. + /// + public double? TopP { get; set; } + + /// + /// Gets or sets the sequences where generation should stop. + /// + public List? Stop { get; set; } + } + /// /// Configuration for FriendliAI generative AI provider. /// diff --git a/src/Weaviate.Client/Models/GenerativeConfig.cs b/src/Weaviate.Client/Models/GenerativeConfig.cs index 85d6858a..c8654564 100644 --- a/src/Weaviate.Client/Models/GenerativeConfig.cs +++ b/src/Weaviate.Client/Models/GenerativeConfig.cs @@ -378,6 +378,68 @@ internal Databricks() { } public double? TopP { get; set; } } + /// + /// Configuration for DeepSeek generative AI provider. + /// + public record Deepseek : IGenerativeConfig + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal Deepseek() { } + + /// + /// The type value for DeepSeek configuration. + /// + public const string TypeValue = "generative-deepseek"; + + /// + /// Gets the type identifier for the configuration. + /// + public string Type => TypeValue; + + /// + /// Gets or sets the model identifier to use. + /// + public string? Model { get; set; } + + /// + /// Gets or sets the temperature for controlling randomness in generation. + /// + public double? Temperature { get; set; } + + /// + /// Gets or sets the maximum number of tokens to generate. + /// + public int? MaxTokens { get; set; } + + /// + /// Gets or sets the frequency penalty to reduce repetition. + /// + public double? FrequencyPenalty { get; set; } + + /// + /// Gets or sets the presence penalty to encourage topic diversity. + /// + public double? PresencePenalty { get; set; } + + /// + /// Gets or sets the top-p (nucleus) sampling parameter. + /// + public double? TopP { get; set; } + + /// + /// Gets or sets the base URL for the DeepSeek API endpoint. + /// + public string? BaseURL { get; set; } + + /// + /// Gets or sets the sequences where generation should stop. + /// + public string[]? Stop { get; set; } + } + /// /// Configuration for FriendliAI generative AI provider. /// diff --git a/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs b/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs index 1bb515c0..b8653376 100644 --- a/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs +++ b/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs @@ -48,6 +48,11 @@ internal static class GenerativeConfigSerialization text, Rest.WeaviateRestClient.RestJsonSerializerOptions ), + GenerativeConfig.Deepseek.TypeValue => + JsonSerializer.Deserialize( + text, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ), GenerativeConfig.FriendliAI.TypeValue => JsonSerializer.Deserialize( text, diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index ffc35323..c04a2ae5 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -351,3 +351,63 @@ Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.get -> string Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.init -> void Weaviate.Client.Models.Backup.IncrementalBaseBackupId.get -> string? Weaviate.Client.Models.Backup.IncrementalBaseBackupId.init -> void +const Weaviate.Client.Models.GenerativeConfig.Deepseek.TypeValue = "generative-deepseek" -> string! +override sealed Weaviate.Client.Models.Generative.Providers.Deepseek.Equals(Weaviate.Client.Models.GenerativeProvider? other) -> bool +override Weaviate.Client.Models.Generative.Providers.Deepseek.$() -> Weaviate.Client.Models.Generative.Providers.Deepseek! +override Weaviate.Client.Models.Generative.Providers.Deepseek.EqualityContract.get -> System.Type! +override Weaviate.Client.Models.Generative.Providers.Deepseek.Equals(object? obj) -> bool +override Weaviate.Client.Models.Generative.Providers.Deepseek.GetHashCode() -> int +override Weaviate.Client.Models.Generative.Providers.Deepseek.PrintMembers(System.Text.StringBuilder! builder) -> bool +override Weaviate.Client.Models.Generative.Providers.Deepseek.ToString() -> string! +override Weaviate.Client.Models.GenerativeConfig.Deepseek.Equals(object? obj) -> bool +override Weaviate.Client.Models.GenerativeConfig.Deepseek.GetHashCode() -> int +override Weaviate.Client.Models.GenerativeConfig.Deepseek.ToString() -> string! +static Weaviate.Client.Models.Generative.Providers.Deepseek.operator !=(Weaviate.Client.Models.Generative.Providers.Deepseek? left, Weaviate.Client.Models.Generative.Providers.Deepseek? right) -> bool +static Weaviate.Client.Models.Generative.Providers.Deepseek.operator ==(Weaviate.Client.Models.Generative.Providers.Deepseek? left, Weaviate.Client.Models.Generative.Providers.Deepseek? right) -> bool +static Weaviate.Client.Models.GenerativeConfig.Deepseek.operator !=(Weaviate.Client.Models.GenerativeConfig.Deepseek? left, Weaviate.Client.Models.GenerativeConfig.Deepseek? right) -> bool +static Weaviate.Client.Models.GenerativeConfig.Deepseek.operator ==(Weaviate.Client.Models.GenerativeConfig.Deepseek? left, Weaviate.Client.Models.GenerativeConfig.Deepseek? right) -> bool +virtual Weaviate.Client.Models.Generative.Providers.Deepseek.Equals(Weaviate.Client.Models.Generative.Providers.Deepseek? other) -> bool +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.$() -> Weaviate.Client.Models.GenerativeConfig.Deepseek! +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.EqualityContract.get -> System.Type! +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.Equals(Weaviate.Client.Models.GenerativeConfig.Deepseek? other) -> bool +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.PrintMembers(System.Text.StringBuilder! builder) -> bool +Weaviate.Client.GenerativeConfigFactory.Deepseek(string? model = null, double? temperature = null, int? maxTokens = null, double? frequencyPenalty = null, double? presencePenalty = null, double? topP = null, string? baseURL = null, string![]? stop = null) -> Weaviate.Client.Models.IGenerativeConfig! +Weaviate.Client.GenerativeProviderFactory.Deepseek(string? baseUrl = null, string? model = null, double? temperature = null, long? maxTokens = null, double? frequencyPenalty = null, double? presencePenalty = null, double? topP = null, System.Collections.Generic.List? stop = null) -> Weaviate.Client.Models.Generative.Providers.Deepseek! +Weaviate.Client.Models.Generative.Providers.Deepseek +Weaviate.Client.Models.Generative.Providers.Deepseek.BaseUrl.get -> string? +Weaviate.Client.Models.Generative.Providers.Deepseek.BaseUrl.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Deepseek() -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Deepseek(Weaviate.Client.Models.Generative.Providers.Deepseek! original) -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.FrequencyPenalty.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.FrequencyPenalty.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.MaxTokens.get -> long? +Weaviate.Client.Models.Generative.Providers.Deepseek.MaxTokens.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Model.get -> string? +Weaviate.Client.Models.Generative.Providers.Deepseek.Model.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.PresencePenalty.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.PresencePenalty.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Stop.get -> System.Collections.Generic.List? +Weaviate.Client.Models.Generative.Providers.Deepseek.Stop.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Temperature.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.Temperature.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.TopP.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.TopP.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek +Weaviate.Client.Models.GenerativeConfig.Deepseek.BaseURL.get -> string? +Weaviate.Client.Models.GenerativeConfig.Deepseek.BaseURL.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Deepseek(Weaviate.Client.Models.GenerativeConfig.Deepseek! original) -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.FrequencyPenalty.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.FrequencyPenalty.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.MaxTokens.get -> int? +Weaviate.Client.Models.GenerativeConfig.Deepseek.MaxTokens.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Model.get -> string? +Weaviate.Client.Models.GenerativeConfig.Deepseek.Model.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.PresencePenalty.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.PresencePenalty.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Stop.get -> string![]? +Weaviate.Client.Models.GenerativeConfig.Deepseek.Stop.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Temperature.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.Temperature.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.TopP.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.TopP.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Type.get -> string! diff --git a/src/Weaviate.Client/gRPC/Search.Builders.cs b/src/Weaviate.Client/gRPC/Search.Builders.cs index 77acb48c..18d293ee 100644 --- a/src/Weaviate.Client/gRPC/Search.Builders.cs +++ b/src/Weaviate.Client/gRPC/Search.Builders.cs @@ -437,6 +437,19 @@ private static V1.GenerativeProvider GetGenerativeProvider(GenerativeProvider pr SetIfNotNull(v => result.Databricks.Temperature = (float)v, a.Temperature); SetIfNotNull(v => result.Databricks.TopP = (float)v, a.TopP); break; + case Models.Generative.Providers.Deepseek a: + result.Deepseek = new V1.GenerativeDeepseek + { + BaseUrl = a.BaseUrl ?? string.Empty, + Model = a.Model ?? string.Empty, + Stop = a.Stop != null ? new V1.TextArray { Values = { a.Stop } } : null, + }; + SetIfNotNull(v => result.Deepseek.Temperature = v, a.Temperature); + SetIfNotNull(v => result.Deepseek.MaxTokens = v, a.MaxTokens); + SetIfNotNull(v => result.Deepseek.FrequencyPenalty = v, a.FrequencyPenalty); + SetIfNotNull(v => result.Deepseek.PresencePenalty = v, a.PresencePenalty); + SetIfNotNull(v => result.Deepseek.TopP = v, a.TopP); + break; case Models.Generative.Providers.FriendliAI a: result.Friendliai = new V1.GenerativeFriendliAI { diff --git a/src/Weaviate.Client/gRPC/proto/v1/generative.proto b/src/Weaviate.Client/gRPC/proto/v1/generative.proto index 4bb98095..18da4598 100644 --- a/src/Weaviate.Client/gRPC/proto/v1/generative.proto +++ b/src/Weaviate.Client/gRPC/proto/v1/generative.proto @@ -49,6 +49,7 @@ message GenerativeProvider { GenerativeNvidia nvidia = 13; GenerativeXAI xai = 14; GenerativeContextualAI contextualai = 15; + GenerativeDeepseek deepseek = 16; } } @@ -219,6 +220,17 @@ message GenerativeContextualAI{ optional TextArray knowledge = 7; } +message GenerativeDeepseek{ + optional string base_url = 1; + optional string model = 2; + optional double temperature = 3; + optional int64 max_tokens = 4; + optional double frequency_penalty = 5; + optional double presence_penalty = 6; + optional double top_p = 7; + optional TextArray stop = 8; +} + message GenerativeAnthropicMetadata { message Usage { int64 input_tokens = 1; @@ -336,6 +348,15 @@ message GenerativeXAIMetadata { optional Usage usage = 1; } +message GenerativeDeepseekMetadata { + message Usage { + optional int64 prompt_tokens = 1; + optional int64 completion_tokens = 2; + optional int64 total_tokens = 3; + } + optional Usage usage = 1; +} + message GenerativeMetadata { oneof kind { GenerativeAnthropicMetadata anthropic = 1; @@ -351,6 +372,7 @@ message GenerativeMetadata { GenerativeFriendliAIMetadata friendliai = 11; GenerativeNvidiaMetadata nvidia = 12; GenerativeXAIMetadata xai = 13; + GenerativeDeepseekMetadata deepseek = 14; } } From 43567317781667a14fe19dc5cf02a8536b171d8b Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:39:46 +0200 Subject: [PATCH 05/14] fix: aggregate reported 0/0.0/false where the server sent nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every scalar in the aggregate reply is `optional` in aggregate.proto. FromGrpcProperty read the Int, Number and Boolean ones unconditionally, so an unset field surfaced as the field type's zero — indistinguishable from a real aggregate of zeros. The Date branch six lines below already gated on its Has* flags; Int, Number and Boolean were simply missed. Python fixed the same defect in 4.21.2 (base_executor.py, python #2036). Proven against a live 1.39.0 before the change: an OverAll with a filter matching no objects and Metric.Integer(maximum, mean, sum) returned Maximum == 0, and the new integration test failed with "Assert.Null() Failure: Value of type 'Nullable' has a value / Expected: null / Actual: 0". Both aggregate shapes route through this one method, so AggregateResult and AggregateGroupByResult are fixed together. Boolean's four members were non-nullable and had to change type, which is source-breaking; PublicAPI carries the *REMOVED*/re-add pairs. That half cannot be shown against a live server, because 1.39.0 always populates all four however few objects match and whatever metrics were requested — on an empty match it sends totals of 0 and percentages of NaN. It is still wrong to invent false/0 for a field the server did not send, so the guarantee is pinned by a unit test that drives FromGrpcProperty off a proto message directly, with a companion case proving a deliberate zero still survives as zero rather than being swallowed. Count stays unconditional, as in python: 0 is the right answer for an empty aggregate, not a missing one. The integration tests record two observed server behaviours worth knowing: unrequested Int/Number sub-metrics really are absent (so the fix is what makes them null), while the boolean members are always present. --- .../Integration/TestCollectionAggregate.cs | 143 +++++++++++++++++ .../Unit/TestAggregateResultAccessors.cs | 144 ++++++++++++++++++ src/Weaviate.Client/Models/Aggregate.cs | 59 ++++--- src/Weaviate.Client/PublicAPI.Unshipped.txt | 8 + 4 files changed, 330 insertions(+), 24 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs b/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs index d7c7c170..f12fdfe2 100644 --- a/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs +++ b/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs @@ -903,4 +903,147 @@ await collectionClient.Data.Insert( break; } } + + /// + /// When a filter matches nothing there is no maximum, no mean and no sum, and the server + /// says so by leaving those optional proto fields unset. The client must report null rather + /// than the zero value of the field's type: a caller cannot tell a returned 0 apart from a + /// genuine aggregate of zeros. Date already handled this correctly, so it is asserted + /// alongside as the reference behaviour. + /// + [Fact] + public async Task Test_OverAll_With_NoMatches_Returns_Null_Not_Zero() + { + var collectionClient = await CollectionFactory( + properties: new[] + { + Property.Text("text"), + Property.Int("int"), + Property.Number("float"), + Property.Bool("bool"), + Property.Date("date"), + } + ); + + await collectionClient.Data.Insert( + new + { + text = "one", + @int = 1, + @float = 1.0, + @bool = true, + date = new DateTime(2021, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }, + cancellationToken: TestContext.Current.CancellationToken + ); + + var result = await collectionClient.Aggregate.OverAll( + filters: Filter.Property("text").IsEqual("no-such-value"), + returnMetrics: + [ + Metrics.ForProperty("int").Integer(maximum: true, mean: true, sum: true), + Metrics.ForProperty("float").Number(maximum: true, mean: true, sum: true), + Metrics + .ForProperty("bool") + .Boolean( + percentageFalse: true, + percentageTrue: true, + totalFalse: true, + totalTrue: true + ), + Metrics.ForProperty("date").Date(maximum: true, minimum: true), + ], + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.Equal(0, result.TotalCount); + + var integer = Assert.IsType(result.Properties["int"]); + Assert.Null(integer.Maximum); + Assert.Null(integer.Mean); + Assert.Null(integer.Sum); + + var number = Assert.IsType(result.Properties["float"]); + Assert.Null(number.Maximum); + Assert.Null(number.Mean); + Assert.Null(number.Sum); + + // Booleans behave differently and are asserted for what the server actually does: it + // sends all four, the counts as 0 and the percentages as NaN (0/0). They are present, + // so a presence check correctly surfaces them rather than inventing null. + var boolean = Assert.IsType(result.Properties["bool"]); + Assert.Equal(0, boolean.TotalFalse); + Assert.Equal(0, boolean.TotalTrue); + Assert.NotNull(boolean.PercentageFalse); + Assert.True(double.IsNaN(boolean.PercentageFalse.Value)); + Assert.NotNull(boolean.PercentageTrue); + Assert.True(double.IsNaN(boolean.PercentageTrue.Value)); + + // Reference: the Date branch already used presence checks before this fix. + var date = Assert.IsType(result.Properties["date"]); + Assert.Null(date.Maximum); + Assert.Null(date.Minimum); + } + + /// + /// A numeric sub-metric the caller did not ask for is not computed and not sent, so it must + /// read back as null. Objects do match here, so the nulls come from the metric selection + /// alone rather than from an empty result set — the complement of the test above. + /// Booleans are asserted for what Weaviate 1.39.0 actually does: it fills in all four + /// members whatever was requested, so they are present and carry real values. + /// + [Fact] + public async Task Test_OverAll_Unrequested_Metrics_Are_Null() + { + var collectionClient = await CollectionFactory( + properties: new[] + { + Property.Int("int"), + Property.Number("float"), + Property.Bool("bool"), + } + ); + + await collectionClient.Data.Insert( + new + { + @int = 7, + @float = 7.5, + @bool = true, + }, + cancellationToken: TestContext.Current.CancellationToken + ); + + var result = await collectionClient.Aggregate.OverAll( + returnMetrics: + [ + Metrics.ForProperty("int").Integer(maximum: true), + Metrics.ForProperty("float").Number(mean: true), + Metrics.ForProperty("bool").Boolean(totalTrue: true), + ], + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.Equal(1, result.TotalCount); + + var integer = Assert.IsType(result.Properties["int"]); + Assert.Equal(7, integer.Maximum); + Assert.Null(integer.Mean); + Assert.Null(integer.Median); + Assert.Null(integer.Minimum); + Assert.Null(integer.Mode); + Assert.Null(integer.Sum); + + var number = Assert.IsType(result.Properties["float"]); + Assert.Equal(7.5, number.Mean); + Assert.Null(number.Maximum); + Assert.Null(number.Minimum); + Assert.Null(number.Sum); + + var boolean = Assert.IsType(result.Properties["bool"]); + Assert.Equal(1, boolean.TotalTrue); + Assert.Equal(0, boolean.TotalFalse); + Assert.Equal(1, boolean.PercentageTrue); + Assert.Equal(0, boolean.PercentageFalse); + } } diff --git a/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs b/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs index 844fe272..4f5241ab 100644 --- a/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs +++ b/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs @@ -779,4 +779,148 @@ public void Group_Match_Func_ReturnsCorrectValue() } #endregion + + #region Grpc Presence Mapping + + /// + /// Every scalar in the aggregate reply is optional in the proto. When the server + /// leaves one unset the client must report null, not the field type's zero — a returned 0, + /// 0.0 or false is indistinguishable from a real aggregate. Driven straight off the proto + /// message so an unset field is guaranteed, which a live server will not always produce: + /// Weaviate 1.39.0 always fills the four boolean members in, so only this test pins them. + /// + [Fact] + public void FromGrpcProperty_UnsetScalars_MapToNull() + { + var integer = AggregateResult.FromGrpcProperty( + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + { + Property = "intField", + Int = + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Integer(), + } + ); + var typedInteger = Assert.IsType(integer); + Assert.Null(typedInteger.Maximum); + Assert.Null(typedInteger.Mean); + Assert.Null(typedInteger.Median); + Assert.Null(typedInteger.Minimum); + Assert.Null(typedInteger.Mode); + Assert.Null(typedInteger.Sum); + // Count is deliberately still read unconditionally, as python does: a count of 0 is a + // meaningful answer for an empty aggregate, not an absent one. + Assert.Equal(0, typedInteger.Count); + + var number = AggregateResult.FromGrpcProperty( + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + { + Property = "floatField", + Number = + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Number(), + } + ); + var typedNumber = Assert.IsType(number); + Assert.Null(typedNumber.Maximum); + Assert.Null(typedNumber.Mean); + Assert.Null(typedNumber.Median); + Assert.Null(typedNumber.Minimum); + Assert.Null(typedNumber.Mode); + Assert.Null(typedNumber.Sum); + + var boolean = AggregateResult.FromGrpcProperty( + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + { + Property = "boolField", + Boolean = + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Boolean(), + } + ); + var typedBoolean = Assert.IsType(boolean); + Assert.Null(typedBoolean.PercentageFalse); + Assert.Null(typedBoolean.PercentageTrue); + Assert.Null(typedBoolean.TotalFalse); + Assert.Null(typedBoolean.TotalTrue); + } + + /// + /// The complement of the case above: a scalar the server did set is carried through, so the + /// presence checks do not swallow real values. A deliberate zero must survive as zero. + /// + [Fact] + public void FromGrpcProperty_SetScalars_MapToValues() + { + var integer = AggregateResult.FromGrpcProperty( + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + { + Property = "intField", + Int = + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Integer + { + Count = 3, + Maximum = 0, + Mean = 0, + Median = 2, + Minimum = -5, + Mode = 1, + Sum = 0, + }, + } + ); + var typedInteger = Assert.IsType(integer); + Assert.Equal(3, typedInteger.Count); + Assert.Equal(0, typedInteger.Maximum); + Assert.Equal(0, typedInteger.Mean); + Assert.Equal(2, typedInteger.Median); + Assert.Equal(-5, typedInteger.Minimum); + Assert.Equal(1, typedInteger.Mode); + Assert.Equal(0, typedInteger.Sum); + + var number = AggregateResult.FromGrpcProperty( + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + { + Property = "floatField", + Number = + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Number + { + Count = 2, + Maximum = 0, + Mean = 0, + Median = 1.5, + Minimum = -1.5, + Mode = 0, + Sum = 0, + }, + } + ); + var typedNumber = Assert.IsType(number); + Assert.Equal(0, typedNumber.Maximum); + Assert.Equal(0, typedNumber.Mean); + Assert.Equal(1.5, typedNumber.Median); + Assert.Equal(-1.5, typedNumber.Minimum); + Assert.Equal(0, typedNumber.Mode); + Assert.Equal(0, typedNumber.Sum); + + var boolean = AggregateResult.FromGrpcProperty( + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + { + Property = "boolField", + Boolean = + new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Boolean + { + Count = 4, + PercentageFalse = 0, + PercentageTrue = 1, + TotalFalse = 0, + TotalTrue = 4, + }, + } + ); + var typedBoolean = Assert.IsType(boolean); + Assert.Equal(0, typedBoolean.PercentageFalse); + Assert.Equal(1, typedBoolean.PercentageTrue); + Assert.Equal(0, typedBoolean.TotalFalse); + Assert.Equal(4, typedBoolean.TotalTrue); + } + + #endregion } diff --git a/src/Weaviate.Client/Models/Aggregate.cs b/src/Weaviate.Client/Models/Aggregate.cs index 23fe6bd1..9c2a30a3 100644 --- a/src/Weaviate.Client/Models/Aggregate.cs +++ b/src/Weaviate.Client/Models/Aggregate.cs @@ -733,36 +733,43 @@ x.Text.TopOccurences is null ) ).ToList(), }, + // Every scalar below is `optional` in aggregate.proto, and the server leaves them + // unset when there is nothing to aggregate — an empty result set. Reading them + // without a presence check yields the field type's zero, which a caller cannot tell + // apart from a real 0/0.0/false, so each one is gated on its Has* flag exactly as + // the Date branch further down already does. V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Int => new Aggregate.Integer { Count = x.Int.Count, - Maximum = x.Int.Maximum, - Mean = x.Int.Mean, - Median = x.Int.Median, - Minimum = x.Int.Minimum, - Mode = x.Int.Mode, - Sum = x.Int.Sum, + Maximum = x.Int.HasMaximum ? x.Int.Maximum : null, + Mean = x.Int.HasMean ? x.Int.Mean : null, + Median = x.Int.HasMedian ? x.Int.Median : null, + Minimum = x.Int.HasMinimum ? x.Int.Minimum : null, + Mode = x.Int.HasMode ? x.Int.Mode : null, + Sum = x.Int.HasSum ? x.Int.Sum : null, }, V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Number => new Aggregate.Number { Count = x.Number.Count, - Maximum = x.Number.Maximum, - Mean = x.Number.Mean, - Median = x.Number.Median, - Minimum = x.Number.Minimum, - Mode = x.Number.Mode, - Sum = x.Number.Sum, + Maximum = x.Number.HasMaximum ? x.Number.Maximum : null, + Mean = x.Number.HasMean ? x.Number.Mean : null, + Median = x.Number.HasMedian ? x.Number.Median : null, + Minimum = x.Number.HasMinimum ? x.Number.Minimum : null, + Mode = x.Number.HasMode ? x.Number.Mode : null, + Sum = x.Number.HasSum ? x.Number.Sum : null, }, V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Boolean => new Aggregate.Boolean { Count = x.Boolean.Count, - PercentageFalse = x.Boolean.PercentageFalse, - PercentageTrue = x.Boolean.PercentageTrue, - TotalFalse = x.Boolean.TotalFalse, - TotalTrue = x.Boolean.TotalTrue, + PercentageFalse = x.Boolean.HasPercentageFalse + ? x.Boolean.PercentageFalse + : null, + PercentageTrue = x.Boolean.HasPercentageTrue ? x.Boolean.PercentageTrue : null, + TotalFalse = x.Boolean.HasTotalFalse ? x.Boolean.TotalFalse : null, + TotalTrue = x.Boolean.HasTotalTrue ? x.Boolean.TotalTrue : null, }, V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Date => new Aggregate.Date @@ -1269,24 +1276,28 @@ public record Number : Numeric { }; public record Boolean : Property { /// - /// Gets or sets the value of the percentage false + /// Gets or sets the value of the percentage false, or null when the server returned no + /// value because nothing matched. /// - public double PercentageFalse { get; internal set; } + public double? PercentageFalse { get; internal set; } /// - /// Gets or sets the value of the percentage true + /// Gets or sets the value of the percentage true, or null when the server returned no + /// value because nothing matched. /// - public double PercentageTrue { get; internal set; } + public double? PercentageTrue { get; internal set; } /// - /// Gets or sets the value of the total false + /// Gets or sets the value of the total false, or null when the server returned no value + /// because nothing matched. /// - public long TotalFalse { get; internal set; } + public long? TotalFalse { get; internal set; } /// - /// Gets or sets the value of the total true + /// Gets or sets the value of the total true, or null when the server returned no value + /// because nothing matched. /// - public long TotalTrue { get; internal set; } + public long? TotalTrue { get; internal set; } }; /// diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index c04a2ae5..dc53845f 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -411,3 +411,11 @@ Weaviate.Client.Models.GenerativeConfig.Deepseek.Temperature.set -> void Weaviate.Client.Models.GenerativeConfig.Deepseek.TopP.get -> double? Weaviate.Client.Models.GenerativeConfig.Deepseek.TopP.set -> void Weaviate.Client.Models.GenerativeConfig.Deepseek.Type.get -> string! +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.PercentageFalse.get -> double +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.PercentageTrue.get -> double +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.TotalFalse.get -> long +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.TotalTrue.get -> long +Weaviate.Client.Models.Aggregate.Boolean.PercentageFalse.get -> double? +Weaviate.Client.Models.Aggregate.Boolean.PercentageTrue.get -> double? +Weaviate.Client.Models.Aggregate.Boolean.TotalFalse.get -> long? +Weaviate.Client.Models.Aggregate.Boolean.TotalTrue.get -> long? From ccab7a59e3de6d8f3fe572022b23e04e9d5114ba Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:35:46 +0200 Subject: [PATCH 06/14] fix: drop vectorizeCollectionName from the Multi2VecGoogleGemini factories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects 55b0693, which folded a vectorizeCollectionName parameter into both Gemini overloads alongside dimensions. dimensions was right; this was not. No multi2vec module reads vectorizeClassName server-side, so the parameter did nothing. Python's multi2vec_google_gemini does not expose it, and neither does any other multi2vec_* factory — f8a6d4f1 documents it as having no effect across all of them. #367 removed it from Multi2VecTwelveLabs two PRs ago for the same reason, so keeping it here would have contradicted a decision already made on this branch's own predecessor. Confirmed against the live 1.39.0 server: creating the collection without the key stores no vectorizeClassName and the server defaults nothing back in, so the integration test now asserts null there instead of false. The property itself stays on the Multi2VecGoogle record: it is shipped API and inherited by the Gemini path, so the factory pins it to null explicitly (which the WEAVIATE002 analyzer requires) rather than dropping it. Only the two Unshipped signature lines are rewritten, with no *REMOVED* markers: the parameter never shipped, it was added earlier on this same branch. The *REMOVED* entries above them still describe the genuinely removed pre-PR signatures. The Vertex Multi2VecGoogle overloads are deliberately untouched — they carried vectorizeCollectionName in PublicAPI.Shipped.txt before this branch, so removing it there is a breaking change and a separate decision. --- .../Integration/TestVectorizers.cs | 7 ++++--- .../Unit/TestVectorizers.cs | 12 ++++++++---- .../Configure/VectorizerFactory.cs | 16 ++++++++-------- src/Weaviate.Client/Models/Vectorizer.cs | 2 +- src/Weaviate.Client/PublicAPI.Unshipped.txt | 4 ++-- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs index 00083d07..5066ebc6 100644 --- a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs @@ -32,8 +32,7 @@ public async Task Test_Multi2VecGoogleGemini_Creates_Collection() v.Multi2VecGoogleGemini( imageFields: ["image"], textFields: ["text"], - dimensions: 512, - vectorizeCollectionName: false + dimensions: 512 ) ) ); @@ -49,7 +48,9 @@ public async Task Test_Multi2VecGoogleGemini_Creates_Collection() Assert.Equal("multi2vec-palm", google.Identifier); Assert.Equal("generativelanguage.googleapis.com", google.ApiEndpoint); Assert.Equal(512, google.Dimensions); - Assert.False(google.VectorizeCollectionName); + // The factory does not offer vectorizeClassName because no multi2vec module reads it, + // and the server confirms it: nothing is sent, and nothing is stored or defaulted back. + Assert.Null(google.VectorizeCollectionName); Assert.NotNull(google.ImageFields); Assert.Equal(["image"], google.ImageFields); Assert.NotNull(google.TextFields); diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs index e39dee09..d71f3302 100644 --- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs @@ -369,8 +369,7 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() textFields: new[] { "text" }, videoFields: new[] { "video" }, audioFields: new[] { "audio" }, - dimensions: 512, - vectorizeCollectionName: false + dimensions: 512 ) ); @@ -399,7 +398,11 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() Assert.Contains("\"videoFields\":[\"video\"]", json); Assert.Contains("\"audioFields\":[\"audio\"]", json); Assert.Contains("\"dimensions\":512", json); - Assert.Contains("\"vectorizeClassName\":false", json); + // vectorizeClassName does nothing in a multi2vec module, so the factory does not offer + // it. The property is inherited shipped API on Multi2VecGoogle, so unlike + // Multi2VecTwelveLabs it still serializes here — but only as null under this bare + // options object, and the REST client's options drop it before the wire. + Assert.Contains("\"vectorizeClassName\":null", json); Assert.DoesNotContain("\"weights\"", json); } @@ -458,7 +461,8 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields() + "\"textFields\":[0.23],\"videoFields\":[0.33,0.34]}", json ); - // Left unset by this case, so the omit-when-null path stays covered. + // dimensions is left unset by this case, so the omit-when-null path stays covered; + // vectorizeClassName is never settable through this factory at all. Assert.Contains("\"dimensions\":null", json); Assert.Contains("\"vectorizeClassName\":null", json); Assert.DoesNotContain("depthFields", json); diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs index 2075a51c..f44c0d72 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactory.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs @@ -417,7 +417,6 @@ public VectorizerConfig Multi2VecGoogle( /// The video interval seconds /// The model /// The dimensions - /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecGoogleGemini( WeightedFields imageFields, @@ -427,8 +426,7 @@ public VectorizerConfig Multi2VecGoogleGemini( string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, - int? dimensions = null, - bool? vectorizeCollectionName = null + int? dimensions = null ) => new Multi2VecGoogle { @@ -444,7 +442,9 @@ public VectorizerConfig Multi2VecGoogleGemini( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - VectorizeCollectionName = vectorizeCollectionName, + // No multi2vec module reads vectorizeClassName, so it is not exposed here; left null + // so the key stays off the wire. + VectorizeCollectionName = null, // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -470,7 +470,6 @@ public VectorizerConfig Multi2VecGoogleGemini( /// The video interval seconds /// The model /// The dimensions - /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecGoogleGemini( string[]? imageFields = null, @@ -480,8 +479,7 @@ public VectorizerConfig Multi2VecGoogleGemini( string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, - int? dimensions = null, - bool? vectorizeCollectionName = null + int? dimensions = null ) => new Multi2VecGoogle { @@ -497,7 +495,9 @@ public VectorizerConfig Multi2VecGoogleGemini( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - VectorizeCollectionName = vectorizeCollectionName, + // No multi2vec module reads vectorizeClassName, so it is not exposed here; left null + // so the key stays off the wire. + VectorizeCollectionName = null, }; /// diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs index a6a8c771..8bfc5d1e 100644 --- a/src/Weaviate.Client/Models/Vectorizer.cs +++ b/src/Weaviate.Client/Models/Vectorizer.cs @@ -468,7 +468,7 @@ internal Multi2VecPalm() { } /// has ever provided, so a collection configured with it could not be created. The Gemini API /// is reached through the multi2vec-google module by setting /// to generativelanguage.googleapis.com, - /// which is what + /// which is what /// now returns. Kept only so existing source that names the type still compiles. /// [Obsolete( diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index dc53845f..8d069a7f 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -341,8 +341,8 @@ Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.Location.get -> string? Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ProjectId.get -> string? Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null, string? apiEndpoint = null) -> Weaviate.Client.Models.VectorizerConfig! Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null, string? apiEndpoint = null) -> Weaviate.Client.Models.VectorizerConfig! -Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! -Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null) -> Weaviate.Client.Models.VectorizerConfig! *REMOVED*Weaviate.Client.Models.BackupCreateRequest.BackupCreateRequest(string! Id, Weaviate.Client.Models.BackupBackend! Backend, Weaviate.Client.Internal.AutoArray? IncludeCollections = null, Weaviate.Client.Internal.AutoArray? ExcludeCollections = null, int? CPUPercentage = null, Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel = null) -> void *REMOVED*Weaviate.Client.Models.BackupCreateRequest.Deconstruct(out string! Id, out Weaviate.Client.Models.BackupBackend! Backend, out Weaviate.Client.Internal.AutoArray? IncludeCollections, out Weaviate.Client.Internal.AutoArray? ExcludeCollections, out int? CPUPercentage, out Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel) -> void Weaviate.Client.Models.BackupCreateRequest.BackupCreateRequest(string! Id, Weaviate.Client.Models.BackupBackend! Backend, Weaviate.Client.Internal.AutoArray? IncludeCollections = null, Weaviate.Client.Internal.AutoArray? ExcludeCollections = null, int? CPUPercentage = null, Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel = null, string? IncrementalBaseBackupId = null) -> void From a1430ece85f4ec943ba7468f6660d08312a24239 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:57:42 +0200 Subject: [PATCH 07/14] docs: mark vectorizeCollectionName obsolete on the multivector vectorizers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A maintainer confirmed on #368 that vectorizeCollectionName is not available for multivector modules, and that python only keeps it to avoid a breaking change. The server bears that out: no multi2vec-* or multi2multivec-* module has a VectorizeClassName() accessor, own or via an embedded BaseClassSettings. They register "vectorizeClassName" as a class-config default in config.go and never read it back — only usecases/modulecomponents/vectorizer/object_texts.go consumes it, behind an icheck interface no multivector settings type implements. So the eight multivector records get [Obsolete] on the property, mirroring python's "Deprecated, has no effect", with the XML docs saying the same. The property stays: removing it would break callers. text2vec-* is untouched — the setting is real there, and the digitalocean test asserts vectorizeClassName:false deliberately. text2multivec-jinaai is deliberately left alone despite the name. Its class settings embed basesettings.BaseClassSettings, which supplies the VectorizeClassName() accessor that base_class_settings.go actually calls, so the setting is live for it. It is a text vectorizer that happens to emit multiple vectors, not a multivector-input module. The factory parameters could not be marked: [Obsolete] is not valid on a parameter (CS0592 — it is only valid on class, struct, enum, constructor, method, property, indexer, field, event, interface and delegate), verified with the compiler rather than assumed. Their docs carry python's wording instead, which is what python does too — f8a6d4f1 is documentation only. Consumers still get a real CS0618 when they read or assign the property, including on a config read back from the server. The eighteen factory assignments the client itself must keep — WEAVIATE002 requires every public property to be initialised in a vectorizer factory — are suppressed one line at a time with scoped pragmas and a reason, not at file or project level. The one test that asserts on the property is suppressed the same way rather than deleted, since the assertion is the point. --- .../Integration/TestVectorizers.cs | 3 + .../Configure/VectorizerFactory.cs | 74 +++++++++++++++---- .../Configure/VectorizerFactoryMulti.cs | 10 ++- src/Weaviate.Client/Models/Vectorizer.cs | 40 ++++++++-- 4 files changed, 103 insertions(+), 24 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs index 5066ebc6..78ae3d12 100644 --- a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs @@ -50,7 +50,10 @@ public async Task Test_Multi2VecGoogleGemini_Creates_Collection() Assert.Equal(512, google.Dimensions); // The factory does not offer vectorizeClassName because no multi2vec module reads it, // and the server confirms it: nothing is sent, and nothing is stored or defaulted back. + // Reading the obsolete property is the point of the assertion, hence the suppression. +#pragma warning disable CS0618 Assert.Null(google.VectorizeCollectionName); +#pragma warning restore CS0618 Assert.NotNull(google.ImageFields); Assert.Equal(["image"], google.ImageFields); Assert.NotNull(google.TextFields); diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs index f44c0d72..e78677d5 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactory.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs @@ -61,7 +61,7 @@ public VectorizerConfig Text2VecWeaviate( /// AWS region. /// Model name to use. /// Number of vector dimensions. - /// Whether to vectorize the collection name. + /// Deprecated, has no effect. /// Multi2VecAWSBedrock vectorizer configuration. public VectorizerConfig Multi2VecAWSBedrock( WeightedFields imageFields, @@ -78,7 +78,10 @@ public VectorizerConfig Multi2VecAWSBedrock( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -93,7 +96,7 @@ public VectorizerConfig Multi2VecAWSBedrock( /// AWS region. /// Model name to use. /// Number of vector dimensions. - /// Whether to vectorize the collection name. + /// Deprecated, has no effect. /// Multi2VecAWSBedrock vectorizer configuration. public VectorizerConfig Multi2VecAWSBedrock( string[]? imageFields = null, @@ -110,7 +113,10 @@ public VectorizerConfig Multi2VecAWSBedrock( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -119,7 +125,7 @@ public VectorizerConfig Multi2VecAWSBedrock( /// Weighted image fields. /// Weighted text fields. /// Inference URL for the model. - /// Whether to vectorize the collection name. + /// Deprecated, has no effect. /// Multi2VecClip vectorizer configuration. public VectorizerConfig Multi2VecClip( WeightedFields imageFields, @@ -132,7 +138,10 @@ public VectorizerConfig Multi2VecClip( ImageFields = ModalityFields.OrNull(imageFields), InferenceUrl = inferenceUrl, TextFields = ModalityFields.OrNull(textFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -145,7 +154,7 @@ public VectorizerConfig Multi2VecClip( /// Array of image field names. /// Array of text field names. /// Inference URL for the model. - /// Whether to vectorize the collection name. + /// Deprecated, has no effect. /// Multi2VecClip vectorizer configuration. public VectorizerConfig Multi2VecClip( string[]? imageFields = null, @@ -158,7 +167,10 @@ public VectorizerConfig Multi2VecClip( ImageFields = ModalityFields.OrNull(imageFields), InferenceUrl = inferenceUrl, TextFields = ModalityFields.OrNull(textFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -170,7 +182,7 @@ public VectorizerConfig Multi2VecClip( /// Model name to use. /// Number of vector dimensions. /// Truncation strategy. - /// Whether to vectorize the collection name. + /// Deprecated, has no effect. /// Multi2VecCohere vectorizer configuration. public VectorizerConfig Multi2VecCohere( WeightedFields imageFields, @@ -189,7 +201,10 @@ public VectorizerConfig Multi2VecCohere( Dimensions = dimensions, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -205,7 +220,7 @@ public VectorizerConfig Multi2VecCohere( /// Model name to use. /// Number of vector dimensions. /// Truncation strategy. - /// Whether to vectorize the collection name. + /// Deprecated, has no effect. /// Multi2VecCohere vectorizer configuration. public VectorizerConfig Multi2VecCohere( string[]? imageFields = null, @@ -224,7 +239,10 @@ public VectorizerConfig Multi2VecCohere( Dimensions = dimensions, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -237,7 +255,7 @@ public VectorizerConfig Multi2VecCohere( /// Weighted IMU fields. /// Weighted thermal fields. /// Weighted video fields. - /// Whether to vectorize the collection name. + /// Deprecated, has no effect. /// Multi2VecBind vectorizer configuration. public VectorizerConfig Multi2VecBind( WeightedFields imageFields, @@ -258,7 +276,10 @@ public VectorizerConfig Multi2VecBind( TextFields = ModalityFields.OrNull(textFields), ThermalFields = ModalityFields.OrNull(thermalFields), VideoFields = ModalityFields.OrNull(videoFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 // This overload's parameters happen to be in FromWeightedFields' own declaration // order (image, text, audio, depth, imu, thermal, video), so the named arguments // are a safeguard rather than a correction: they keep the mapping right if either @@ -284,7 +305,7 @@ public VectorizerConfig Multi2VecBind( /// The imu fields /// The thermal fields /// The video fields - /// The vectorize collection name + /// Deprecated, has no effect. /// The vectorizer config public VectorizerConfig Multi2VecBind( string[]? imageFields = null, @@ -305,7 +326,10 @@ public VectorizerConfig Multi2VecBind( TextFields = ModalityFields.OrNull(textFields), ThermalFields = ModalityFields.OrNull(thermalFields), VideoFields = ModalityFields.OrNull(videoFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -320,7 +344,7 @@ public VectorizerConfig Multi2VecBind( /// The video interval seconds /// The model /// The dimensions - /// The vectorize collection name + /// Deprecated, has no effect. /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( @@ -348,7 +372,10 @@ public VectorizerConfig Multi2VecGoogle( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -373,7 +400,7 @@ public VectorizerConfig Multi2VecGoogle( /// The video interval seconds /// The model /// The dimensions - /// The vectorize collection name + /// Deprecated, has no effect. /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( @@ -401,7 +428,10 @@ public VectorizerConfig Multi2VecGoogle( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -444,7 +474,9 @@ public VectorizerConfig Multi2VecGoogleGemini( Dimensions = dimensions, // No multi2vec module reads vectorizeClassName, so it is not exposed here; left null // so the key stays off the wire. +#pragma warning disable CS0618 VectorizeCollectionName = null, +#pragma warning restore CS0618 // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -497,7 +529,9 @@ public VectorizerConfig Multi2VecGoogleGemini( Dimensions = dimensions, // No multi2vec module reads vectorizeClassName, so it is not exposed here; left null // so the key stays off the wire. +#pragma warning disable CS0618 VectorizeCollectionName = null, +#pragma warning restore CS0618 }; /// @@ -510,7 +544,7 @@ public VectorizerConfig Multi2VecGoogleGemini( /// The dimensions /// The model /// The truncate - /// The vectorize collection name + /// Deprecated, has no effect. /// The vectorizer config public VectorizerConfig Multi2VecVoyageAI( WeightedFields imageFields, @@ -531,7 +565,10 @@ public VectorizerConfig Multi2VecVoyageAI( TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), Truncate = truncate, + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 // FromWeightedFields declares seven optional modalities in the order image, text, // audio, depth, imu, thermal, video. Video is the one at risk — positionally it // would land in audioFields, which this module does not have — and it was already @@ -553,7 +590,7 @@ public VectorizerConfig Multi2VecVoyageAI( /// The dimensions /// The model /// The truncate - /// The vectorize collection name + /// Deprecated, has no effect. /// The vectorizer config public VectorizerConfig Multi2VecVoyageAI( string[]? imageFields = null, @@ -574,7 +611,10 @@ public VectorizerConfig Multi2VecVoyageAI( Model = model, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -1115,7 +1155,7 @@ public VectorizerConfig Text2VecJinaAI( /// The model /// The base url /// The dimensions - /// The vectorize collection name + /// Deprecated, has no effect. /// The vectorizer config public VectorizerConfig Multi2VecJinaAI( string[]? imageFields = null, @@ -1132,7 +1172,10 @@ public VectorizerConfig Multi2VecJinaAI( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -1143,7 +1186,7 @@ public VectorizerConfig Multi2VecJinaAI( /// The model /// The base url /// The dimensions - /// The vectorize collection name + /// Deprecated, has no effect. /// The vectorizer config public VectorizerConfig Multi2VecJinaAI( WeightedFields imageFields, @@ -1164,7 +1207,10 @@ public VectorizerConfig Multi2VecJinaAI( imageFields: imageFields, textFields: textFields ), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; #pragma warning restore CA1822 // Mark members as static } diff --git a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs index f816d8d8..31491aed 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs @@ -52,7 +52,7 @@ public VectorizerConfig Text2MultiVecJinaAI( /// The text fields /// The base url /// The model - /// The vectorize collection name + /// Deprecated, has no effect. /// The vectorizer config public VectorizerConfig Multi2MultiVecJinaAI( string[]? imageFields = null, @@ -67,7 +67,10 @@ public VectorizerConfig Multi2MultiVecJinaAI( Model = model, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 }; /// @@ -77,7 +80,7 @@ public VectorizerConfig Multi2MultiVecJinaAI( /// The text fields /// The base url /// The model - /// The vectorize collection name + /// Deprecated, has no effect. /// The vectorizer config public VectorizerConfig Multi2MultiVecJinaAI( WeightedFields imageFields, @@ -92,7 +95,10 @@ public VectorizerConfig Multi2MultiVecJinaAI( Model = model, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), + // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. +#pragma warning disable CS0618 VectorizeCollectionName = vectorizeCollectionName, +#pragma warning restore CS0618 Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs index 8bfc5d1e..cf310fdf 100644 --- a/src/Weaviate.Client/Models/Vectorizer.cs +++ b/src/Weaviate.Client/Models/Vectorizer.cs @@ -187,9 +187,12 @@ internal Multi2VecAWS() { } public string[]? TextFields { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -230,9 +233,12 @@ internal Multi2VecClip() { } public string[]? TextFields { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -289,9 +295,12 @@ internal Multi2VecCohere() { } public string? Truncate { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -352,9 +361,12 @@ internal Multi2VecBind() { } public string[]? VideoFields { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -436,9 +448,12 @@ internal Multi2VecGoogle() { } public int? Dimensions { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -526,9 +541,12 @@ internal Multi2VecJinaAI() { } public string[]? TextFields { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -583,9 +601,12 @@ internal Multi2MultiVecJinaAI() { } internal VectorizerWeights? Weights { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; } @@ -669,9 +690,12 @@ internal Multi2VecVoyageAI() { } public bool? Truncate { get; set; } = null; /// - /// Gets or sets the value of the vectorize collection name + /// Deprecated, has no effect. + /// No multivector module reads this setting server-side; it is only registered as a + /// class-config default. Retained because removing it would be a breaking change. /// [JsonPropertyName("vectorizeClassName")] + [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// From e7df693b69d6e098c705aa985870b8248411ea7b Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:16:05 +0200 Subject: [PATCH 08/14] style: put the CS0618 suppression reason on the pragma, as the repo does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suppressions added in a1430ec carried a standalone comment line above each pragma, repeated verbatim sixteen times. The house form puts a terse reason as a trailing comment on the pragma itself, on both the disable and the restore — Models/Extensions.cs:116-118 is the exact analogue, an obsolete property assigned inside an object initializer. Also drops two comments that had become restatements once the property carried [Obsolete]: the Gemini factories now note only the part the attribute does not say — that they omit the parameter their Vertex siblings expose — and the unit test drops a clause that repeated the obsolete message while keeping why the key still serializes here when Multi2VecTwelveLabs drops it. The suppression reason in the integration test moves onto the pragma too, where it reads as the reason rather than as a third comment line. No behaviour change: 20 lines out, 3 in. --- .../Integration/TestVectorizers.cs | 5 +- .../Unit/TestVectorizers.cs | 7 +- .../Configure/VectorizerFactory.cs | 84 ++++++++----------- .../Configure/VectorizerFactoryMulti.cs | 10 +-- 4 files changed, 43 insertions(+), 63 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs index 78ae3d12..22bd301a 100644 --- a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs @@ -50,10 +50,9 @@ public async Task Test_Multi2VecGoogleGemini_Creates_Collection() Assert.Equal(512, google.Dimensions); // The factory does not offer vectorizeClassName because no multi2vec module reads it, // and the server confirms it: nothing is sent, and nothing is stored or defaulted back. - // Reading the obsolete property is the point of the assertion, hence the suppression. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Reading the obsolete property is the assertion Assert.Null(google.VectorizeCollectionName); -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Reading the obsolete property is the assertion Assert.NotNull(google.ImageFields); Assert.Equal(["image"], google.ImageFields); Assert.NotNull(google.TextFields); diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs index d71f3302..1254958e 100644 --- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs @@ -398,10 +398,9 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() Assert.Contains("\"videoFields\":[\"video\"]", json); Assert.Contains("\"audioFields\":[\"audio\"]", json); Assert.Contains("\"dimensions\":512", json); - // vectorizeClassName does nothing in a multi2vec module, so the factory does not offer - // it. The property is inherited shipped API on Multi2VecGoogle, so unlike - // Multi2VecTwelveLabs it still serializes here — but only as null under this bare - // options object, and the REST client's options drop it before the wire. + // The property is inherited shipped API on Multi2VecGoogle, so unlike Multi2VecTwelveLabs + // it still serializes here — but only as null under this bare options object, and the + // REST client's options drop it before the wire. Assert.Contains("\"vectorizeClassName\":null", json); Assert.DoesNotContain("\"weights\"", json); } diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs index e78677d5..914af693 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactory.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs @@ -78,10 +78,9 @@ public VectorizerConfig Multi2VecAWSBedrock( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -113,10 +112,9 @@ public VectorizerConfig Multi2VecAWSBedrock( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -138,10 +136,9 @@ public VectorizerConfig Multi2VecClip( ImageFields = ModalityFields.OrNull(imageFields), InferenceUrl = inferenceUrl, TextFields = ModalityFields.OrNull(textFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -167,10 +164,9 @@ public VectorizerConfig Multi2VecClip( ImageFields = ModalityFields.OrNull(imageFields), InferenceUrl = inferenceUrl, TextFields = ModalityFields.OrNull(textFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -201,10 +197,9 @@ public VectorizerConfig Multi2VecCohere( Dimensions = dimensions, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -239,10 +234,9 @@ public VectorizerConfig Multi2VecCohere( Dimensions = dimensions, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -276,10 +270,9 @@ public VectorizerConfig Multi2VecBind( TextFields = ModalityFields.OrNull(textFields), ThermalFields = ModalityFields.OrNull(thermalFields), VideoFields = ModalityFields.OrNull(videoFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // This overload's parameters happen to be in FromWeightedFields' own declaration // order (image, text, audio, depth, imu, thermal, video), so the named arguments // are a safeguard rather than a correction: they keep the mapping right if either @@ -326,10 +319,9 @@ public VectorizerConfig Multi2VecBind( TextFields = ModalityFields.OrNull(textFields), ThermalFields = ModalityFields.OrNull(thermalFields), VideoFields = ModalityFields.OrNull(videoFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -372,10 +364,9 @@ public VectorizerConfig Multi2VecGoogle( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -428,10 +419,9 @@ public VectorizerConfig Multi2VecGoogle( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -472,11 +462,10 @@ public VectorizerConfig Multi2VecGoogleGemini( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - // No multi2vec module reads vectorizeClassName, so it is not exposed here; left null - // so the key stays off the wire. -#pragma warning disable CS0618 + // Not offered as a parameter here, unlike Vertex; null keeps it off the wire. +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = null, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -527,11 +516,10 @@ public VectorizerConfig Multi2VecGoogleGemini( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - // No multi2vec module reads vectorizeClassName, so it is not exposed here; left null - // so the key stays off the wire. -#pragma warning disable CS0618 + // Not offered as a parameter here, unlike Vertex; null keeps it off the wire. +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = null, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -565,10 +553,9 @@ public VectorizerConfig Multi2VecVoyageAI( TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), Truncate = truncate, - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // FromWeightedFields declares seven optional modalities in the order image, text, // audio, depth, imu, thermal, video. Video is the one at risk — positionally it // would land in audioFields, which this module does not have — and it was already @@ -611,10 +598,9 @@ public VectorizerConfig Multi2VecVoyageAI( Model = model, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -1172,10 +1158,9 @@ public VectorizerConfig Multi2VecJinaAI( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -1207,10 +1192,9 @@ public VectorizerConfig Multi2VecJinaAI( imageFields: imageFields, textFields: textFields ), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; #pragma warning restore CA1822 // Mark members as static } diff --git a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs index 31491aed..2c678750 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs @@ -67,10 +67,9 @@ public VectorizerConfig Multi2MultiVecJinaAI( Model = model, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -95,10 +94,9 @@ public VectorizerConfig Multi2MultiVecJinaAI( Model = model, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), - // Inert for multivector modules; still shipped API, and WEAVIATE002 requires it here. -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 +#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields From ed398d0440db3d2bf1116cf52837d161f56ca8b0 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:32:13 +0200 Subject: [PATCH 09/14] fix: correct the incremental-backup version floors The gate comment claimed 1.37.0 was the first server to accept the field; the wire field has existed since 1.34.18. Keep the 1.37.0 gate as the documented feature floor, matching python, and say so. The read side has a different floor: servers only return the base id from 1.37.6, so on older ones a null does not mean the backup is non-incremental. Say that on the property, where deleting the base backup is the hazard. Collapse the duplicated create/list tests into theories. --- .../Unit/TestBackupClient.cs | 279 +++++------------- src/Weaviate.Client/BackupClient.cs | 3 +- src/Weaviate.Client/Models/Backup.cs | 4 +- 3 files changed, 78 insertions(+), 208 deletions(-) diff --git a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs index 8099822f..50c33901 100644 --- a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs +++ b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs @@ -9,6 +9,40 @@ namespace Weaviate.Client.Tests.Unit; /// public class TestBackupClient { + /// + /// A create response body, reused by the incremental-backup cases below. + /// + private const string CreateResponseJson = """ + { + "id": "my-backup", + "backend": "filesystem", + "status": "STARTED", + "path": "/backups" + } + """; + + /// + /// A client that answers POST /v1/backups with , pinned to a + /// server version so the incremental-backup version gate can be exercised. + /// + private static (WeaviateClient Client, MockHttpMessageHandler Handler) CreateBackupClient( + string serverVersion + ) => + MockWeaviateClient.CreateWithMockHandler( + syncHandler: req => + req.Method == HttpMethod.Post + ? new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + CreateResponseJson, + System.Text.Encoding.UTF8, + "application/json" + ), + } + : null!, + serverVersion: serverVersion + ); + /// /// CancelRestore() must issue a DELETE to /v1/backups/{backend}/{id}/restore. /// @@ -58,10 +92,10 @@ await client.Backup.CancelRestore( } /// - /// GetStatus should populate Size from the response. + /// GetStatus should populate Size and the incremental base id from the response. /// [Fact] - public async Task GetStatus_SizeIsPopulatedFromResponse() + public async Task GetStatus_PopulatesSizeAndIncrementalBaseBackupId() { var json = """ { @@ -69,7 +103,8 @@ public async Task GetStatus_SizeIsPopulatedFromResponse() "status": "SUCCESS", "path": "/backups", "backend": "filesystem", - "size": 1.5 + "size": 1.5, + "incremental_base_backup_id": "base-backup" } """; @@ -87,6 +122,7 @@ public async Task GetStatus_SizeIsPopulatedFromResponse() ); Assert.Equal(1.5, backup.Size); + Assert.Equal("base-backup", backup.IncrementalBaseBackupId); } /// @@ -217,46 +253,27 @@ BackupStatus expected Assert.Equal(expected, backup.Status); } - /// - /// A create response body, reused by the incremental-backup cases below. - /// - private const string CreateResponseJson = """ - { - "id": "my-backup", - "backend": "filesystem", - "status": "STARTED", - "path": "/backups" - } - """; - /// /// Create() must put IncrementalBaseBackupId on the wire under the spec's - /// incremental_base_backup_id key. Without it the client silently drops the - /// caller's request and takes a full backup instead of an incremental one. + /// incremental_base_backup_id key, or the client silently takes a full backup instead; + /// and omit the key when no base is asked for, which is why a plain backup still works on a + /// server below 1.37.0 — the gate is on the field, not the operation. /// - [Fact] - public async Task Create_SendsIncrementalBaseBackupId() + [Theory] + [InlineData("1.37.0", "base-backup")] + [InlineData("1.36.0", null)] + public async Task Create_SendsIncrementalBaseBackupId_OnlyWhenRequested( + string serverVersion, + string? incrementalBaseBackupId + ) { - var (client, handler) = MockWeaviateClient.CreateWithMockHandler( - syncHandler: req => - req.Method == HttpMethod.Post - ? new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent( - CreateResponseJson, - System.Text.Encoding.UTF8, - "application/json" - ), - } - : null!, - serverVersion: "1.37.0" - ); + var (client, handler) = CreateBackupClient(serverVersion); await client.Backup.Create( new BackupCreateRequest( "my-backup", new FilesystemBackend("/backups"), - IncrementalBaseBackupId: "base-backup" + IncrementalBaseBackupId: incrementalBaseBackupId ), TestContext.Current.CancellationToken ); @@ -265,64 +282,21 @@ await client.Backup.Create( var body = await handler.LastRequest!.Content!.ReadAsStringAsync( TestContext.Current.CancellationToken ); - Assert.Contains("\"incremental_base_backup_id\":\"base-backup\"", body); - } - /// - /// A create request that asks for no base backup must not send the key at all, so a plain - /// backup is unchanged. - /// - [Fact] - public async Task Create_OmitsIncrementalBaseBackupId_WhenNotRequested() - { - var (client, handler) = MockWeaviateClient.CreateWithMockHandler( - syncHandler: req => - req.Method == HttpMethod.Post - ? new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent( - CreateResponseJson, - System.Text.Encoding.UTF8, - "application/json" - ), - } - : null!, - serverVersion: "1.37.0" - ); - - await client.Backup.Create( - new BackupCreateRequest("my-backup", new FilesystemBackend("/backups")), - TestContext.Current.CancellationToken - ); - - Assert.NotNull(handler.LastRequest); - var body = await handler.LastRequest!.Content!.ReadAsStringAsync( - TestContext.Current.CancellationToken - ); - Assert.DoesNotContain("incremental_base_backup_id", body); + if (incrementalBaseBackupId is null) + Assert.DoesNotContain("incremental_base_backup_id", body); + else + Assert.Contains($"\"incremental_base_backup_id\":\"{incrementalBaseBackupId}\"", body); } /// - /// Incremental backups arrived in Weaviate 1.37.0, so asking for one against an older - /// server must fail in the client rather than silently producing a full backup. + /// The 1.37.0 gate is the documented feature floor, not the wire field's age: asking for an + /// incremental backup against an older server must fail rather than silently taking a full one. /// [Fact] public async Task Create_Throws_WhenIncrementalRequestedOnOlderServer() { - var (client, _) = MockWeaviateClient.CreateWithMockHandler( - syncHandler: req => - req.Method == HttpMethod.Post - ? new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent( - CreateResponseJson, - System.Text.Encoding.UTF8, - "application/json" - ), - } - : null!, - serverVersion: "1.36.0" - ); + var (client, _) = CreateBackupClient("1.36.0"); var exception = await Assert.ThrowsAsync(async () => await client.Backup.Create( @@ -340,58 +314,23 @@ await client.Backup.Create( } /// - /// The version gate is on the field, not the operation: a plain backup must still work on a - /// server older than 1.37.0. + /// List() parsed Size and the incremental base id and then dropped both; they must survive + /// onto the model, and be null rather than empty when the server omits them. /// - [Fact] - public async Task Create_PlainBackup_Succeeds_OnOlderServer() - { - var (client, handler) = MockWeaviateClient.CreateWithMockHandler( - syncHandler: req => - req.Method == HttpMethod.Post - ? new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent( - CreateResponseJson, - System.Text.Encoding.UTF8, - "application/json" - ), - } - : null!, - serverVersion: "1.36.0" - ); - - await client.Backup.Create( - new BackupCreateRequest("my-backup", new FilesystemBackend("/backups")), - TestContext.Current.CancellationToken - ); - - Assert.NotNull(handler.LastRequest); - } - - /// - /// List() parses the list payload and then discarded two fields it had already read: Size - /// was left null for every listed backup even though the property exists and the create - /// status path fills it, and the incremental base id had nowhere to go at all. Both must - /// survive onto the model. - /// - [Fact] - public async Task List_PopulatesSizeAndIncrementalBaseBackupId() + [Theory] + [InlineData( + """[{"id":"my-backup","classes":["Article"],"status":"SUCCESS","startedAt":"2026-08-14T10:00:00Z","completedAt":"2026-08-14T10:05:00Z","size":2.5,"incremental_base_backup_id":"base-backup"}]""", + "base-backup" + )] + [InlineData( + """[{"id":"my-backup","classes":["Article"],"status":"SUCCESS","size":2.5}]""", + null + )] + public async Task List_PopulatesSizeAndIncrementalBaseBackupId( + string json, + string? expectedIncrementalBaseBackupId + ) { - var json = """ - [ - { - "id": "my-backup", - "classes": ["Article"], - "status": "SUCCESS", - "startedAt": "2026-08-14T10:00:00Z", - "completedAt": "2026-08-14T10:05:00Z", - "size": 2.5, - "incremental_base_backup_id": "base-backup" - } - ] - """; - var (client, _) = MockWeaviateClient.CreateWithMockHandler( syncHandler: _ => new HttpResponseMessage(HttpStatusCode.OK) { @@ -407,76 +346,6 @@ public async Task List_PopulatesSizeAndIncrementalBaseBackupId() var backup = Assert.Single(backups); Assert.Equal("my-backup", backup.Id); Assert.Equal(2.5, backup.Size); - Assert.Equal("base-backup", backup.IncrementalBaseBackupId); - } - - /// - /// A full (non-incremental) backup has no base, and the server omits the key entirely, so - /// the model must report null rather than an empty string. - /// - [Fact] - public async Task List_IncrementalBaseBackupIdIsNull_ForFullBackup() - { - var json = """ - [ - { - "id": "my-backup", - "classes": ["Article"], - "status": "SUCCESS", - "size": 2.5 - } - ] - """; - - var (client, _) = MockWeaviateClient.CreateWithMockHandler( - syncHandler: _ => new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"), - } - ); - - var backups = await client.Backup.List( - BackupStorageProvider.Filesystem, - TestContext.Current.CancellationToken - ); - - var backup = Assert.Single(backups); - Assert.Null(backup.IncrementalBaseBackupId); - Assert.Equal(2.5, backup.Size); - } - - /// - /// The create-status response carries the same field, and the model is shared with the list - /// path, so leaving it unset there would reintroduce the same silent null. - /// - [Fact] - public async Task GetStatus_PopulatesIncrementalBaseBackupId() - { - var json = """ - { - "id": "my-backup", - "status": "SUCCESS", - "path": "/backups", - "backend": "filesystem", - "size": 1.5, - "incremental_base_backup_id": "base-backup" - } - """; - - var (client, _) = MockWeaviateClient.CreateWithMockHandler( - syncHandler: _ => new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"), - } - ); - - var backup = await client.Backup.GetStatus( - new FilesystemBackend("/backups"), - "my-backup", - TestContext.Current.CancellationToken - ); - - Assert.Equal("base-backup", backup.IncrementalBaseBackupId); - Assert.Equal(1.5, backup.Size); + Assert.Equal(expectedIncrementalBaseBackupId, backup.IncrementalBaseBackupId); } } diff --git a/src/Weaviate.Client/BackupClient.cs b/src/Weaviate.Client/BackupClient.cs index 99cce289..fb5ad779 100644 --- a/src/Weaviate.Client/BackupClient.cs +++ b/src/Weaviate.Client/BackupClient.cs @@ -88,7 +88,8 @@ public async Task CreateSync( } /// - /// The first Weaviate server version that accepts a file-based incremental backup base. + /// The version file-based incremental backups are documented against; the wire field itself + /// is older (1.34.18). Gate on the documented floor, as the python client does. /// private static readonly Version IncrementalBackupMinimumVersion = new(1, 37, 0); diff --git a/src/Weaviate.Client/Models/Backup.cs b/src/Weaviate.Client/Models/Backup.cs index f531168f..cb7abe05 100644 --- a/src/Weaviate.Client/Models/Backup.cs +++ b/src/Weaviate.Client/Models/Backup.cs @@ -297,8 +297,8 @@ public record Backup( public double? Size { get; init; } /// - /// Gets the id of the base backup this backup was built on, or null when the backup is not - /// incremental. + /// Gets the id of the base backup this backup was built on. Only servers 1.37.6 and later + /// return it; on older servers null does not mean the backup is non-incremental. /// public string? IncrementalBaseBackupId { get; init; } } From 40c6cdfd0fc85855aef3fe672d659ea931278c2c Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:32:27 +0200 Subject: [PATCH 10/14] fix: presence-check aggregate Count alongside the other scalars The gating commit added a comment saying every scalar is checked against its Has* flag, but Count was left ungated in all five branches, so an unrequested count still read 0 - the defect the commit set out to fix. Property.Count is already long?, so gating costs no API change. Correct the Boolean member docs: when nothing matches the server does send all four, so null means it did not send the value, not that nothing matched. Drop two ConvertToNumeric overloads left unreachable by the nullable callers, and update the accessor docs, which still showed the members non-nullable. --- docs/AGGREGATE_RESULT_ACCESSORS.md | 41 +++++-- .../Integration/TestCollectionAggregate.cs | 111 ++++-------------- .../Unit/TestAggregateResultAccessors.cs | 107 ++++++++--------- src/Weaviate.Client/Models/Aggregate.cs | 28 ++--- .../Models/Typed/TypedAggregateResults.cs | 32 ----- 5 files changed, 113 insertions(+), 206 deletions(-) diff --git a/docs/AGGREGATE_RESULT_ACCESSORS.md b/docs/AGGREGATE_RESULT_ACCESSORS.md index 9fec2b24..cfe55d8a 100644 --- a/docs/AGGREGATE_RESULT_ACCESSORS.md +++ b/docs/AGGREGATE_RESULT_ACCESSORS.md @@ -107,11 +107,11 @@ if (quantityAgg != null) Console.WriteLine($"Total quantity: {quantityAgg.Sum}"); } -// Access boolean aggregation +// Access boolean aggregation (its members are null when the server did not send them) var inStockAgg = result.Boolean("inStock"); -if (inStockAgg != null) +if (inStockAgg?.PercentageTrue is { } inStockPercentage) { - Console.WriteLine($"In stock: {inStockAgg.PercentageTrue:P0}"); + Console.WriteLine($"In stock: {inStockPercentage:P0}"); } // Access date aggregation @@ -157,9 +157,13 @@ if (result.TryGetText("category", out var category)) } // Generic TryGet -if (result.TryGet("inStock", out var stockAgg)) +if ( + result.TryGet("inStock", out var stockAgg) + && stockAgg.TotalTrue is { } inStock + && stockAgg.TotalFalse is { } outOfStock +) { - Console.WriteLine($"In stock: {stockAgg.TotalTrue}, Out of stock: {stockAgg.TotalFalse}"); + Console.WriteLine($"In stock: {inStock}, Out of stock: {outOfStock}"); } ``` @@ -196,7 +200,11 @@ result.Match("field", text: t => Console.WriteLine($"Text: {t.Count} occurrences"), integer: i => Console.WriteLine($"Integer: sum={i.Sum}"), number: n => Console.WriteLine($"Number: mean={n.Mean}"), - boolean: b => Console.WriteLine($"Boolean: {b.PercentageTrue:P0} true"), + boolean: b => + { + if (b.PercentageTrue is { } percentage) + Console.WriteLine($"Boolean: {percentage:P0} true"); + }, date: d => Console.WriteLine($"Date range: {d.Minimum} to {d.Maximum}") ); @@ -205,7 +213,10 @@ var description = result.Match("field", text: t => $"Text with {t.TopOccurrences.Count} unique values", integer: i => $"Integer ranging from {i.Minimum} to {i.Maximum}", number: n => $"Number with mean {n.Mean:F2}", - boolean: b => $"Boolean: {b.PercentageTrue:P0} true", + boolean: b => + b.PercentageTrue is { } percentage + ? $"Boolean: {percentage:P0} true" + : "Boolean: percentage not returned", date: d => $"Dates from {d.Minimum:d} to {d.Maximum:d}" ); @@ -223,7 +234,10 @@ foreach (var (name, _) in result.Properties) text: t => $"{name}: {t.Count} items, top: {t.TopOccurrences.FirstOrDefault()?.Value}", integer: i => $"{name}: range [{i.Minimum}, {i.Maximum}], sum: {i.Sum}", number: n => $"{name}: range [{n.Minimum:F2}, {n.Maximum:F2}], mean: {n.Mean:F2}", - boolean: b => $"{name}: {b.TotalTrue} true, {b.TotalFalse} false", + boolean: b => + b.TotalTrue is { } trueCount && b.TotalFalse is { } falseCount + ? $"{name}: {trueCount} true, {falseCount} false" + : $"{name}: boolean counts not returned", date: d => $"{name}: {d.Minimum:d} to {d.Maximum:d}" ); @@ -312,10 +326,13 @@ For boolean properties: | Property | Type | Description | |----------|------|-------------| | `Count` | `long?` | Number of values | -| `TotalTrue` | `long` | Count of true values | -| `TotalFalse` | `long` | Count of false values | -| `PercentageTrue` | `double` | Percentage of true values (0-1) | -| `PercentageFalse` | `double` | Percentage of false values (0-1) | +| `TotalTrue` | `long?` | Count of true values | +| `TotalFalse` | `long?` | Count of false values | +| `PercentageTrue` | `double?` | Percentage of true values (0-1) | +| `PercentageFalse` | `double?` | Percentage of false values (0-1) | + +Every member is nullable: null means the server did not send that value, which is not the same as +zero. Check before formatting — interpolating a null renders as an empty string. ### Aggregate.Date diff --git a/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs b/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs index f12fdfe2..cce1cc3e 100644 --- a/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs +++ b/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs @@ -905,14 +905,13 @@ await collectionClient.Data.Insert( } /// - /// When a filter matches nothing there is no maximum, no mean and no sum, and the server - /// says so by leaving those optional proto fields unset. The client must report null rather - /// than the zero value of the field's type: a caller cannot tell a returned 0 apart from a - /// genuine aggregate of zeros. Date already handled this correctly, so it is asserted - /// alongside as the reference behaviour. + /// Two ways a scalar goes missing, on one collection: a filter that matches nothing, and a + /// sub-metric the caller never asked for. Both leave the optional proto field unset, and the + /// client must report null rather than the zero of the field's type — a caller cannot tell a + /// returned 0 apart from a genuine aggregate of zeros. /// [Fact] - public async Task Test_OverAll_With_NoMatches_Returns_Null_Not_Zero() + public async Task Test_OverAll_Absent_Metrics_Are_Null_Not_Zero() { var collectionClient = await CollectionFactory( properties: new[] @@ -920,8 +919,6 @@ public async Task Test_OverAll_With_NoMatches_Returns_Null_Not_Zero() Property.Text("text"), Property.Int("int"), Property.Number("float"), - Property.Bool("bool"), - Property.Date("date"), } ); @@ -929,104 +926,48 @@ await collectionClient.Data.Insert( new { text = "one", - @int = 1, - @float = 1.0, - @bool = true, - date = new DateTime(2021, 1, 1, 0, 0, 0, DateTimeKind.Utc), + @int = 7, + @float = 7.5, }, cancellationToken: TestContext.Current.CancellationToken ); - var result = await collectionClient.Aggregate.OverAll( + // Nothing matches: the metrics were requested, but there is nothing to compute them over. + var noMatches = await collectionClient.Aggregate.OverAll( filters: Filter.Property("text").IsEqual("no-such-value"), returnMetrics: [ Metrics.ForProperty("int").Integer(maximum: true, mean: true, sum: true), Metrics.ForProperty("float").Number(maximum: true, mean: true, sum: true), - Metrics - .ForProperty("bool") - .Boolean( - percentageFalse: true, - percentageTrue: true, - totalFalse: true, - totalTrue: true - ), - Metrics.ForProperty("date").Date(maximum: true, minimum: true), ], cancellationToken: TestContext.Current.CancellationToken ); - Assert.Equal(0, result.TotalCount); - - var integer = Assert.IsType(result.Properties["int"]); - Assert.Null(integer.Maximum); - Assert.Null(integer.Mean); - Assert.Null(integer.Sum); - - var number = Assert.IsType(result.Properties["float"]); - Assert.Null(number.Maximum); - Assert.Null(number.Mean); - Assert.Null(number.Sum); + Assert.Equal(0, noMatches.TotalCount); - // Booleans behave differently and are asserted for what the server actually does: it - // sends all four, the counts as 0 and the percentages as NaN (0/0). They are present, - // so a presence check correctly surfaces them rather than inventing null. - var boolean = Assert.IsType(result.Properties["bool"]); - Assert.Equal(0, boolean.TotalFalse); - Assert.Equal(0, boolean.TotalTrue); - Assert.NotNull(boolean.PercentageFalse); - Assert.True(double.IsNaN(boolean.PercentageFalse.Value)); - Assert.NotNull(boolean.PercentageTrue); - Assert.True(double.IsNaN(boolean.PercentageTrue.Value)); - - // Reference: the Date branch already used presence checks before this fix. - var date = Assert.IsType(result.Properties["date"]); - Assert.Null(date.Maximum); - Assert.Null(date.Minimum); - } - - /// - /// A numeric sub-metric the caller did not ask for is not computed and not sent, so it must - /// read back as null. Objects do match here, so the nulls come from the metric selection - /// alone rather than from an empty result set — the complement of the test above. - /// Booleans are asserted for what Weaviate 1.39.0 actually does: it fills in all four - /// members whatever was requested, so they are present and carry real values. - /// - [Fact] - public async Task Test_OverAll_Unrequested_Metrics_Are_Null() - { - var collectionClient = await CollectionFactory( - properties: new[] - { - Property.Int("int"), - Property.Number("float"), - Property.Bool("bool"), - } - ); + var emptyInteger = Assert.IsType(noMatches.Properties["int"]); + Assert.Null(emptyInteger.Maximum); + Assert.Null(emptyInteger.Mean); + Assert.Null(emptyInteger.Sum); - await collectionClient.Data.Insert( - new - { - @int = 7, - @float = 7.5, - @bool = true, - }, - cancellationToken: TestContext.Current.CancellationToken - ); + var emptyNumber = Assert.IsType(noMatches.Properties["float"]); + Assert.Null(emptyNumber.Maximum); + Assert.Null(emptyNumber.Mean); + Assert.Null(emptyNumber.Sum); - var result = await collectionClient.Aggregate.OverAll( + // Objects do match here, so the nulls come from the metric selection alone. + var unrequested = await collectionClient.Aggregate.OverAll( returnMetrics: [ Metrics.ForProperty("int").Integer(maximum: true), Metrics.ForProperty("float").Number(mean: true), - Metrics.ForProperty("bool").Boolean(totalTrue: true), ], cancellationToken: TestContext.Current.CancellationToken ); - Assert.Equal(1, result.TotalCount); + Assert.Equal(1, unrequested.TotalCount); - var integer = Assert.IsType(result.Properties["int"]); + var integer = Assert.IsType(unrequested.Properties["int"]); Assert.Equal(7, integer.Maximum); Assert.Null(integer.Mean); Assert.Null(integer.Median); @@ -1034,16 +975,10 @@ await collectionClient.Data.Insert( Assert.Null(integer.Mode); Assert.Null(integer.Sum); - var number = Assert.IsType(result.Properties["float"]); + var number = Assert.IsType(unrequested.Properties["float"]); Assert.Equal(7.5, number.Mean); Assert.Null(number.Maximum); Assert.Null(number.Minimum); Assert.Null(number.Sum); - - var boolean = Assert.IsType(result.Properties["bool"]); - Assert.Equal(1, boolean.TotalTrue); - Assert.Equal(0, boolean.TotalFalse); - Assert.Equal(1, boolean.PercentageTrue); - Assert.Equal(0, boolean.PercentageFalse); } } diff --git a/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs b/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs index 4f5241ab..37277b82 100644 --- a/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs +++ b/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs @@ -1,4 +1,5 @@ using Weaviate.Client.Models; +using Agg = Weaviate.Client.Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation; namespace Weaviate.Client.Tests.Unit; @@ -787,39 +788,28 @@ public void Group_Match_Func_ReturnsCorrectValue() /// leaves one unset the client must report null, not the field type's zero — a returned 0, /// 0.0 or false is indistinguishable from a real aggregate. Driven straight off the proto /// message so an unset field is guaranteed, which a live server will not always produce: - /// Weaviate 1.39.0 always fills the four boolean members in, so only this test pins them. + /// it always fills the four boolean members in, so only this test pins them. /// [Fact] public void FromGrpcProperty_UnsetScalars_MapToNull() { var integer = AggregateResult.FromGrpcProperty( - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation - { - Property = "intField", - Int = - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Integer(), - } + new Agg { Property = "intField", Int = new Agg.Types.Integer() } ); var typedInteger = Assert.IsType(integer); + Assert.Null(typedInteger.Count); Assert.Null(typedInteger.Maximum); Assert.Null(typedInteger.Mean); Assert.Null(typedInteger.Median); Assert.Null(typedInteger.Minimum); Assert.Null(typedInteger.Mode); Assert.Null(typedInteger.Sum); - // Count is deliberately still read unconditionally, as python does: a count of 0 is a - // meaningful answer for an empty aggregate, not an absent one. - Assert.Equal(0, typedInteger.Count); var number = AggregateResult.FromGrpcProperty( - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation - { - Property = "floatField", - Number = - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Number(), - } + new Agg { Property = "floatField", Number = new Agg.Types.Number() } ); var typedNumber = Assert.IsType(number); + Assert.Null(typedNumber.Count); Assert.Null(typedNumber.Maximum); Assert.Null(typedNumber.Mean); Assert.Null(typedNumber.Median); @@ -828,18 +818,26 @@ public void FromGrpcProperty_UnsetScalars_MapToNull() Assert.Null(typedNumber.Sum); var boolean = AggregateResult.FromGrpcProperty( - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation - { - Property = "boolField", - Boolean = - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Boolean(), - } + new Agg { Property = "boolField", Boolean = new Agg.Types.Boolean() } ); var typedBoolean = Assert.IsType(boolean); + Assert.Null(typedBoolean.Count); Assert.Null(typedBoolean.PercentageFalse); Assert.Null(typedBoolean.PercentageTrue); Assert.Null(typedBoolean.TotalFalse); Assert.Null(typedBoolean.TotalTrue); + + var text = AggregateResult.FromGrpcProperty( + new Agg { Property = "textField", Text = new Agg.Types.Text() } + ); + var typedText = Assert.IsType(text); + Assert.Null(typedText.Count); + + var date = AggregateResult.FromGrpcProperty( + new Agg { Property = "dateField", Date = new Agg.Types.Date() } + ); + var typedDate = Assert.IsType(date); + Assert.Null(typedDate.Count); } /// @@ -850,20 +848,19 @@ public void FromGrpcProperty_UnsetScalars_MapToNull() public void FromGrpcProperty_SetScalars_MapToValues() { var integer = AggregateResult.FromGrpcProperty( - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + new Agg { Property = "intField", - Int = - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Integer - { - Count = 3, - Maximum = 0, - Mean = 0, - Median = 2, - Minimum = -5, - Mode = 1, - Sum = 0, - }, + Int = new Agg.Types.Integer + { + Count = 3, + Maximum = 0, + Mean = 0, + Median = 2, + Minimum = -5, + Mode = 1, + Sum = 0, + }, } ); var typedInteger = Assert.IsType(integer); @@ -876,20 +873,19 @@ public void FromGrpcProperty_SetScalars_MapToValues() Assert.Equal(0, typedInteger.Sum); var number = AggregateResult.FromGrpcProperty( - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + new Agg { Property = "floatField", - Number = - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Number - { - Count = 2, - Maximum = 0, - Mean = 0, - Median = 1.5, - Minimum = -1.5, - Mode = 0, - Sum = 0, - }, + Number = new Agg.Types.Number + { + Count = 2, + Maximum = 0, + Mean = 0, + Median = 1.5, + Minimum = -1.5, + Mode = 0, + Sum = 0, + }, } ); var typedNumber = Assert.IsType(number); @@ -901,18 +897,17 @@ public void FromGrpcProperty_SetScalars_MapToValues() Assert.Equal(0, typedNumber.Sum); var boolean = AggregateResult.FromGrpcProperty( - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation + new Agg { Property = "boolField", - Boolean = - new Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation.Types.Boolean - { - Count = 4, - PercentageFalse = 0, - PercentageTrue = 1, - TotalFalse = 0, - TotalTrue = 4, - }, + Boolean = new Agg.Types.Boolean + { + Count = 4, + PercentageFalse = 0, + PercentageTrue = 1, + TotalFalse = 0, + TotalTrue = 4, + }, } ); var typedBoolean = Assert.IsType(boolean); diff --git a/src/Weaviate.Client/Models/Aggregate.cs b/src/Weaviate.Client/Models/Aggregate.cs index 9c2a30a3..afc41814 100644 --- a/src/Weaviate.Client/Models/Aggregate.cs +++ b/src/Weaviate.Client/Models/Aggregate.cs @@ -716,11 +716,12 @@ V1.AggregateReply.Types.Aggregations.Types.Aggregation x { return x.AggregationCase switch { + // Every scalar here is optional in aggregate.proto: unset maps to null, not 0/false. V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Text => (Aggregate.Property) new Aggregate.Text { - Count = x.Text.Count, + Count = x.Text.HasCount ? x.Text.Count : null, TopOccurrences = ( x.Text.TopOccurences is null ? [] @@ -733,15 +734,10 @@ x.Text.TopOccurences is null ) ).ToList(), }, - // Every scalar below is `optional` in aggregate.proto, and the server leaves them - // unset when there is nothing to aggregate — an empty result set. Reading them - // without a presence check yields the field type's zero, which a caller cannot tell - // apart from a real 0/0.0/false, so each one is gated on its Has* flag exactly as - // the Date branch further down already does. V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Int => new Aggregate.Integer { - Count = x.Int.Count, + Count = x.Int.HasCount ? x.Int.Count : null, Maximum = x.Int.HasMaximum ? x.Int.Maximum : null, Mean = x.Int.HasMean ? x.Int.Mean : null, Median = x.Int.HasMedian ? x.Int.Median : null, @@ -752,7 +748,7 @@ x.Text.TopOccurences is null V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Number => new Aggregate.Number { - Count = x.Number.Count, + Count = x.Number.HasCount ? x.Number.Count : null, Maximum = x.Number.HasMaximum ? x.Number.Maximum : null, Mean = x.Number.HasMean ? x.Number.Mean : null, Median = x.Number.HasMedian ? x.Number.Median : null, @@ -763,7 +759,7 @@ x.Text.TopOccurences is null V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Boolean => new Aggregate.Boolean { - Count = x.Boolean.Count, + Count = x.Boolean.HasCount ? x.Boolean.Count : null, PercentageFalse = x.Boolean.HasPercentageFalse ? x.Boolean.PercentageFalse : null, @@ -774,7 +770,7 @@ x.Text.TopOccurences is null V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Date => new Aggregate.Date { - Count = x.Date.Count, + Count = x.Date.HasCount ? x.Date.Count : null, Maximum = x.Date.HasMaximum ? DateTime.Parse( x.Date.Maximum, @@ -1276,26 +1272,22 @@ public record Number : Numeric { }; public record Boolean : Property { /// - /// Gets or sets the value of the percentage false, or null when the server returned no - /// value because nothing matched. + /// Gets or sets the value of the percentage false, or null when the server did not send the value. /// public double? PercentageFalse { get; internal set; } /// - /// Gets or sets the value of the percentage true, or null when the server returned no - /// value because nothing matched. + /// Gets or sets the value of the percentage true, or null when the server did not send the value. /// public double? PercentageTrue { get; internal set; } /// - /// Gets or sets the value of the total false, or null when the server returned no value - /// because nothing matched. + /// Gets or sets the value of the total false, or null when the server did not send the value. /// public long? TotalFalse { get; internal set; } /// - /// Gets or sets the value of the total true, or null when the server returned no value - /// because nothing matched. + /// Gets or sets the value of the total true, or null when the server did not send the value. /// public long? TotalTrue { get; internal set; } }; diff --git a/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs b/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs index 308d42b2..a8cab359 100644 --- a/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs +++ b/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs @@ -397,38 +397,6 @@ Aggregate.Text t _ => null, }; } - - /// - /// Converts a long value to the target type. - /// - private static object? ConvertToNumeric(long value, Type targetType) - { - return targetType switch - { - _ when targetType == typeof(long) => value, - _ when targetType == typeof(int) => (int)value, - _ when targetType == typeof(double) => (double)value, - _ when targetType == typeof(float) => (float)value, - _ when targetType == typeof(decimal) => (decimal)value, - _ => null, - }; - } - - /// - /// Converts a double value to the target type. - /// - private static object? ConvertToNumeric(double value, Type targetType) - { - return targetType switch - { - _ when targetType == typeof(double) => value, - _ when targetType == typeof(float) => (float)value, - _ when targetType == typeof(decimal) => (decimal)value, - _ when targetType == typeof(long) => (long)value, - _ when targetType == typeof(int) => (int)value, - _ => null, - }; - } } /// From 4fa7d5ae7cd283f2041040d637d0106ef313d8f1 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:32:46 +0200 Subject: [PATCH 11/14] revert: drop the vectorizeCollectionName deprecation sweep C# cannot mark a parameter [Obsolete] (CS0592), so the sweep marked only the property - and every assignment to it sat behind a pragma inside the library. A caller passing vectorizeCollectionName: true got no diagnostic at all, which is the one audience the deprecation was for. The cost was 38 suppression lines. Python never surfaced vectorize_collection_name on its multivector factories, so there is nothing to deprecate toward; converging on that is a deliberate breaking change and does not belong in this PR. Keeps the type-level [Obsolete] on the Multi2VecGoogleGemini shim, which is real, and 55b0693's Gemini collapse. Also make the Gemini serialization tests assert absence instead of an explicit null, which only tested the test's own serializer options. --- .../Integration/TestVectorizers.cs | 16 +---- .../Unit/TestVectorizers.cs | 25 ++++--- .../Configure/VectorizerFactory.cs | 68 +++++-------------- .../Configure/VectorizerFactoryMulti.cs | 8 +-- src/Weaviate.Client/Models/Vectorizer.cs | 55 +++------------ 5 files changed, 44 insertions(+), 128 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs index 22bd301a..2278c234 100644 --- a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs @@ -11,12 +11,6 @@ public class TestVectorizers : IntegrationTests { /// /// Tests that the Google Gemini multimodal factory produces a collection the server accepts. - /// This is the whole point of the factory and it used to be impossible: the config emitted - /// the module name multi2vec-google-gemini, which no Weaviate build provides, so - /// creation failed with no module with name "multi2vec-google-gemini" present. Gemini - /// is reached through multi2vec-google (wire name multi2vec-palm) by pointing - /// apiEndpoint at the generative-language host, so the round trip must show that - /// module, that endpoint, and no Vertex-only project id or location. /// [Fact] public async Task Test_Multi2VecGoogleGemini_Creates_Collection() @@ -48,11 +42,8 @@ public async Task Test_Multi2VecGoogleGemini_Creates_Collection() Assert.Equal("multi2vec-palm", google.Identifier); Assert.Equal("generativelanguage.googleapis.com", google.ApiEndpoint); Assert.Equal(512, google.Dimensions); - // The factory does not offer vectorizeClassName because no multi2vec module reads it, - // and the server confirms it: nothing is sent, and nothing is stored or defaulted back. -#pragma warning disable CS0618 // Reading the obsolete property is the assertion + // The factory has no vectorizeClassName: nothing is sent, nothing stored or defaulted back. Assert.Null(google.VectorizeCollectionName); -#pragma warning restore CS0618 // Reading the obsolete property is the assertion Assert.NotNull(google.ImageFields); Assert.Equal(["image"], google.ImageFields); Assert.NotNull(google.TextFields); @@ -63,10 +54,7 @@ public async Task Test_Multi2VecGoogleGemini_Creates_Collection() } /// - /// Tests that the Vertex AI multimodal factory still produces a collection the server - /// accepts, and that project id and location survive the round trip. Kept alongside the - /// Gemini case because both now share one config type: a regression that dropped either - /// field would otherwise only show up on the Vertex path. + /// Tests that the Vertex AI factory creates a collection with project id and location intact. /// [Fact] public async Task Test_Multi2VecGoogle_Vertex_Creates_Collection() diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs index 1254958e..a0bf713d 100644 --- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; using Weaviate.Client.Models; using Weaviate.Client.Models.Vectorizers; using Quantizers = Weaviate.Client.Models.VectorIndex.Quantizers; @@ -377,9 +378,12 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() var dto = vc.Vectorizer?.ToDto() ?? default; var json = JsonSerializer.Serialize( dto, + // Mirrors WeaviateRestClient.RestJsonSerializerOptions, so what is absent here is + // absent on the wire. new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, } ); @@ -388,20 +392,16 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() Assert.Contains("\"multi2vec-palm\"", json); Assert.DoesNotContain("multi2vec-google-gemini", json); Assert.Contains("\"apiEndpoint\":\"generativelanguage.googleapis.com\"", json); - // Vertex-only settings the Gemini API has no equivalent of. This test serializes with a - // bare options object, so nulls are written out; the REST client's options drop them, so - // neither key reaches the wire. - Assert.Contains("\"projectId\":null", json); - Assert.Contains("\"location\":null", json); + // Vertex-only settings the Gemini API has no equivalent of; neither may be sent. + Assert.DoesNotContain("projectId", json); + Assert.DoesNotContain("location", json); Assert.Contains("\"imageFields\":[\"image\"]", json); Assert.Contains("\"textFields\":[\"text\"]", json); Assert.Contains("\"videoFields\":[\"video\"]", json); Assert.Contains("\"audioFields\":[\"audio\"]", json); Assert.Contains("\"dimensions\":512", json); - // The property is inherited shipped API on Multi2VecGoogle, so unlike Multi2VecTwelveLabs - // it still serializes here — but only as null under this bare options object, and the - // REST client's options drop it before the wire. - Assert.Contains("\"vectorizeClassName\":null", json); + // Inherited shipped API on Multi2VecGoogle, but this factory never sets it. + Assert.DoesNotContain("vectorizeClassName", json); Assert.DoesNotContain("\"weights\"", json); } @@ -440,9 +440,12 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields() var dto = vc.Vectorizer?.ToDto() ?? default; var json = JsonSerializer.Serialize( dto, + // Mirrors WeaviateRestClient.RestJsonSerializerOptions, so what is absent here is + // absent on the wire. new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, } ); @@ -462,8 +465,8 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields() ); // dimensions is left unset by this case, so the omit-when-null path stays covered; // vectorizeClassName is never settable through this factory at all. - Assert.Contains("\"dimensions\":null", json); - Assert.Contains("\"vectorizeClassName\":null", json); + Assert.DoesNotContain("dimensions", json); + Assert.DoesNotContain("vectorizeClassName", json); Assert.DoesNotContain("depthFields", json); Assert.DoesNotContain("imuFields", json); Assert.DoesNotContain("thermalFields", json); diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs index 914af693..9ae331f5 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactory.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs @@ -61,7 +61,7 @@ public VectorizerConfig Text2VecWeaviate( /// AWS region. /// Model name to use. /// Number of vector dimensions. - /// Deprecated, has no effect. + /// Whether to vectorize the collection name. /// Multi2VecAWSBedrock vectorizer configuration. public VectorizerConfig Multi2VecAWSBedrock( WeightedFields imageFields, @@ -78,9 +78,7 @@ public VectorizerConfig Multi2VecAWSBedrock( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -95,7 +93,7 @@ public VectorizerConfig Multi2VecAWSBedrock( /// AWS region. /// Model name to use. /// Number of vector dimensions. - /// Deprecated, has no effect. + /// Whether to vectorize the collection name. /// Multi2VecAWSBedrock vectorizer configuration. public VectorizerConfig Multi2VecAWSBedrock( string[]? imageFields = null, @@ -112,9 +110,7 @@ public VectorizerConfig Multi2VecAWSBedrock( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -123,7 +119,7 @@ public VectorizerConfig Multi2VecAWSBedrock( /// Weighted image fields. /// Weighted text fields. /// Inference URL for the model. - /// Deprecated, has no effect. + /// Whether to vectorize the collection name. /// Multi2VecClip vectorizer configuration. public VectorizerConfig Multi2VecClip( WeightedFields imageFields, @@ -136,9 +132,7 @@ public VectorizerConfig Multi2VecClip( ImageFields = ModalityFields.OrNull(imageFields), InferenceUrl = inferenceUrl, TextFields = ModalityFields.OrNull(textFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -151,7 +145,7 @@ public VectorizerConfig Multi2VecClip( /// Array of image field names. /// Array of text field names. /// Inference URL for the model. - /// Deprecated, has no effect. + /// Whether to vectorize the collection name. /// Multi2VecClip vectorizer configuration. public VectorizerConfig Multi2VecClip( string[]? imageFields = null, @@ -164,9 +158,7 @@ public VectorizerConfig Multi2VecClip( ImageFields = ModalityFields.OrNull(imageFields), InferenceUrl = inferenceUrl, TextFields = ModalityFields.OrNull(textFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -178,7 +170,7 @@ public VectorizerConfig Multi2VecClip( /// Model name to use. /// Number of vector dimensions. /// Truncation strategy. - /// Deprecated, has no effect. + /// Whether to vectorize the collection name. /// Multi2VecCohere vectorizer configuration. public VectorizerConfig Multi2VecCohere( WeightedFields imageFields, @@ -197,9 +189,7 @@ public VectorizerConfig Multi2VecCohere( Dimensions = dimensions, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields @@ -215,7 +205,7 @@ public VectorizerConfig Multi2VecCohere( /// Model name to use. /// Number of vector dimensions. /// Truncation strategy. - /// Deprecated, has no effect. + /// Whether to vectorize the collection name. /// Multi2VecCohere vectorizer configuration. public VectorizerConfig Multi2VecCohere( string[]? imageFields = null, @@ -234,9 +224,7 @@ public VectorizerConfig Multi2VecCohere( Dimensions = dimensions, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -249,7 +237,7 @@ public VectorizerConfig Multi2VecCohere( /// Weighted IMU fields. /// Weighted thermal fields. /// Weighted video fields. - /// Deprecated, has no effect. + /// Whether to vectorize the collection name. /// Multi2VecBind vectorizer configuration. public VectorizerConfig Multi2VecBind( WeightedFields imageFields, @@ -270,9 +258,7 @@ public VectorizerConfig Multi2VecBind( TextFields = ModalityFields.OrNull(textFields), ThermalFields = ModalityFields.OrNull(thermalFields), VideoFields = ModalityFields.OrNull(videoFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // This overload's parameters happen to be in FromWeightedFields' own declaration // order (image, text, audio, depth, imu, thermal, video), so the named arguments // are a safeguard rather than a correction: they keep the mapping right if either @@ -298,7 +284,7 @@ public VectorizerConfig Multi2VecBind( /// The imu fields /// The thermal fields /// The video fields - /// Deprecated, has no effect. + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecBind( string[]? imageFields = null, @@ -319,9 +305,7 @@ public VectorizerConfig Multi2VecBind( TextFields = ModalityFields.OrNull(textFields), ThermalFields = ModalityFields.OrNull(thermalFields), VideoFields = ModalityFields.OrNull(videoFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -336,7 +320,7 @@ public VectorizerConfig Multi2VecBind( /// The video interval seconds /// The model /// The dimensions - /// Deprecated, has no effect. + /// The vectorize collection name /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( @@ -364,9 +348,7 @@ public VectorizerConfig Multi2VecGoogle( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -391,7 +373,7 @@ public VectorizerConfig Multi2VecGoogle( /// The video interval seconds /// The model /// The dimensions - /// Deprecated, has no effect. + /// The vectorize collection name /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( @@ -419,9 +401,7 @@ public VectorizerConfig Multi2VecGoogle( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -450,8 +430,7 @@ public VectorizerConfig Multi2VecGoogleGemini( ) => new Multi2VecGoogle { - // The Gemini API is not scoped to a Vertex AI project or region; left null so both - // are omitted on the wire. + // The Gemini API has no Vertex project or region; null keeps both off the wire. ProjectId = null, Location = null, ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com", @@ -462,10 +441,7 @@ public VectorizerConfig Multi2VecGoogleGemini( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - // Not offered as a parameter here, unlike Vertex; null keeps it off the wire. -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = null, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -504,8 +480,7 @@ public VectorizerConfig Multi2VecGoogleGemini( ) => new Multi2VecGoogle { - // The Gemini API is not scoped to a Vertex AI project or region; left null so both - // are omitted on the wire. + // The Gemini API has no Vertex project or region; null keeps both off the wire. ProjectId = null, Location = null, ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com", @@ -516,10 +491,7 @@ public VectorizerConfig Multi2VecGoogleGemini( VideoIntervalSeconds = videoIntervalSeconds, ModelId = model, Dimensions = dimensions, - // Not offered as a parameter here, unlike Vertex; null keeps it off the wire. -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = null, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -532,7 +504,7 @@ public VectorizerConfig Multi2VecGoogleGemini( /// The dimensions /// The model /// The truncate - /// Deprecated, has no effect. + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecVoyageAI( WeightedFields imageFields, @@ -553,9 +525,7 @@ public VectorizerConfig Multi2VecVoyageAI( TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), Truncate = truncate, -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment // FromWeightedFields declares seven optional modalities in the order image, text, // audio, depth, imu, thermal, video. Video is the one at risk — positionally it // would land in audioFields, which this module does not have — and it was already @@ -577,7 +547,7 @@ public VectorizerConfig Multi2VecVoyageAI( /// The dimensions /// The model /// The truncate - /// Deprecated, has no effect. + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecVoyageAI( string[]? imageFields = null, @@ -598,9 +568,7 @@ public VectorizerConfig Multi2VecVoyageAI( Model = model, TextFields = ModalityFields.OrNull(textFields), Truncate = truncate, -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -1141,7 +1109,7 @@ public VectorizerConfig Text2VecJinaAI( /// The model /// The base url /// The dimensions - /// Deprecated, has no effect. + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecJinaAI( string[]? imageFields = null, @@ -1158,9 +1126,7 @@ public VectorizerConfig Multi2VecJinaAI( Dimensions = dimensions, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -1171,7 +1137,7 @@ public VectorizerConfig Multi2VecJinaAI( /// The model /// The base url /// The dimensions - /// Deprecated, has no effect. + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2VecJinaAI( WeightedFields imageFields, @@ -1192,9 +1158,7 @@ public VectorizerConfig Multi2VecJinaAI( imageFields: imageFields, textFields: textFields ), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; #pragma warning restore CA1822 // Mark members as static } diff --git a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs index 2c678750..f816d8d8 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs @@ -52,7 +52,7 @@ public VectorizerConfig Text2MultiVecJinaAI( /// The text fields /// The base url /// The model - /// Deprecated, has no effect. + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2MultiVecJinaAI( string[]? imageFields = null, @@ -67,9 +67,7 @@ public VectorizerConfig Multi2MultiVecJinaAI( Model = model, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment }; /// @@ -79,7 +77,7 @@ public VectorizerConfig Multi2MultiVecJinaAI( /// The text fields /// The base url /// The model - /// Deprecated, has no effect. + /// The vectorize collection name /// The vectorizer config public VectorizerConfig Multi2MultiVecJinaAI( WeightedFields imageFields, @@ -94,9 +92,7 @@ public VectorizerConfig Multi2MultiVecJinaAI( Model = model, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), -#pragma warning disable CS0618 // Inert for multivector; WEAVIATE002 requires the assignment VectorizeCollectionName = vectorizeCollectionName, -#pragma warning restore CS0618 // Inert for multivector; WEAVIATE002 requires the assignment Weights = VectorizerWeights.FromWeightedFields( imageFields: imageFields, textFields: textFields diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs index cf310fdf..af202ec9 100644 --- a/src/Weaviate.Client/Models/Vectorizer.cs +++ b/src/Weaviate.Client/Models/Vectorizer.cs @@ -187,12 +187,9 @@ internal Multi2VecAWS() { } public string[]? TextFields { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -233,12 +230,9 @@ internal Multi2VecClip() { } public string[]? TextFields { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -295,12 +289,9 @@ internal Multi2VecCohere() { } public string? Truncate { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -361,12 +352,9 @@ internal Multi2VecBind() { } public string[]? VideoFields { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -448,12 +436,9 @@ internal Multi2VecGoogle() { } public int? Dimensions { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -478,20 +463,9 @@ internal Multi2VecPalm() { } } /// - /// Deprecated. Use Multi2VecGoogle instead. - /// This type used to declare the module name multi2vec-google-gemini, which no server - /// has ever provided, so a collection configured with it could not be created. The Gemini API - /// is reached through the multi2vec-google module by setting - /// to generativelanguage.googleapis.com, - /// which is what - /// now returns. Kept only so existing source that names the type still compiles. + /// Deprecated. Use instead. /// - [Obsolete( - "Multi2VecGoogleGemini declared the non-existent module 'multi2vec-google-gemini' and " - + "could never create a collection. VectorizerFactory.Multi2VecGoogleGemini(...) now " - + "returns a Multi2VecGoogle with ApiEndpoint = 'generativelanguage.googleapis.com'; " - + "bind the result as Multi2VecGoogle (or VectorizerConfig) instead." - )] + [Obsolete("Use Multi2VecGoogle with ApiEndpoint = 'generativelanguage.googleapis.com'.")] public record Multi2VecGoogleGemini : Multi2VecGoogle { /// @@ -541,12 +515,9 @@ internal Multi2VecJinaAI() { } public string[]? TextFields { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// @@ -601,12 +572,9 @@ internal Multi2MultiVecJinaAI() { } internal VectorizerWeights? Weights { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; } @@ -690,12 +658,9 @@ internal Multi2VecVoyageAI() { } public bool? Truncate { get; set; } = null; /// - /// Deprecated, has no effect. - /// No multivector module reads this setting server-side; it is only registered as a - /// class-config default. Retained because removing it would be a breaking change. + /// Gets or sets the value of the vectorize collection name /// [JsonPropertyName("vectorizeClassName")] - [Obsolete("Has no effect: no multivector module reads this setting.")] public bool? VectorizeCollectionName { get; set; } = null; /// From b40902aa06dfc3549a0622284d302a4074e5162e Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:33:02 +0200 Subject: [PATCH 12/14] docs: add the changelog entries for this release Records the additions, the fixes and the breaking changes, including the two the description had not called out: Multi2VecGoogleGemini.Model is removed with no replacement under that name, and BackupCreateRequest went from six positional parameters to seven. Corrects three entries that still advertise Multi2VecGoogleGemini as a working vectorizer; the 1.0.1 entry called it new when it could never create a collection. The original text stays, with the correction after it. --- CHANGELOG.md | 27 ++++++++++++++++--- .../Integration/TestCollections.cs | 13 ++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484f2996..a3495756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`generative-deepseek`** — New `GenerativeConfigFactory.Deepseek(...)` collection config and `GenerativeProviderFactory.Deepseek(...)` runtime provider for the `generative-deepseek` module. Requires Weaviate ≥ 1.36.19. +- **Incremental Backups** — `BackupCreateRequest.IncrementalBaseBackupId` names an existing backup to build on, so files unchanged since that backup are not copied again; `Backup.IncrementalBaseBackupId` surfaces it on `List()` and `GetStatus()`. `Create` throws `WeaviateVersionMismatchException` below 1.37.0, the documented feature floor. The server has accepted the field on create since 1.34.18, but only returns it on read from 1.37.6. +- **`apiEndpoint` on `Multi2VecGoogle`** — Both `VectorizerFactory.Multi2VecGoogle(...)` overloads take an optional `apiEndpoint`, which selects the Gemini API (`generativelanguage.googleapis.com`) instead of Vertex AI. Both `Multi2VecGoogleGemini(...)` overloads also gained an optional `dimensions`. + +### Fixed + +- **Backup `Size` Dropped on List** — `BackupClient.List()` discarded the size and incremental base id it had already parsed from the response, so `Backup.Size` was null for every listed backup. Both fields are now mapped. +- **Aggregate Zeros for Absent Values** — Int, Number, Boolean, Text and Date results, `Count` included, reported `0`, `0.0` or `false` where the server had sent no value at all. Every scalar is now presence-checked and left null when unset, so an empty aggregation is distinguishable from a genuine zero. +- **`Multi2VecGoogleGemini` Emitted a Non-Existent Module** — The vectorizer declared the module name `multi2vec-google-gemini`, which no Weaviate server provides, so a collection configured with it could never be created. `VectorizerFactory.Multi2VecGoogleGemini(...)` now emits `multi2vec-google` with the Gemini API endpoint. + +### Changed + +- **`Multi2VecGoogleGemini` is now an `[Obsolete]` shim over `Multi2VecGoogle`** — This breaks at runtime, not only at compile time: the factory now returns a `Multi2VecGoogle`, so `is`/`as`/pattern matches on `Multi2VecGoogleGemini` silently stop matching, and a `switch` with arms for both types no longer compiles (`CS8120: The switch case is unreachable`). Bind the result as `Multi2VecGoogle` or `VectorizerConfig`. +- **`Multi2VecGoogle.ProjectId` and `.Location` are no longer `required`** — Both getters changed from `string!` to `string?` so the Gemini endpoint, which is scoped to neither a project nor a region, can omit them. Callers with nullable reference types enabled may see CS8600/CS8601. +- **`Aggregate.Boolean` counts are nullable** — `PercentageTrue`, `PercentageFalse`, `TotalTrue` and `TotalFalse` changed from `double`/`long` to `double?`/`long?`. +- **`BackupCreateRequest` gained a trailing optional parameter** — Its primary constructor and `Deconstruct` went from six parameters to seven, so six-element positional deconstruction (`var (id, backend, inc, exc, cpu, comp) = req;`) no longer compiles; deconstruct seven elements instead. +- **Binary compatibility** — The signature changes above are source-compatible for callers using named arguments, but not binary-compatible: assemblies compiled against 1.1.0 throw `MissingMethodException` until recompiled. + ### 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. +- **`Multi2VecGoogleGemini.Model`** — Removed along with the rest of the type's own members; the base `Multi2VecGoogle` declares the same setting as `ModelId`. Migrate `.Model` → `.ModelId`. --- @@ -67,7 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Vectorizers -- **Audio Field Support** ([#302](https://github.com/weaviate/weaviate-csharp-client/pull/302)): `Multi2VecGoogle` and `Multi2VecGoogleGemini` vectorizers now support audio field configurations with configurable per-field weights. +- **Audio Field Support** ([#302](https://github.com/weaviate/weaviate-csharp-client/pull/302)): `Multi2VecGoogle` and `Multi2VecGoogleGemini` vectorizers now support audio field configurations with configurable per-field weights. (`Multi2VecGoogleGemini` is deprecated in favour of `Multi2VecGoogle` — see Unreleased.) #### API Ergonomics @@ -105,7 +126,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Weaviate 1.36 support: HFresh vector index, async replication config, property index deletion - Critical fix: gRPC vector serialization no longer doubles dimensions for non-`float[]` vectors - Opt-in structured logging via `ILoggerFactory` -- New vectorizers: `Multi2VecGoogleGemini` and `Multi2MultivecWeaviate` +- New vectorizers: `Multi2VecGoogleGemini` (never functional — see Unreleased) and `Multi2MultivecWeaviate` ### Added @@ -119,7 +140,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Vectorizers -- **Multi2VecGoogleGemini** ([#297](https://github.com/weaviate/weaviate-csharp-client/pull/297)): New vectorizer calling the Google Gemini API directly. Supports image, text, and video field weighting. No project ID or location required (unlike the Vertex AI variant). Defaults to `generativelanguage.googleapis.com`. +- **Multi2VecGoogleGemini** ([#297](https://github.com/weaviate/weaviate-csharp-client/pull/297)): New vectorizer calling the Google Gemini API directly. Supports image, text, and video field weighting. No project ID or location required (unlike the Vertex AI variant). Defaults to `generativelanguage.googleapis.com`. **Correction:** this vectorizer declared the module name `multi2vec-google-gemini`, which no Weaviate server provides, so it could never create a collection; fixed in Unreleased. - **Multi2MultivecWeaviate** ([#291](https://github.com/weaviate/weaviate-csharp-client/pull/291)): Support for the `multi2multivec-weaviate` vectorizer, which produces multi-vector embeddings using Weaviate's built-in model. - **Cohere Reranker `BaseURL`** ([#287](https://github.com/weaviate/weaviate-csharp-client/pull/287)): Added `BaseURL` property to `RerankerCohereConfig` and a corresponding parameter to `RerankerConfigFactory.Cohere()`, enabling self-hosted or regional Cohere endpoints. diff --git a/src/Weaviate.Client.Tests/Integration/TestCollections.cs b/src/Weaviate.Client.Tests/Integration/TestCollections.cs index 62075890..73eeffea 100644 --- a/src/Weaviate.Client.Tests/Integration/TestCollections.cs +++ b/src/Weaviate.Client.Tests/Integration/TestCollections.cs @@ -200,17 +200,8 @@ public async Task Collection_Creates_And_Retrieves_Generative_Config() /// than silently falling back to a default. /// /// - /// Every float here is deliberately integral. On a class that uses named vectors, Weaviate - /// 1.39.0 rejects any fractional float in this module's config — temperature: 0.7 - /// comes back as "Wrong temperature configuration, values are between 0.0 and 2.0". The - /// client sends plain JSON numbers and the same payload is accepted on a class-level - /// vectorizer, so this is a server-side defect, not a client one: the settings helper - /// receives the value as a json.Number, its non-integer path returns the caller's - /// "defaultValue", and generative-deepseek passes a -100 sentinel there - /// (usecases/modulecomponents/settings/class_settings_property_helper.go getNumberValue, - /// modules/generative-deepseek/config/class_settings.go getFloatProperty). - /// generative-openai fails identically. Fractional values and the exact wire keys are - /// covered by the unit test instead. + /// Integral floats only; the server drops fractional floats for this module on named-vector + /// classes. See PR #368. Fractional values are covered by the unit test instead. /// [Fact] public async Task Collection_Creates_And_Retrieves_GenerativeDeepseek_Config() From 3cb5430595ed3149a07fd69d6fe46fdf1d7fc01f Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:36:33 +0200 Subject: [PATCH 13/14] test: gate the Gemini vectorizer test on 1.34.20 multi2vec-google has no apiEndpoint before 1.34.20 (introduced in 4862194, in no 1.32.x or 1.33.x tag), so those servers fall through to the Vertex mandatory check and reject the Gemini config for missing projectId/location. RequireModule does not cover this: the module is on every lane, only the Gemini config shape is version-dependent. Verified on real servers: without the gate 1.32.27 reproduces the CI error; with it the test skips there and still runs on 1.34.20. --- src/Weaviate.Client.Tests/Integration/TestVectorizers.cs | 3 +++ src/Weaviate.Client/Configure/VectorizerFactory.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs index 2278c234..b25d4e20 100644 --- a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs @@ -16,6 +16,9 @@ public class TestVectorizers : IntegrationTests public async Task Test_Multi2VecGoogleGemini_Creates_Collection() { RequireModule("multi2vec-google"); + // The module is on every lane, but its Gemini path (apiEndpoint) only lands in 1.34.20; + // older builds still demand the Vertex projectId/location this factory omits. + RequireVersion("1.34.20"); var collection = await CollectionFactory( name: "TestMulti2VecGoogleGemini", diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs index 9ae331f5..0ad8282d 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactory.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs @@ -408,6 +408,7 @@ public VectorizerConfig Multi2VecGoogle( /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API). /// Emits the multi2vec-google module with the Gemini API endpoint; there is no /// separate Gemini module, so no project id or location is sent. + /// Requires Weaviate server version 1.34.20 or later. /// /// The image fields /// The text fields @@ -458,6 +459,7 @@ public VectorizerConfig Multi2VecGoogleGemini( /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API). /// Emits the multi2vec-google module with the Gemini API endpoint; there is no /// separate Gemini module, so no project id or location is sent. + /// Requires Weaviate server version 1.34.20 or later. /// /// The image fields /// The text fields From d12049fdae2d76e56d7330566d0de930c7d8b5f6 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:18:44 +0200 Subject: [PATCH 14/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Weaviate.Client/Configure/GenerativeProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Weaviate.Client/Configure/GenerativeProvider.cs b/src/Weaviate.Client/Configure/GenerativeProvider.cs index 57e8b91a..5d0b9d6c 100644 --- a/src/Weaviate.Client/Configure/GenerativeProvider.cs +++ b/src/Weaviate.Client/Configure/GenerativeProvider.cs @@ -317,7 +317,7 @@ public Providers.Databricks Databricks( }; /// - /// Deepseeks the base url + /// Creates a DeepSeek runtime provider configuration. /// /// The base url /// The model