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!