From 140c8efe3bdddda8cf95943d32715a3f8193b18b Mon Sep 17 00:00:00 2001
From: Ivan Despot <66276597+g-despot@users.noreply.github.com>
Date: Wed, 12 Aug 2026 08:06:18 +0200
Subject: [PATCH 1/4] feat: add multi2vec-twelvelabs vectorizer config support
Adds the Vectorizer.Multi2VecTwelveLabs config record and matching
VectorizerFactory.Multi2VecTwelveLabs overloads (string arrays and
WeightedFields), mirroring the existing multi2vec siblings. Fields per
the server module: baseURL, model, imageFields, textFields,
vectorizeClassName, weights. Requires Weaviate 1.38.9+ / 1.39.0+.
---
.../Unit/TestVectorizers.cs | 163 ++++++++++++++++++
.../Configure/VectorizerFactory.cs | 51 ++++++
src/Weaviate.Client/Models/Vectorizer.cs | 46 +++++
src/Weaviate.Client/PublicAPI.Unshipped.txt | 24 +++
4 files changed, 284 insertions(+)
diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
index a502c6fb..30ee7c86 100644
--- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
+++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
@@ -619,4 +619,167 @@ public void Test_Text2VecAWS_Omits_Unset_Dimensions()
Assert.Contains("\"text2vec-aws\"", json);
Assert.DoesNotContain("\"dimensions\"", json);
}
+
+ ///
+ /// Tests that Multi2VecTwelveLabs serializes all fields correctly under the
+ /// multi2vec-twelvelabs module key.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecTwelveLabs_Serializes_All_Fields()
+ {
+ // Arrange
+ var vc = Configure.Vector(
+ "default",
+ v =>
+ v.Multi2VecTwelveLabs(
+ imageFields: new[] { "image" },
+ textFields: new[] { "text" },
+ baseURL: "https://api.twelvelabs.io/v1.3",
+ model: "marengo3.0",
+ vectorizeCollectionName: false
+ )
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-twelvelabs\"", json);
+ Assert.Contains("\"baseURL\":\"https://api.twelvelabs.io/v1.3\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"model\":\"marengo3.0\"", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ Assert.Contains("\"vectorizeClassName\":false", json);
+ }
+
+ ///
+ /// Tests that Multi2VecTwelveLabs omits unset optional fields so the server can apply
+ /// its defaults (no baseURL, model or vectorizeClassName).
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecTwelveLabs_Omits_Unset_Optional_Fields()
+ {
+ // Arrange
+ var vc = Configure.Vector(
+ "default",
+ v => v.Multi2VecTwelveLabs(imageFields: new[] { "image" }, textFields: new[] { "text" })
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System
+ .Text
+ .Json
+ .Serialization
+ .JsonIgnoreCondition
+ .WhenWritingNull,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-twelvelabs\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ Assert.DoesNotContain("\"baseURL\"", json);
+ Assert.DoesNotContain("\"model\"", json);
+ Assert.DoesNotContain("\"vectorizeClassName\"", json);
+ }
+
+ ///
+ /// Tests that the Multi2VecTwelveLabs WeightedFields overload maps the field names into
+ /// the imageFields and textFields arrays. The weights themselves do not
+ /// reach the wire today (family-wide internal-Weights serialization bug, board task
+ /// b9f5cdf8); the last assertion pins that behavior and flips when the bug is fixed.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Maps_Field_Names()
+ {
+ // Arrange
+ var imageFields = new WeightedFields { ("image", 0.7) };
+ var textFields = new WeightedFields { ("text", 0.3) };
+
+ var vc = Configure.Vector(
+ "default",
+ v => v.Multi2VecTwelveLabs(imageFields: imageFields, textFields: textFields)
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-twelvelabs\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ // Pins current behavior: internal Weights is invisible to the serializer (b9f5cdf8).
+ Assert.DoesNotContain("\"weights\"", json);
+ }
+
+ ///
+ /// Tests that Multi2VecTwelveLabs deserializes from the multi2vec-twelvelabs
+ /// module configuration returned by the server.
+ ///
+ [Fact]
+ public void Test_Multi2VecTwelveLabs_Deserialization()
+ {
+ // Arrange
+ var parameters = new Dictionary
+ {
+ ["baseURL"] = "https://api.twelvelabs.io/v1.3",
+ ["imageFields"] = new[] { "image" },
+ ["model"] = "marengo3.0",
+ ["textFields"] = new[] { "text" },
+ ["vectorizeClassName"] = false,
+ };
+
+ // Act
+ var config = VectorizerConfigFactory.Create("multi2vec-twelvelabs", parameters);
+
+ // Assert
+ var twelveLabs = Assert.IsType(config);
+ Assert.Equal("multi2vec-twelvelabs", twelveLabs.Identifier);
+ Assert.Equal("https://api.twelvelabs.io/v1.3", twelveLabs.BaseURL);
+ Assert.NotNull(twelveLabs.ImageFields);
+ Assert.Equal(["image"], twelveLabs.ImageFields);
+ Assert.Equal("marengo3.0", twelveLabs.Model);
+ Assert.NotNull(twelveLabs.TextFields);
+ Assert.Equal(["text"], twelveLabs.TextFields);
+ Assert.False(twelveLabs.VectorizeCollectionName);
+ }
}
diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs
index 70eb6e63..d7634be0 100644
--- a/src/Weaviate.Client/Configure/VectorizerFactory.cs
+++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs
@@ -522,6 +522,57 @@ public VectorizerConfig Multi2VecVoyageAI(
VectorizeCollectionName = vectorizeCollectionName,
};
+ ///
+ /// Creates a configuration for the Multi2VecTwelveLabs vectorizer using weighted fields.
+ ///
+ /// Weighted image fields.
+ /// Weighted text fields.
+ /// Optional base URL for the model.
+ /// Model name to use.
+ /// Whether to vectorize the collection name.
+ /// Multi2VecTwelveLabs vectorizer configuration.
+ public VectorizerConfig Multi2VecTwelveLabs(
+ WeightedFields imageFields,
+ WeightedFields textFields,
+ string? baseURL = null,
+ string? model = null,
+ bool? vectorizeCollectionName = null
+ ) =>
+ new Multi2VecTwelveLabs
+ {
+ BaseURL = baseURL,
+ ImageFields = imageFields,
+ Model = model,
+ TextFields = textFields,
+ VectorizeCollectionName = vectorizeCollectionName,
+ Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ };
+
+ ///
+ /// Creates a configuration for the Multi2VecTwelveLabs vectorizer using string arrays.
+ ///
+ /// Array of image field names.
+ /// Array of text field names.
+ /// Optional base URL for the model.
+ /// Model name to use.
+ /// Whether to vectorize the collection name.
+ /// Multi2VecTwelveLabs vectorizer configuration.
+ public VectorizerConfig Multi2VecTwelveLabs(
+ string[]? imageFields = null,
+ string[]? textFields = null,
+ string? baseURL = null,
+ string? model = null,
+ bool? vectorizeCollectionName = null
+ ) =>
+ new Multi2VecTwelveLabs
+ {
+ BaseURL = baseURL,
+ ImageFields = imageFields,
+ Model = model,
+ TextFields = textFields,
+ VectorizeCollectionName = vectorizeCollectionName,
+ };
+
///
/// Refs the 2 vec centroid using the specified reference properties
///
diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs
index 92326145..2d20a9da 100644
--- a/src/Weaviate.Client/Models/Vectorizer.cs
+++ b/src/Weaviate.Client/Models/Vectorizer.cs
@@ -650,6 +650,52 @@ internal Multi2VecVoyageAI() { }
internal VectorizerWeights? Weights { get; set; } = null;
}
+ ///
+ /// The configuration for multi-media vectorization using the TwelveLabs module.
+ /// See the documentation for detailed usage.
+ ///
+ [Vectorizer("multi2vec-twelvelabs")]
+ public record Multi2VecTwelveLabs : VectorizerConfig
+ {
+ ///
+ /// Initializes a new instance of the class
+ ///
+ [JsonConstructor]
+ internal Multi2VecTwelveLabs() { }
+
+ ///
+ /// Gets or sets the value of the base url
+ ///
+ [JsonPropertyName("baseURL")]
+ public string? BaseURL { get; set; } = null;
+
+ ///
+ /// Gets or sets the value of the image fields
+ ///
+ public string[]? ImageFields { get; set; } = null;
+
+ ///
+ /// Gets or sets the value of the model
+ ///
+ public string? Model { 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 vectorize collection name
+ ///
+ [JsonPropertyName("vectorizeClassName")]
+ public bool? VectorizeCollectionName { get; set; } = null;
+
+ ///
+ /// Gets or sets the value of the weights
+ ///
+ internal VectorizerWeights? Weights { get; set; } = null;
+ }
+
///
/// The configuration for reference-based vectorization using the centroid method.
/// See the documentation for detailed usage.
diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt
index 9b7e6354..7a332351 100644
--- a/src/Weaviate.Client/PublicAPI.Unshipped.txt
+++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt
@@ -30,3 +30,27 @@ Weaviate.Client.Models.Permissions.Namespaces.Manage.get -> bool
Weaviate.Client.Models.Permissions.Namespaces.Manage.set -> void
Weaviate.Client.Models.Permissions.Namespaces.Namespaces(string? namespace) -> void
Weaviate.Client.Models.Permissions.Namespaces.Resource.get -> Weaviate.Client.Models.NamespacesResource!
+override sealed Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Equals(Weaviate.Client.Models.VectorizerConfig? other) -> bool
+override Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.$() -> Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs!
+override Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.EqualityContract.get -> System.Type!
+override Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Equals(object? obj) -> bool
+override Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.GetHashCode() -> int
+override Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.PrintMembers(System.Text.StringBuilder! builder) -> bool
+override Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.ToString() -> string!
+static Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.operator !=(Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs? left, Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs? right) -> bool
+static Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.operator ==(Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs? left, Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs? right) -> bool
+virtual Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Equals(Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs? other) -> bool
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.BaseURL.get -> string?
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.BaseURL.set -> void
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.ImageFields.get -> string![]?
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.ImageFields.set -> void
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Model.get -> string?
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Model.set -> void
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Multi2VecTwelveLabs(Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs! original) -> void
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.TextFields.get -> string![]?
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.TextFields.set -> void
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.VectorizeCollectionName.get -> bool?
+Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.VectorizeCollectionName.set -> void
+Weaviate.Client.VectorizerFactory.Multi2VecTwelveLabs(string![]? imageFields = null, string![]? textFields = null, string? baseURL = null, string? model = null, bool? vectorizeCollectionName = 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, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig!
From c788369aef769e636904b298d6141d2229fbcea1 Mon Sep 17 00:00:00 2001
From: Ivan Despot <66276597+g-despot@users.noreply.github.com>
Date: Thu, 13 Aug 2026 09:54:08 +0200
Subject: [PATCH 2/4] fix: serialize multimodal vectorizer weights, and correct
the Google factories
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
VectorizerWeights was assigned by every weighted multi2vec factory but
never reached the wire: the property is internal and System.Text.Json
skips non-public members, so all ten weighted overloads silently dropped
their weights. Adds [JsonInclude] with WhenWritingNull, and returns null
from FromWeightedFields when no modality carries weights, so the key is
omitted rather than emitted as an empty object.
Making weights serialize exposed a transposition in Multi2VecGoogle and
Multi2VecGoogleGemini, which passed videoFields and audioFields into the
audioFields and depthFields parameters. Inert while weights were dropped;
live it labels video weights as audio, parks audio weights under a
modality the module does not have, and can fail collection creation with
a weights count mismatch. All ten call sites now use named arguments.
Tests assert the full weights object with a distinct value per modality —
the previous substring-presence assertions passed despite the
transposition — and cover Bind and VoyageAI, which had no weighted
coverage.
---
.../Unit/TestVectorizers.cs | 381 ++++++++++++++++--
.../Configure/VectorizerFactory.cs | 71 +++-
.../Configure/VectorizerFactoryMulti.cs | 5 +-
src/Weaviate.Client/Models/Vectorizer.cs | 107 +++--
4 files changed, 477 insertions(+), 87 deletions(-)
diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
index 30ee7c86..9fde6714 100644
--- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
+++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
@@ -233,7 +233,8 @@ public void Test_Multi2MultiVecWeaviate_Serializes_ImageFields()
}
///
- /// Tests that Multi2VecGoogle serializes audioFields correctly with string arrays
+ /// Tests that Multi2VecGoogle maps each string-array modality to its own key, and that the
+ /// unweighted overload emits no weights object.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -269,15 +270,23 @@ public void Test_Multi2VecGoogle_Serializes_AudioFields_StringArray()
);
// Assert
- Assert.Contains("\"audioFields\"", json);
- Assert.Contains("\"audio\"", json);
- Assert.Contains("\"imageFields\"", json);
- Assert.Contains("\"textFields\"", json);
- Assert.Contains("\"videoFields\"", json);
+ Assert.Contains("\"multi2vec-palm\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ Assert.Contains("\"videoFields\":[\"video\"]", json);
+ Assert.Contains("\"audioFields\":[\"audio\"]", json);
+ Assert.DoesNotContain("\"weights\"", json);
}
///
- /// Tests that Multi2VecGoogle serializes audioFields correctly with WeightedFields
+ /// Tests that Multi2VecGoogle routes every modality's weights to that modality's key.
+ /// The whole weights object is asserted, with a distinct value per modality and a
+ /// distinct field count for video versus audio, because the factory calls
+ /// FromWeightedFields — whose parameters run image, text, audio, depth, imu,
+ /// thermal, video — and a positional call there silently files video weights under
+ /// audioFields and audio weights under depthFields, a modality this module
+ /// does not have. A substring-presence assertion cannot see that swap; an equality
+ /// assertion on the whole object can.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -288,10 +297,10 @@ public void Test_Multi2VecGoogle_Serializes_AudioFields_StringArray()
public void Test_Multi2VecGoogle_Serializes_AudioFields_WeightedFields()
{
// Arrange
- var imageFields = new WeightedFields { ("image", 0.7) };
- var textFields = new WeightedFields { ("text", 0.8) };
- var videoFields = new WeightedFields { ("video", 0.6) };
- var audioFields = new WeightedFields { ("audio", 0.9) };
+ var imageFields = new WeightedFields { ("image", 0.11), ("thumbnail", 0.12) };
+ var textFields = new WeightedFields { ("text", 0.21) };
+ var videoFields = new WeightedFields { ("video", 0.31), ("clip", 0.32) };
+ var audioFields = new WeightedFields { ("audio", 0.41) };
var vc = Configure.Vector(
"default",
@@ -318,15 +327,26 @@ public void Test_Multi2VecGoogle_Serializes_AudioFields_WeightedFields()
);
// Assert
- Assert.Contains("\"audioFields\"", json);
- Assert.Contains("\"audio\"", json);
- Assert.Contains("\"imageFields\"", json);
- Assert.Contains("\"textFields\"", json);
- Assert.Contains("\"videoFields\"", json);
+ Assert.Contains("\"multi2vec-palm\"", json);
+ Assert.Contains("\"imageFields\":[\"image\",\"thumbnail\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ Assert.Contains("\"videoFields\":[\"video\",\"clip\"]", json);
+ Assert.Contains("\"audioFields\":[\"audio\"]", json);
+ // Every weight lands under its own modality, and no modality the module does not
+ // support (depth, imu, thermal) appears.
+ Assert.Contains(
+ "\"weights\":{\"audioFields\":[0.41],\"imageFields\":[0.11,0.12],"
+ + "\"textFields\":[0.21],\"videoFields\":[0.31,0.32]}",
+ json
+ );
+ Assert.DoesNotContain("depthFields", json);
+ Assert.DoesNotContain("imuFields", json);
+ Assert.DoesNotContain("thermalFields", json);
}
///
- /// Tests that Multi2VecGoogleGemini serializes audioFields correctly with string arrays
+ /// Tests that Multi2VecGoogleGemini maps each string-array modality to its own key, and
+ /// that the unweighted overload emits no weights object.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -360,15 +380,18 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray()
);
// Assert
- Assert.Contains("\"audioFields\"", json);
- Assert.Contains("\"audio\"", json);
- Assert.Contains("\"imageFields\"", json);
- Assert.Contains("\"textFields\"", json);
- Assert.Contains("\"videoFields\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ Assert.Contains("\"videoFields\":[\"video\"]", json);
+ Assert.Contains("\"audioFields\":[\"audio\"]", json);
+ Assert.DoesNotContain("\"weights\"", json);
}
///
- /// Tests that Multi2VecGoogleGemini serializes audioFields correctly with WeightedFields
+ /// Tests that Multi2VecGoogleGemini routes every modality's weights to that modality's
+ /// key. Asserted as a whole weights object with a distinct value per modality, for
+ /// the same reason as the Multi2VecGoogle case above: this factory shares the
+ /// FromWeightedFields parameter order in which audio precedes video.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -379,10 +402,10 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray()
public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields()
{
// Arrange
- var imageFields = new WeightedFields { ("image", 0.7) };
- var textFields = new WeightedFields { ("text", 0.8) };
- var videoFields = new WeightedFields { ("video", 0.6) };
- var audioFields = new WeightedFields { ("audio", 0.9) };
+ var imageFields = new WeightedFields { ("image", 0.13), ("thumbnail", 0.14) };
+ var textFields = new WeightedFields { ("text", 0.23) };
+ var videoFields = new WeightedFields { ("video", 0.33), ("clip", 0.34) };
+ var audioFields = new WeightedFields { ("audio", 0.43) };
var vc = Configure.Vector(
"default",
@@ -407,11 +430,137 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields()
);
// Assert
- Assert.Contains("\"audioFields\"", json);
- Assert.Contains("\"audio\"", json);
- Assert.Contains("\"imageFields\"", json);
- Assert.Contains("\"textFields\"", json);
- Assert.Contains("\"videoFields\"", json);
+ Assert.Contains("\"imageFields\":[\"image\",\"thumbnail\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ Assert.Contains("\"videoFields\":[\"video\",\"clip\"]", json);
+ Assert.Contains("\"audioFields\":[\"audio\"]", json);
+ Assert.Contains(
+ "\"weights\":{\"audioFields\":[0.43],\"imageFields\":[0.13,0.14],"
+ + "\"textFields\":[0.23],\"videoFields\":[0.33,0.34]}",
+ json
+ );
+ Assert.DoesNotContain("depthFields", json);
+ Assert.DoesNotContain("imuFields", json);
+ Assert.DoesNotContain("thermalFields", json);
+ }
+
+ ///
+ /// Tests that Multi2VecBind routes all seven modalities' weights to their own keys. This
+ /// is the widest FromWeightedFields call in the client, so every weight gets its own
+ /// value: any transposition between neighbouring modalities shows up as a wrong number
+ /// rather than as a still-present key.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecBind_WeightedFields_Overload_Serializes_All_Modality_Weights()
+ {
+ // Arrange
+ var imageFields = new WeightedFields { ("image", 0.11) };
+ var textFields = new WeightedFields { ("text", 0.21) };
+ var audioFields = new WeightedFields { ("audio", 0.31) };
+ var depthFields = new WeightedFields { ("depth", 0.41) };
+ var imuFields = new WeightedFields { ("imu", 0.51) };
+ var thermalFields = new WeightedFields { ("thermal", 0.61) };
+ var videoFields = new WeightedFields { ("video", 0.71) };
+
+ var vc = Configure.Vector(
+ "default",
+ v =>
+ v.Multi2VecBind(
+ imageFields: imageFields,
+ textFields: textFields,
+ audioFields: audioFields,
+ depthFields: depthFields,
+ imuFields: imuFields,
+ thermalFields: thermalFields,
+ videoFields: videoFields
+ )
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-bind\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ Assert.Contains("\"audioFields\":[\"audio\"]", json);
+ Assert.Contains("\"depthFields\":[\"depth\"]", json);
+ Assert.Contains("\"imuFields\":[\"imu\"]", json);
+ Assert.Contains("\"thermalFields\":[\"thermal\"]", json);
+ Assert.Contains("\"videoFields\":[\"video\"]", json);
+ Assert.Contains(
+ "\"weights\":{\"audioFields\":[0.31],\"depthFields\":[0.41],"
+ + "\"imageFields\":[0.11],\"imuFields\":[0.51],\"textFields\":[0.21],"
+ + "\"thermalFields\":[0.61],\"videoFields\":[0.71]}",
+ json
+ );
+ }
+
+ ///
+ /// Tests that Multi2VecVoyageAI routes its three modalities' weights to their own keys.
+ /// Video is the modality at risk here: it is the last FromWeightedFields parameter,
+ /// so a positional third argument would land it in audioFields, which this module
+ /// does not support.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecVoyageAI_WeightedFields_Overload_Serializes_All_Modality_Weights()
+ {
+ // Arrange
+ var imageFields = new WeightedFields { ("image", 0.15) };
+ var textFields = new WeightedFields { ("text", 0.25), ("caption", 0.26) };
+ var videoFields = new WeightedFields { ("video", 0.35) };
+
+ var vc = Configure.Vector(
+ "default",
+ v =>
+ v.Multi2VecVoyageAI(
+ imageFields: imageFields,
+ textFields: textFields,
+ videoFields: videoFields
+ )
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-voyageai\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"textFields\":[\"text\",\"caption\"]", json);
+ Assert.Contains("\"videoFields\":[\"video\"]", json);
+ Assert.Contains(
+ "\"weights\":{\"imageFields\":[0.15],\"textFields\":[0.25,0.26],"
+ + "\"videoFields\":[0.35]}",
+ json
+ );
+ Assert.DoesNotContain("audioFields", json);
+ Assert.DoesNotContain("depthFields", json);
}
///
@@ -622,7 +771,9 @@ public void Test_Text2VecAWS_Omits_Unset_Dimensions()
///
/// Tests that Multi2VecTwelveLabs serializes all fields correctly under the
- /// multi2vec-twelvelabs module key.
+ /// multi2vec-twelvelabs module key, and that the unweighted overload emits no
+ /// weights key at all (asserted without DefaultIgnoreCondition, so a missing
+ /// per-property ignore condition would show up as "weights":null).
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -663,6 +814,7 @@ public void Test_Multi2VecTwelveLabs_Serializes_All_Fields()
Assert.Contains("\"model\":\"marengo3.0\"", json);
Assert.Contains("\"textFields\":[\"text\"]", json);
Assert.Contains("\"vectorizeClassName\":false", json);
+ Assert.DoesNotContain("\"weights\"", json);
}
///
@@ -711,9 +863,8 @@ public void Test_Multi2VecTwelveLabs_Omits_Unset_Optional_Fields()
///
/// Tests that the Multi2VecTwelveLabs WeightedFields overload maps the field names into
- /// the imageFields and textFields arrays. The weights themselves do not
- /// reach the wire today (family-wide internal-Weights serialization bug, board task
- /// b9f5cdf8); the last assertion pins that behavior and flips when the bug is fixed.
+ /// the imageFields and textFields arrays and emits the matching
+ /// weights object, with each weight array in the same order as its field list.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -721,10 +872,10 @@ public void Test_Multi2VecTwelveLabs_Omits_Unset_Optional_Fields()
"CA1869:Cache and reuse 'JsonSerializerOptions' instances",
Justification = ""
)]
- public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Maps_Field_Names()
+ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Maps_Field_Names_And_Weights()
{
// Arrange
- var imageFields = new WeightedFields { ("image", 0.7) };
+ var imageFields = new WeightedFields { ("image", 0.7), ("thumbnail", 0.2) };
var textFields = new WeightedFields { ("text", 0.3) };
var vc = Configure.Vector(
@@ -745,9 +896,159 @@ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Maps_Field_Names()
// Assert
Assert.Contains("\"multi2vec-twelvelabs\"", json);
- Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"imageFields\":[\"image\",\"thumbnail\"]", json);
Assert.Contains("\"textFields\":[\"text\"]", json);
- // Pins current behavior: internal Weights is invisible to the serializer (b9f5cdf8).
+ // The weights the caller supplied reach the wire, per modality, in field order.
+ Assert.Contains("\"weights\":{\"imageFields\":[0.7,0.2],\"textFields\":[0.3]}", json);
+ }
+
+ ///
+ /// Tests that a modality whose weighted field collection is empty contributes no weight
+ /// array, so the server never sees a weights.textFields shorter than
+ /// textFields.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Omits_Empty_Modality_Weights()
+ {
+ // Arrange
+ var imageFields = new WeightedFields { ("image", 0.7) };
+ var textFields = new WeightedFields();
+
+ var vc = Configure.Vector(
+ "default",
+ v => v.Multi2VecTwelveLabs(imageFields: imageFields, textFields: textFields)
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-twelvelabs\"", json);
+ Assert.Contains("\"weights\":{\"imageFields\":[0.7]}", json);
+ }
+
+ ///
+ /// Tests that when no modality supplies a weight the weights key is dropped
+ /// entirely rather than serialized as an empty object.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Omits_Weights_When_All_Empty()
+ {
+ // Arrange
+ var vc = Configure.Vector(
+ "default",
+ v =>
+ v.Multi2VecTwelveLabs(
+ imageFields: new WeightedFields(),
+ textFields: new WeightedFields()
+ )
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-twelvelabs\"", json);
+ Assert.DoesNotContain("\"weights\"", json);
+ }
+
+ ///
+ /// Tests that the weights payload is not specific to Multi2VecTwelveLabs: the same
+ /// internal Weights property on every multi2vec record now reaches the wire, pinned
+ /// here on the Multi2VecClip sibling.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecClip_WeightedFields_Overload_Serializes_Weights()
+ {
+ // Arrange
+ var imageFields = new WeightedFields { ("image", 0.9) };
+ var textFields = new WeightedFields { ("title", 0.6), ("body", 0.4) };
+
+ var vc = Configure.Vector(
+ "default",
+ v => v.Multi2VecClip(imageFields: imageFields, textFields: textFields)
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-clip\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", json);
+ Assert.Contains("\"textFields\":[\"title\",\"body\"]", json);
+ Assert.Contains("\"weights\":{\"imageFields\":[0.9],\"textFields\":[0.6,0.4]}", json);
+ }
+
+ ///
+ /// Tests that the Multi2VecClip string-array overload, which supplies no weights, emits no
+ /// weights key.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecClip_StringArray_Overload_Omits_Weights()
+ {
+ // Arrange
+ var vc = Configure.Vector(
+ "default",
+ v => v.Multi2VecClip(imageFields: new[] { "image" }, textFields: new[] { "text" })
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-clip\"", json);
Assert.DoesNotContain("\"weights\"", json);
}
diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs
index d7634be0..b33180de 100644
--- a/src/Weaviate.Client/Configure/VectorizerFactory.cs
+++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs
@@ -79,7 +79,10 @@ public VectorizerConfig Multi2VecAWSBedrock(
ImageFields = imageFields,
TextFields = textFields,
VectorizeCollectionName = vectorizeCollectionName,
- Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -130,7 +133,10 @@ public VectorizerConfig Multi2VecClip(
InferenceUrl = inferenceUrl,
TextFields = textFields,
VectorizeCollectionName = vectorizeCollectionName,
- Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -184,7 +190,10 @@ public VectorizerConfig Multi2VecCohere(
TextFields = textFields,
Truncate = truncate,
VectorizeCollectionName = vectorizeCollectionName,
- Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -250,14 +259,17 @@ public VectorizerConfig Multi2VecBind(
ThermalFields = thermalFields,
VideoFields = videoFields,
VectorizeCollectionName = vectorizeCollectionName,
+ // Named arguments are mandatory here: FromWeightedFields declares seven optional
+ // modalities in the order image, text, audio, depth, imu, thermal, video, so a
+ // positional call would silently file each modality's weights under its neighbour.
Weights = VectorizerWeights.FromWeightedFields(
- imageFields,
- textFields,
- audioFields,
- depthFields,
- imuFields,
- thermalFields,
- videoFields
+ imageFields: imageFields,
+ textFields: textFields,
+ audioFields: audioFields,
+ depthFields: depthFields,
+ imuFields: imuFields,
+ thermalFields: thermalFields,
+ videoFields: videoFields
),
};
@@ -333,11 +345,14 @@ public VectorizerConfig Multi2VecGoogle(
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 a
+ // positional call would silently file video weights under audio.
Weights = VectorizerWeights.FromWeightedFields(
- imageFields,
- textFields,
- videoFields,
- audioFields
+ imageFields: imageFields,
+ textFields: textFields,
+ videoFields: videoFields,
+ audioFields: audioFields
),
};
@@ -410,11 +425,14 @@ public VectorizerConfig Multi2VecGoogleGemini(
AudioFields = audioFields,
VideoIntervalSeconds = videoIntervalSeconds,
Model = model,
+ // Named arguments are mandatory here: FromWeightedFields declares seven optional
+ // modalities in the order image, text, audio, depth, imu, thermal, video, so a
+ // positional call would silently file video weights under audio.
Weights = VectorizerWeights.FromWeightedFields(
- imageFields,
- textFields,
- videoFields,
- audioFields
+ imageFields: imageFields,
+ textFields: textFields,
+ videoFields: videoFields,
+ audioFields: audioFields
),
};
@@ -481,9 +499,12 @@ public VectorizerConfig Multi2VecVoyageAI(
VideoFields = videoFields,
Truncate = truncate,
VectorizeCollectionName = vectorizeCollectionName,
+ // Named arguments are mandatory here: FromWeightedFields declares seven optional
+ // modalities in the order image, text, audio, depth, imu, thermal, video, so a
+ // positional call would silently file video weights under audio.
Weights = VectorizerWeights.FromWeightedFields(
- imageFields,
- textFields,
+ imageFields: imageFields,
+ textFields: textFields,
videoFields: videoFields
),
};
@@ -545,7 +566,10 @@ public VectorizerConfig Multi2VecTwelveLabs(
Model = model,
TextFields = textFields,
VectorizeCollectionName = vectorizeCollectionName,
- Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -1102,7 +1126,10 @@ public VectorizerConfig Multi2VecJinaAI(
Dimensions = dimensions,
ImageFields = imageFields,
TextFields = textFields,
- Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
VectorizeCollectionName = vectorizeCollectionName,
};
#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 16857bb8..0d702444 100644
--- a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs
+++ b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs
@@ -93,7 +93,10 @@ public VectorizerConfig Multi2MultiVecJinaAI(
ImageFields = imageFields,
TextFields = textFields,
VectorizeCollectionName = vectorizeCollectionName,
- Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs
index 2d20a9da..4f55fbd0 100644
--- a/src/Weaviate.Client/Models/Vectorizer.cs
+++ b/src/Weaviate.Client/Models/Vectorizer.cs
@@ -8,13 +8,22 @@ namespace Weaviate.Client.Models;
public static class Vectorizer
{
///
- /// Unified weights configuration for multi-media vectorizers.
- /// All fields are optional and will be omitted from JSON when null.
+ /// Unified weights configuration for multi-media vectorizers, serialized as the
+ /// weights object of a multi2vec module configuration. All modalities are optional
+ /// and are omitted from JSON when null.
///
+ ///
+ /// The server validates a modality's weight array against that modality's field array
+ /// (weights.<name>Fields does not equal number of <name>Fields), so a
+ /// weight array is only emitted when it is non-empty and therefore the same length as the
+ /// field name list it was derived from.
+ ///
internal record VectorizerWeights
{
///
- /// Creates the weighted fields using the specified image fields
+ /// Creates the weights payload from the supplied weighted field collections. A modality
+ /// with no weighted fields contributes no weight array, and when no modality supplies
+ /// weights the result is null so that the weights key is omitted entirely.
///
/// The image fields
/// The text fields
@@ -23,8 +32,8 @@ internal record VectorizerWeights
/// The imu fields
/// The thermal fields
/// The video fields
- /// The vectorizer weights
- public static VectorizerWeights FromWeightedFields(
+ /// The vectorizer weights, or null when no weights were supplied
+ public static VectorizerWeights? FromWeightedFields(
WeightedFields? imageFields = null,
WeightedFields? textFields = null,
WeightedFields? audioFields = null,
@@ -32,18 +41,38 @@ public static VectorizerWeights FromWeightedFields(
WeightedFields? imuFields = null,
WeightedFields? thermalFields = null,
WeightedFields? videoFields = null
- ) =>
- new()
+ )
+ {
+ static double[]? ToWeights(WeightedFields? fields) =>
+ fields is { Count: > 0 } ? fields.Weights : null;
+
+ var weights = new VectorizerWeights
{
- ImageFields = imageFields?.Weights,
- TextFields = textFields?.Weights,
- AudioFields = audioFields?.Weights,
- DepthFields = depthFields?.Weights,
- IMUFields = imuFields?.Weights,
- ThermalFields = thermalFields?.Weights,
- VideoFields = videoFields?.Weights,
+ ImageFields = ToWeights(imageFields),
+ TextFields = ToWeights(textFields),
+ AudioFields = ToWeights(audioFields),
+ DepthFields = ToWeights(depthFields),
+ IMUFields = ToWeights(imuFields),
+ ThermalFields = ToWeights(thermalFields),
+ VideoFields = ToWeights(videoFields),
};
+ return weights.HasWeights ? weights : null;
+ }
+
+ ///
+ /// Gets a value indicating whether any modality supplied a weight array. Internal, so it
+ /// is invisible to the serializer.
+ ///
+ internal bool HasWeights =>
+ AudioFields is not null
+ || DepthFields is not null
+ || ImageFields is not null
+ || IMUFields is not null
+ || TextFields is not null
+ || ThermalFields is not null
+ || VideoFields is not null;
+
///
/// Gets or sets the value of the audio fields
///
@@ -164,8 +193,11 @@ internal Multi2VecAWS() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -204,8 +236,11 @@ internal Multi2VecClip() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -260,8 +295,11 @@ internal Multi2VecCohere() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -320,8 +358,11 @@ internal Multi2VecBind() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -390,8 +431,11 @@ internal Multi2VecGoogle() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -457,8 +501,11 @@ internal Multi2VecGoogleGemini() { }
public string? Model { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -508,8 +555,11 @@ internal Multi2VecJinaAI() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -548,8 +598,11 @@ internal Multi2MultiVecJinaAI() { }
public string[]? TextFields { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
///
@@ -645,8 +698,11 @@ internal Multi2VecVoyageAI() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
@@ -691,8 +747,11 @@ internal Multi2VecTwelveLabs() { }
public bool? VectorizeCollectionName { get; set; } = null;
///
- /// Gets or sets the value of the weights
+ /// 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;
}
From 06490e31438fb3c4455bc22a8e48a8a57f1e88cd Mon Sep 17 00:00:00 2001
From: Ivan Despot <66276597+g-despot@users.noreply.github.com>
Date: Thu, 13 Aug 2026 10:30:04 +0200
Subject: [PATCH 3/4] fix: omit empty multimodal modalities instead of sending
empty arrays
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
An empty WeightedFields or string[] converted to [] and was sent as e.g.
"textFields": [], which the server rejects — so an image-only weighted
config failed collection creation even though its empty weight array was
already correctly omitted. Multi2VecBind was worst affected: all seven
modalities are required parameters, so callers must pass empty for the
ones they don't use, making any real subset unusable.
Normalises empty to null at the 61 modality assignment sites across all
ten weighted factories and every string[] overload, matching the guard
already applied to the weights themselves — a modality's names and its
weight array now drop out by one rule. Write path only; deserialization
of server responses is untouched.
Also corrects the named-argument comments. Only the two Google factories
were actually transposed; Bind and VoyageAI were already correct, so
their named arguments are a safeguard against future reordering rather
than a fix.
---
.../Unit/TestVectorizers.cs | 174 +++++++++++++++++-
.../Configure/VectorizerFactory.cs | 136 +++++++-------
.../Configure/VectorizerFactoryMulti.cs | 10 +-
src/Weaviate.Client/Models/ModalityFields.cs | 37 ++++
4 files changed, 281 insertions(+), 76 deletions(-)
create mode 100644 src/Weaviate.Client/Models/ModalityFields.cs
diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
index 9fde6714..56f53897 100644
--- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
+++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
@@ -509,6 +509,79 @@ public void Test_Multi2VecBind_WeightedFields_Overload_Serializes_All_Modality_W
);
}
+ ///
+ /// Tests the configuration Multi2VecBind's weighted overload forces on every caller: all
+ /// seven modalities are required parameters, so an unused one has to be passed as an empty
+ /// WeightedFields. Each empty modality must vanish from the payload — both its field
+ /// name list and its weight array — because the server rejects a modality key that is
+ /// present but empty. Without that, the overload cannot create anything but a collection
+ /// that uses all seven modalities.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecBind_WeightedFields_Overload_Omits_Empty_Modalities()
+ {
+ // Arrange — only image and audio are in use; the other five are unavoidably empty.
+ var vc = Configure.Vector(
+ "default",
+ v =>
+ v.Multi2VecBind(
+ imageFields: new WeightedFields { ("image", 0.11) },
+ textFields: new WeightedFields(),
+ audioFields: new WeightedFields { ("audio", 0.31) },
+ depthFields: new WeightedFields(),
+ imuFields: new WeightedFields(),
+ thermalFields: new WeightedFields(),
+ videoFields: new WeightedFields()
+ )
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+ var wireJson = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System
+ .Text
+ .Json
+ .Serialization
+ .JsonIgnoreCondition
+ .WhenWritingNull,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-bind\"", json);
+ Assert.Contains("\"weights\":{\"audioFields\":[0.31],\"imageFields\":[0.11]}", json);
+ Assert.DoesNotContain("\"textFields\":[]", json);
+ Assert.DoesNotContain("\"depthFields\":[]", json);
+ Assert.DoesNotContain("\"imuFields\":[]", json);
+ Assert.DoesNotContain("\"thermalFields\":[]", json);
+ Assert.DoesNotContain("\"videoFields\":[]", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", wireJson);
+ Assert.Contains("\"audioFields\":[\"audio\"]", wireJson);
+ Assert.DoesNotContain("\"textFields\"", wireJson);
+ Assert.DoesNotContain("\"depthFields\"", wireJson);
+ Assert.DoesNotContain("\"imuFields\"", wireJson);
+ Assert.DoesNotContain("\"thermalFields\"", wireJson);
+ Assert.DoesNotContain("\"videoFields\"", wireJson);
+ }
+
///
/// Tests that Multi2VecVoyageAI routes its three modalities' weights to their own keys.
/// Video is the modality at risk here: it is the last FromWeightedFields parameter,
@@ -903,9 +976,11 @@ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Maps_Field_Names_An
}
///
- /// Tests that a modality whose weighted field collection is empty contributes no weight
- /// array, so the server never sees a weights.textFields shorter than
- /// textFields.
+ /// Tests that a modality whose weighted field collection is empty drops out of the payload
+ /// completely: no weight array, and no field name list either. The server rejects a
+ /// modality key that is present but empty (must contain at least one text field name in
+ /// textFields), so emitting "textFields":[] would make this image-only
+ /// configuration impossible to create.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -934,15 +1009,36 @@ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Omits_Empty_Modalit
WriteIndented = false,
}
);
+ // The REST client serializes with WhenWritingNull, so the wire shape is the one that
+ // decides whether the server sees a textFields key at all.
+ var wireJson = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System
+ .Text
+ .Json
+ .Serialization
+ .JsonIgnoreCondition
+ .WhenWritingNull,
+ WriteIndented = false,
+ }
+ );
// Assert
Assert.Contains("\"multi2vec-twelvelabs\"", json);
Assert.Contains("\"weights\":{\"imageFields\":[0.7]}", json);
+ // The empty modality contributes no field name list, not an empty one.
+ Assert.DoesNotContain("\"textFields\":[]", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", wireJson);
+ Assert.DoesNotContain("\"textFields\"", wireJson);
}
///
- /// Tests that when no modality supplies a weight the weights key is dropped
- /// entirely rather than serialized as an empty object.
+ /// Tests that when every modality is empty the weights key is dropped entirely
+ /// rather than serialized as an empty object, and that no modality key is emitted either:
+ /// the payload carries the module key alone and the server decides what to make of it.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -972,10 +1068,78 @@ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Omits_Weights_When_
WriteIndented = false,
}
);
+ var wireJson = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System
+ .Text
+ .Json
+ .Serialization
+ .JsonIgnoreCondition
+ .WhenWritingNull,
+ WriteIndented = false,
+ }
+ );
+
+ // Assert
+ Assert.Contains("\"multi2vec-twelvelabs\"", json);
+ Assert.DoesNotContain("\"weights\"", json);
+ Assert.DoesNotContain("\"imageFields\"", wireJson);
+ Assert.DoesNotContain("\"textFields\"", wireJson);
+ }
+
+ ///
+ /// Tests that the string-array overload treats an empty array exactly like an empty
+ /// WeightedFields: the modality is omitted rather than emitted as [], which
+ /// the server rejects.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecTwelveLabs_StringArray_Overload_Omits_Empty_Modality()
+ {
+ // Arrange
+ var vc = Configure.Vector(
+ "default",
+ v => v.Multi2VecTwelveLabs(imageFields: ["image"], textFields: [])
+ );
+
+ // Act
+ var dto = vc.Vectorizer?.ToDto() ?? default;
+ var json = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ }
+ );
+ var wireJson = JsonSerializer.Serialize(
+ dto,
+ new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System
+ .Text
+ .Json
+ .Serialization
+ .JsonIgnoreCondition
+ .WhenWritingNull,
+ WriteIndented = false,
+ }
+ );
// Assert
Assert.Contains("\"multi2vec-twelvelabs\"", json);
+ Assert.DoesNotContain("\"textFields\":[]", json);
Assert.DoesNotContain("\"weights\"", json);
+ Assert.Contains("\"imageFields\":[\"image\"]", wireJson);
+ Assert.DoesNotContain("\"textFields\"", wireJson);
}
///
diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs
index b33180de..d422b245 100644
--- a/src/Weaviate.Client/Configure/VectorizerFactory.cs
+++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs
@@ -76,8 +76,8 @@ public VectorizerConfig Multi2VecAWSBedrock(
Region = region,
Model = model,
Dimensions = dimensions,
- ImageFields = imageFields,
- TextFields = textFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
@@ -108,8 +108,8 @@ public VectorizerConfig Multi2VecAWSBedrock(
Region = region,
Model = model,
Dimensions = dimensions,
- ImageFields = imageFields,
- TextFields = textFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -129,9 +129,9 @@ public VectorizerConfig Multi2VecClip(
) =>
new Multi2VecClip
{
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
InferenceUrl = inferenceUrl,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
@@ -155,9 +155,9 @@ public VectorizerConfig Multi2VecClip(
) =>
new Multi2VecClip
{
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
InferenceUrl = inferenceUrl,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -184,10 +184,10 @@ public VectorizerConfig Multi2VecCohere(
new Multi2VecCohere
{
BaseURL = baseURL,
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
Model = model,
Dimensions = dimensions,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
Truncate = truncate,
VectorizeCollectionName = vectorizeCollectionName,
Weights = VectorizerWeights.FromWeightedFields(
@@ -219,10 +219,10 @@ public VectorizerConfig Multi2VecCohere(
new Multi2VecCohere
{
BaseURL = baseURL,
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
Model = model,
Dimensions = dimensions,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
Truncate = truncate,
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -251,17 +251,18 @@ public VectorizerConfig Multi2VecBind(
) =>
new Multi2VecBind
{
- AudioFields = audioFields,
- DepthFields = depthFields,
- ImageFields = imageFields,
- IMUFields = imuFields,
- TextFields = textFields,
- ThermalFields = thermalFields,
- VideoFields = videoFields,
+ AudioFields = ModalityFields.OrNull(audioFields),
+ DepthFields = ModalityFields.OrNull(depthFields),
+ ImageFields = ModalityFields.OrNull(imageFields),
+ IMUFields = ModalityFields.OrNull(imuFields),
+ TextFields = ModalityFields.OrNull(textFields),
+ ThermalFields = ModalityFields.OrNull(thermalFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
VectorizeCollectionName = vectorizeCollectionName,
- // Named arguments are mandatory here: FromWeightedFields declares seven optional
- // modalities in the order image, text, audio, depth, imu, thermal, video, so a
- // positional call would silently file each modality's weights under its neighbour.
+ // 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
+ // parameter list is ever reordered.
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
textFields: textFields,
@@ -297,13 +298,13 @@ public VectorizerConfig Multi2VecBind(
) =>
new Multi2VecBind
{
- AudioFields = audioFields,
- DepthFields = depthFields,
- ImageFields = imageFields,
- IMUFields = imuFields,
- TextFields = textFields,
- ThermalFields = thermalFields,
- VideoFields = videoFields,
+ AudioFields = ModalityFields.OrNull(audioFields),
+ DepthFields = ModalityFields.OrNull(depthFields),
+ ImageFields = ModalityFields.OrNull(imageFields),
+ IMUFields = ModalityFields.OrNull(imuFields),
+ TextFields = ModalityFields.OrNull(textFields),
+ ThermalFields = ModalityFields.OrNull(thermalFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -337,17 +338,18 @@ public VectorizerConfig Multi2VecGoogle(
{
ProjectId = projectId,
Location = location,
- ImageFields = imageFields,
- TextFields = textFields,
- VideoFields = videoFields,
- AudioFields = audioFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
+ AudioFields = ModalityFields.OrNull(audioFields),
VideoIntervalSeconds = videoIntervalSeconds,
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 a
- // positional call would silently file video weights under audio.
+ // 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
+ // under depth.
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
textFields: textFields,
@@ -386,10 +388,10 @@ public VectorizerConfig Multi2VecGoogle(
{
ProjectId = projectId,
Location = location,
- ImageFields = imageFields,
- TextFields = textFields,
- VideoFields = videoFields,
- AudioFields = audioFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
+ AudioFields = ModalityFields.OrNull(audioFields),
VideoIntervalSeconds = videoIntervalSeconds,
ModelId = model,
Dimensions = dimensions,
@@ -419,15 +421,16 @@ public VectorizerConfig Multi2VecGoogleGemini(
new Multi2VecGoogleGemini
{
ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com",
- ImageFields = imageFields,
- TextFields = textFields,
- VideoFields = videoFields,
- AudioFields = audioFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
+ AudioFields = ModalityFields.OrNull(audioFields),
VideoIntervalSeconds = videoIntervalSeconds,
Model = model,
// Named arguments are mandatory here: FromWeightedFields declares seven optional
- // modalities in the order image, text, audio, depth, imu, thermal, video, so a
- // positional call would silently file video weights under audio.
+ // 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
+ // under depth.
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
textFields: textFields,
@@ -459,10 +462,10 @@ public VectorizerConfig Multi2VecGoogleGemini(
new Multi2VecGoogleGemini
{
ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com",
- ImageFields = imageFields,
- TextFields = textFields,
- VideoFields = videoFields,
- AudioFields = audioFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
+ AudioFields = ModalityFields.OrNull(audioFields),
VideoIntervalSeconds = videoIntervalSeconds,
Model = model,
};
@@ -493,15 +496,16 @@ public VectorizerConfig Multi2VecVoyageAI(
{
BaseURL = baseURL,
Dimensions = dimensions,
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
Model = model,
- TextFields = textFields,
- VideoFields = videoFields,
+ TextFields = ModalityFields.OrNull(textFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
Truncate = truncate,
VectorizeCollectionName = vectorizeCollectionName,
- // Named arguments are mandatory here: FromWeightedFields declares seven optional
- // modalities in the order image, text, audio, depth, imu, thermal, video, so a
- // positional call would silently file video weights under audio.
+ // 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
+ // passed by name; naming all three is a safeguard against a future reordering.
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
textFields: textFields,
@@ -535,10 +539,10 @@ public VectorizerConfig Multi2VecVoyageAI(
{
BaseURL = baseURL,
Dimensions = dimensions,
- ImageFields = imageFields,
- VideoFields = videoFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ VideoFields = ModalityFields.OrNull(videoFields),
Model = model,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
Truncate = truncate,
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -562,9 +566,9 @@ public VectorizerConfig Multi2VecTwelveLabs(
new Multi2VecTwelveLabs
{
BaseURL = baseURL,
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
Model = model,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
@@ -591,9 +595,9 @@ public VectorizerConfig Multi2VecTwelveLabs(
new Multi2VecTwelveLabs
{
BaseURL = baseURL,
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
Model = model,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -1096,8 +1100,8 @@ public VectorizerConfig Multi2VecJinaAI(
Model = model,
BaseURL = baseURL,
Dimensions = dimensions,
- ImageFields = imageFields,
- TextFields = textFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -1124,8 +1128,8 @@ public VectorizerConfig Multi2VecJinaAI(
Model = model,
BaseURL = baseURL,
Dimensions = dimensions,
- ImageFields = imageFields,
- TextFields = textFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
textFields: textFields
diff --git a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs
index 0d702444..f816d8d8 100644
--- a/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs
+++ b/src/Weaviate.Client/Configure/VectorizerFactoryMulti.cs
@@ -65,8 +65,8 @@ public VectorizerConfig Multi2MultiVecJinaAI(
{
BaseURL = baseURL,
Model = model,
- ImageFields = imageFields,
- TextFields = textFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -90,8 +90,8 @@ public VectorizerConfig Multi2MultiVecJinaAI(
{
BaseURL = baseURL,
Model = model,
- ImageFields = imageFields,
- TextFields = textFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
@@ -115,7 +115,7 @@ public VectorizerConfig Multi2MultiVecWeaviate(
{
BaseURL = baseURL,
Model = model,
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
};
#pragma warning restore CA1822 // Mark members as static
}
diff --git a/src/Weaviate.Client/Models/ModalityFields.cs b/src/Weaviate.Client/Models/ModalityFields.cs
new file mode 100644
index 00000000..5eb152af
--- /dev/null
+++ b/src/Weaviate.Client/Models/ModalityFields.cs
@@ -0,0 +1,37 @@
+namespace Weaviate.Client.Models;
+
+///
+/// Normalises the per-modality field name lists of the multi2vec configurations.
+///
+///
+/// The server's ValidateMultiModal rejects any modality key that is present but empty
+/// (must contain at least one <name> field name in <name>Fields), and rejects a
+/// null value as well (<name>Fields must be an array). A modality the caller left
+/// empty therefore has to be absent from the payload, not serialized as []. Mapping it to
+/// null achieves that: the REST serializer runs with
+/// , so a null
+/// field list emits no key at all. This mirrors the empty-collection-is-null convention already
+/// used by .
+///
+internal static class ModalityFields
+{
+ ///
+ /// Returns the field names of , or null when the modality carries no
+ /// fields. Applies the same Count: > 0 guard that
+ /// applies to the matching weight
+ /// array, so an empty modality drops out of both the field list and the weights together.
+ ///
+ /// The weighted fields supplied for one modality
+ /// The field names, or null when there are none
+ internal static string[]? OrNull(WeightedFields? fields) =>
+ fields is { Count: > 0 } ? fields.FieldNames : null;
+
+ ///
+ /// Returns , or null when the modality carries no fields. An empty
+ /// array is as unusable to the server as an empty , so the plain
+ /// string-array overloads normalise it the same way.
+ ///
+ /// The field names supplied for one modality
+ /// The field names, or null when there are none
+ internal static string[]? OrNull(string[]? fields) => fields is { Length: > 0 } ? fields : null;
+}
From 4fb078540602d9646c37453ded89995319e76cbd Mon Sep 17 00:00:00 2001
From: Ivan Despot <66276597+g-despot@users.noreply.github.com>
Date: Fri, 14 Aug 2026 08:01:24 +0200
Subject: [PATCH 4/4] fix: drop vectorizeCollectionName from
multi2vec-twelvelabs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
No multi2vec module reads the setting. In modules/multi2vec-twelvelabs the
only references are a default registration and the DefaultVectorizeClassName
constant; nothing consumes it. The python client documents it as "Deprecated,
has no effect" across all eight of its multi2vec_* factories and omits it
from multi2vec_twelvelabs entirely.
C# was worse than python here: python drops the value, C# sent it, so the
setting round-tripped through the stored schema and read as though it had
taken effect.
Removed from the record and both factory overloads. The API is unreleased —
PublicAPI.Shipped.txt has no TwelveLabs entries — so the Unshipped lines are
deleted rather than marked *REMOVED*. The other nine multi2vec records keep
theirs; removing those is a breaking change and a separate decision.
---
.../Unit/TestVectorizers.cs | 22 ++++++++++++++-----
.../Configure/VectorizerFactory.cs | 10 ++-------
src/Weaviate.Client/Models/Vectorizer.cs | 9 ++++----
src/Weaviate.Client/PublicAPI.Unshipped.txt | 6 ++---
4 files changed, 24 insertions(+), 23 deletions(-)
diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
index 56f53897..7638dcd3 100644
--- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
+++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
@@ -847,6 +847,11 @@ public void Test_Text2VecAWS_Omits_Unset_Dimensions()
/// multi2vec-twelvelabs module key, and that the unweighted overload emits no
/// weights key at all (asserted without DefaultIgnoreCondition, so a missing
/// per-property ignore condition would show up as "weights":null).
+ /// Every field the record exposes is set here, so the absence of vectorizeClassName
+ /// pins that the client never sends that key: the setting is a no-op for multi2vec modules,
+ /// and sending it makes a caller believe a value they chose took effect. Serializing without
+ /// DefaultIgnoreCondition means re-adding the property would surface here even if it
+ /// were left null.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -864,8 +869,7 @@ public void Test_Multi2VecTwelveLabs_Serializes_All_Fields()
imageFields: new[] { "image" },
textFields: new[] { "text" },
baseURL: "https://api.twelvelabs.io/v1.3",
- model: "marengo3.0",
- vectorizeCollectionName: false
+ model: "marengo3.0"
)
);
@@ -886,13 +890,16 @@ public void Test_Multi2VecTwelveLabs_Serializes_All_Fields()
Assert.Contains("\"imageFields\":[\"image\"]", json);
Assert.Contains("\"model\":\"marengo3.0\"", json);
Assert.Contains("\"textFields\":[\"text\"]", json);
- Assert.Contains("\"vectorizeClassName\":false", json);
+ // vectorizeClassName does nothing in a multi2vec module, so the client does not offer it
+ // and never puts it on the wire, not even as null.
+ Assert.DoesNotContain("vectorizeClassName", json);
Assert.DoesNotContain("\"weights\"", json);
}
///
/// Tests that Multi2VecTwelveLabs omits unset optional fields so the server can apply
- /// its defaults (no baseURL, model or vectorizeClassName).
+ /// its defaults (no baseURL and no model), and that vectorizeClassName
+ /// is absent on the wire shape too — it is not part of this vectorizer's surface at all.
///
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -1218,7 +1225,10 @@ public void Test_Multi2VecClip_StringArray_Overload_Omits_Weights()
///
/// Tests that Multi2VecTwelveLabs deserializes from the multi2vec-twelvelabs
- /// module configuration returned by the server.
+ /// module configuration returned by the server. The server stamps its own
+ /// vectorizeClassName default into the stored config even though the module never
+ /// reads it, so the payload here includes that key: the client must ignore it rather than
+ /// fail, since it no longer models the setting.
///
[Fact]
public void Test_Multi2VecTwelveLabs_Deserialization()
@@ -1230,6 +1240,7 @@ public void Test_Multi2VecTwelveLabs_Deserialization()
["imageFields"] = new[] { "image" },
["model"] = "marengo3.0",
["textFields"] = new[] { "text" },
+ // Server-supplied and unmodelled; present to prove it is tolerated, not mapped.
["vectorizeClassName"] = false,
};
@@ -1245,6 +1256,5 @@ public void Test_Multi2VecTwelveLabs_Deserialization()
Assert.Equal("marengo3.0", twelveLabs.Model);
Assert.NotNull(twelveLabs.TextFields);
Assert.Equal(["text"], twelveLabs.TextFields);
- Assert.False(twelveLabs.VectorizeCollectionName);
}
}
diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs
index d422b245..138608ea 100644
--- a/src/Weaviate.Client/Configure/VectorizerFactory.cs
+++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs
@@ -554,14 +554,12 @@ public VectorizerConfig Multi2VecVoyageAI(
/// Weighted text fields.
/// Optional base URL for the model.
/// Model name to use.
- /// Whether to vectorize the collection name.
/// Multi2VecTwelveLabs vectorizer configuration.
public VectorizerConfig Multi2VecTwelveLabs(
WeightedFields imageFields,
WeightedFields textFields,
string? baseURL = null,
- string? model = null,
- bool? vectorizeCollectionName = null
+ string? model = null
) =>
new Multi2VecTwelveLabs
{
@@ -569,7 +567,6 @@ public VectorizerConfig Multi2VecTwelveLabs(
ImageFields = ModalityFields.OrNull(imageFields),
Model = model,
TextFields = ModalityFields.OrNull(textFields),
- VectorizeCollectionName = vectorizeCollectionName,
Weights = VectorizerWeights.FromWeightedFields(
imageFields: imageFields,
textFields: textFields
@@ -583,14 +580,12 @@ public VectorizerConfig Multi2VecTwelveLabs(
/// Array of text field names.
/// Optional base URL for the model.
/// Model name to use.
- /// Whether to vectorize the collection name.
/// Multi2VecTwelveLabs vectorizer configuration.
public VectorizerConfig Multi2VecTwelveLabs(
string[]? imageFields = null,
string[]? textFields = null,
string? baseURL = null,
- string? model = null,
- bool? vectorizeCollectionName = null
+ string? model = null
) =>
new Multi2VecTwelveLabs
{
@@ -598,7 +593,6 @@ public VectorizerConfig Multi2VecTwelveLabs(
ImageFields = ModalityFields.OrNull(imageFields),
Model = model,
TextFields = ModalityFields.OrNull(textFields),
- VectorizeCollectionName = vectorizeCollectionName,
};
///
diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs
index 4f55fbd0..5d66e4f8 100644
--- a/src/Weaviate.Client/Models/Vectorizer.cs
+++ b/src/Weaviate.Client/Models/Vectorizer.cs
@@ -740,11 +740,10 @@ internal Multi2VecTwelveLabs() { }
///
public string[]? TextFields { get; set; } = null;
- ///
- /// Gets or sets the value of the vectorize collection name
- ///
- [JsonPropertyName("vectorizeClassName")]
- public bool? VectorizeCollectionName { get; set; } = null;
+ // No VectorizeCollectionName here: vectorizeClassName is a no-op for multi2vec modules
+ // (multi2vec-twelvelabs registers the default and never reads it), so exposing it would
+ // let a caller set a value that changes nothing yet reads back from the schema as if it
+ // had taken effect. The server still stamps its own default into the stored config.
///
/// Gets or sets the per-modality weights (the weights object), omitted when null.
diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt
index 7a332351..b65585ac 100644
--- a/src/Weaviate.Client/PublicAPI.Unshipped.txt
+++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt
@@ -50,7 +50,5 @@ Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Model.set -> void
Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.Multi2VecTwelveLabs(Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs! original) -> void
Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.TextFields.get -> string![]?
Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.TextFields.set -> void
-Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.VectorizeCollectionName.get -> bool?
-Weaviate.Client.Models.Vectorizer.Multi2VecTwelveLabs.VectorizeCollectionName.set -> void
-Weaviate.Client.VectorizerFactory.Multi2VecTwelveLabs(string![]? imageFields = null, string![]? textFields = null, string? baseURL = null, string? model = null, bool? vectorizeCollectionName = 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, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig!
+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!