diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs
index 13ea4a57..33a74b33 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,210 @@ 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 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,
+ /// 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);
}
///
@@ -620,6 +842,422 @@ public void Test_Text2VecAWS_Omits_Unset_Dimensions()
Assert.DoesNotContain("\"dimensions\"", json);
}
+ ///
+ /// Tests that Multi2VecTwelveLabs serializes all fields correctly under the
+ /// 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(
+ "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"
+ )
+ );
+
+ // 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);
+ // 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 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(
+ "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 and emits the matching
+ /// weights object, with each weight array in the same order as its field list.
+ ///
+ [Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1869:Cache and reuse 'JsonSerializerOptions' instances",
+ Justification = ""
+ )]
+ public void Test_Multi2VecTwelveLabs_WeightedFields_Overload_Maps_Field_Names_And_Weights()
+ {
+ // Arrange
+ var imageFields = new WeightedFields { ("image", 0.7), ("thumbnail", 0.2) };
+ 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\",\"thumbnail\"]", json);
+ Assert.Contains("\"textFields\":[\"text\"]", json);
+ // 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 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(
+ "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,
+ }
+ );
+ // 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 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(
+ "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,
+ }
+ );
+ 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// Tests that Multi2VecTwelveLabs deserializes from the multi2vec-twelvelabs
+ /// 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()
+ {
+ // Arrange
+ var parameters = new Dictionary
+ {
+ ["baseURL"] = "https://api.twelvelabs.io/v1.3",
+ ["imageFields"] = new[] { "image" },
+ ["model"] = "marengo3.0",
+ ["textFields"] = new[] { "text" },
+ // Server-supplied and unmodelled; present to prove it is tolerated, not mapped.
+ ["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);
+ }
+
///
/// Tests that Text2VecOpenAI serializes endpoint when it is set.
///
diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs
index 846a78a6..bef93a37 100644
--- a/src/Weaviate.Client/Configure/VectorizerFactory.cs
+++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs
@@ -76,10 +76,13 @@ 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, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -105,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,
};
@@ -126,11 +129,14 @@ 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, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -149,9 +155,9 @@ public VectorizerConfig Multi2VecClip(
) =>
new Multi2VecClip
{
- ImageFields = imageFields,
+ ImageFields = ModalityFields.OrNull(imageFields),
InferenceUrl = inferenceUrl,
- TextFields = textFields,
+ TextFields = ModalityFields.OrNull(textFields),
VectorizeCollectionName = vectorizeCollectionName,
};
@@ -178,13 +184,16 @@ 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(imageFields, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -210,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,
};
@@ -242,22 +251,26 @@ 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,
+ // 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,
- textFields,
- audioFields,
- depthFields,
- imuFields,
- thermalFields,
- videoFields
+ imageFields: imageFields,
+ textFields: textFields,
+ audioFields: audioFields,
+ depthFields: depthFields,
+ imuFields: imuFields,
+ thermalFields: thermalFields,
+ videoFields: videoFields
),
};
@@ -285,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,
};
@@ -325,19 +338,23 @@ 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 the
+ // positional call this replaced filed video weights under audio and audio weights
+ // under depth.
Weights = VectorizerWeights.FromWeightedFields(
- imageFields,
- textFields,
- videoFields,
- audioFields
+ imageFields: imageFields,
+ textFields: textFields,
+ videoFields: videoFields,
+ audioFields: audioFields
),
};
@@ -371,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,
@@ -404,17 +421,21 @@ 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 the
+ // positional call this replaced filed video weights under audio and audio weights
+ // under depth.
Weights = VectorizerWeights.FromWeightedFields(
- imageFields,
- textFields,
- videoFields,
- audioFields
+ imageFields: imageFields,
+ textFields: textFields,
+ videoFields: videoFields,
+ audioFields: audioFields
),
};
@@ -441,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,
};
@@ -475,15 +496,19 @@ 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,
+ // 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,
- textFields,
+ imageFields: imageFields,
+ textFields: textFields,
videoFields: videoFields
),
};
@@ -514,14 +539,62 @@ 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,
};
+ ///
+ /// 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.
+ /// Multi2VecTwelveLabs vectorizer configuration.
+ public VectorizerConfig Multi2VecTwelveLabs(
+ WeightedFields imageFields,
+ WeightedFields textFields,
+ string? baseURL = null,
+ string? model = null
+ ) =>
+ new Multi2VecTwelveLabs
+ {
+ BaseURL = baseURL,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ Model = model,
+ TextFields = ModalityFields.OrNull(textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: 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.
+ /// Multi2VecTwelveLabs vectorizer configuration.
+ public VectorizerConfig Multi2VecTwelveLabs(
+ string[]? imageFields = null,
+ string[]? textFields = null,
+ string? baseURL = null,
+ string? model = null
+ ) =>
+ new Multi2VecTwelveLabs
+ {
+ BaseURL = baseURL,
+ ImageFields = ModalityFields.OrNull(imageFields),
+ Model = model,
+ TextFields = ModalityFields.OrNull(textFields),
+ };
+
///
/// Refs the 2 vec centroid using the specified reference properties
///
@@ -1027,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,
};
@@ -1055,9 +1128,12 @@ public VectorizerConfig Multi2VecJinaAI(
Model = model,
BaseURL = baseURL,
Dimensions = dimensions,
- ImageFields = imageFields,
- TextFields = textFields,
- Weights = VectorizerWeights.FromWeightedFields(imageFields, textFields),
+ ImageFields = ModalityFields.OrNull(imageFields),
+ TextFields = ModalityFields.OrNull(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..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,10 +90,13 @@ 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, textFields),
+ Weights = VectorizerWeights.FromWeightedFields(
+ imageFields: imageFields,
+ textFields: textFields
+ ),
};
///
@@ -112,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;
+}
diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs
index 8f3eae17..cf66c1bf 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,59 @@ 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;
+ }
+
+ ///
+ /// 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;
+
+ // 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.
+ /// [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 d60d9286..30fd71ba 100644
--- a/src/Weaviate.Client/PublicAPI.Unshipped.txt
+++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt
@@ -291,3 +291,25 @@ Weaviate.Client.Typed.TypedQueryClient.NearVector(Weaviate.Client.Models.Vect
Weaviate.Client.Typed.TypedQueryClient.NearVector(Weaviate.Client.Models.VectorSearchInput.FactoryFn! vectors, Weaviate.Client.Models.GroupByRequest! groupBy, Weaviate.Client.Models.Filter? filters = null, float? certainty = null, float? distance = null, Weaviate.Client.Models.Diversity? diversitySelection = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, System.Collections.Generic.IList? returnReferences = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
static Weaviate.Client.QueryClientNearTextExtensions.NearText(this Weaviate.Client.QueryClient! client, Weaviate.Client.Models.NearTextInput! query, Weaviate.Client.Models.Filter? filters = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.Diversity? diversitySelection = null, uint? autoLimit = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, System.Collections.Generic.IList? returnReferences = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
static Weaviate.Client.QueryClientNearTextExtensions.NearText(this Weaviate.Client.QueryClient! client, Weaviate.Client.Models.NearTextInput! query, Weaviate.Client.Models.GroupByRequest! groupBy, Weaviate.Client.Models.Filter? filters = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.Diversity? diversitySelection = null, uint? autoLimit = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, System.Collections.Generic.IList? returnReferences = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+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.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!