Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/Weaviate.Client.Tests/Integration/TestCollections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,47 @@ await collection.Config.Update(
Assert.Equal(456, rqQuantizer.RescoreLimit);
}

/// <summary>
/// Tests that test hnsw centered rq4
/// </summary>
[Fact]
public async Task Test_hnsw_centered_rq4()
{
RequireVersion("1.39.3", message: "RQ centering only supported in server version 1.39.3+");

var collection = await CollectionFactory(
vectorConfig:
[
Configure.Vector(
"hnswRq4c",
t => t.SelfProvided(),
new VectorIndex.HNSW
{
Quantizer = new VectorIndex.Quantizers.RQ
{
Bits = 4,
Centering = true,
RescoreLimit = 20,
TrainingLimit = 5000,
},
}
),
]
);
var config = await collection.Config.Get(TestContext.Current.CancellationToken);
Assert.NotNull(config);
var vcRQ = config.VectorConfig["hnswRq4c"];
Assert.NotNull(vcRQ);
var hnswConfig = vcRQ.VectorIndexConfig as VectorIndex.HNSW;
Assert.NotNull(hnswConfig);
var rqQuantizer = hnswConfig.Quantizer as VectorIndex.Quantizers.RQ;
Assert.NotNull(rqQuantizer);
Assert.Equal(4, rqQuantizer.Bits);
Assert.True(rqQuantizer.Centering);
Assert.Equal(20, rqQuantizer.RescoreLimit);
Assert.Equal(5000, rqQuantizer.TrainingLimit);
Comment thread
g-despot marked this conversation as resolved.
}

/// <summary>
/// Tests that test flat rq
/// </summary>
Expand Down
87 changes: 87 additions & 0 deletions src/Weaviate.Client.Tests/Unit/TestVectorIndexConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,91 @@ public void VectorIndexConfig_HFresh_From_Json_With_NamedVector()
Assert.Equal(8, rq.Bits);
Assert.Equal(50, rq.RescoreLimit);
}

/// <summary>
/// Tests that a centered 4-bit RQ config serializes <c>centering</c> and
/// <c>trainingLimit</c> under the exact keys the server reads
/// (entities/vectorindex/hnsw/rq_config.go), omits them when unset so the server
/// defaults apply, and round-trips.
/// </summary>
[Fact]
public void VectorIndexConfig_HNSW_With_Centered_RQ4_Roundtrip()
{
var original = new VectorIndex.HNSW
{
Quantizer = new VectorIndex.Quantizers.RQ
{
Bits = 4,
Centering = true,
RescoreLimit = 123,
TrainingLimit = 5012,
},
};
var plain = new VectorIndex.HNSW
{
Quantizer = new VectorIndex.Quantizers.RQ { Bits = 8, RescoreLimit = 123 },
};

var json = VectorIndexSerialization.SerializeHnsw(original);
var jsonPlain = VectorIndexSerialization.SerializeHnsw(plain);

using var doc = JsonDocument.Parse(json);
var rqJson = doc.RootElement.GetProperty("rq");
Assert.Equal(4, rqJson.GetProperty("bits").GetInt32());
Assert.True(rqJson.GetProperty("centering").GetBoolean());
Assert.Equal(123, rqJson.GetProperty("rescoreLimit").GetInt32());
Assert.Equal(5012, rqJson.GetProperty("trainingLimit").GetInt32());

// The default PQ/SQ blocks carry their own trainingLimit, so scope the omit check to rq.
using var docPlain = JsonDocument.Parse(jsonPlain);
var rqJsonPlain = docPlain.RootElement.GetProperty("rq");
Assert.False(rqJsonPlain.TryGetProperty("centering", out _));
Assert.False(rqJsonPlain.TryGetProperty("trainingLimit", out _));

var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(
json,
Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions
);
var roundtripped = (VectorIndex.HNSW?)VectorIndexSerialization.Factory("hnsw", dict);

Assert.NotNull(roundtripped?.Quantizer);
var rq4 = Assert.IsType<VectorIndex.Quantizers.RQ>(roundtripped?.Quantizer);
Assert.Equal(4, rq4.Bits);
Assert.True(rq4.Centering);
Assert.Equal(123, rq4.RescoreLimit);
Assert.Equal(5012, rq4.TrainingLimit);
}

/// <summary>
/// Tests that serializing an RQ config with centering but without 4 bits throws, matching
/// the server rule, while a training limit without centering passes through — the server
/// accepts that combination and simply ignores the value.
/// </summary>
[Fact]
public void VectorIndexConfig_RQ_Centering_Without_4_Bits_Throws()
{
var withBits8 = new VectorIndex.HNSW
{
Quantizer = new VectorIndex.Quantizers.RQ { Bits = 8, Centering = true },
};
var withBitsUnset = new VectorIndex.HNSW
{
Quantizer = new VectorIndex.Quantizers.RQ { Centering = true },
};
var trainingLimitOnly = new VectorIndex.HNSW
{
Quantizer = new VectorIndex.Quantizers.RQ { Bits = 8, TrainingLimit = 5000 },
};

var ex = Assert.Throws<WeaviateClientException>(() =>
VectorIndexSerialization.SerializeHnsw(withBits8)
);
Assert.Contains("RQ centering requires bits: 4", ex.Message);

Assert.Throws<WeaviateClientException>(() =>
VectorIndexSerialization.SerializeHnsw(withBitsUnset)
);

_ = VectorIndexSerialization.SerializeHnsw(trainingLimitOnly);
}
}
20 changes: 17 additions & 3 deletions src/Weaviate.Client/Models/Serialization.VectorIndexConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,20 @@ params QuantizerConfigBase?[] quantizers
return quantizers.FirstOrDefault(q => q?.Enabled == true);
}

// The server enforces the same rule (entities/vectorindex/hnsw/rq_config.go); failing here
// just surfaces it before the request. TrainingLimit is deliberately not checked: the server
// accepts it with any bits value and simply ignores it unless centering is enabled.
private static VectorIndex.Quantizers.RQ? ValidateRQ(VectorIndex.Quantizers.RQ? rq)
{
if (rq is { Centering: true } && rq.Bits != 4)
{
throw new WeaviateClientException(
$"RQ centering requires bits: 4, but got bits: {rq.Bits?.ToString() ?? "unset"}."
);
}
return rq;
}

// HNSW mapping
/// <summary>
/// Returns the hnsw using the specified dto
Expand Down Expand Up @@ -471,7 +485,7 @@ public static HnswDto ToDto(this VectorIndex.HNSW hnsw)
dto.SQ = hnsw.Quantizer as VectorIndex.Quantizers.SQ;
break;
case "rq":
dto.RQ = hnsw.Quantizer as VectorIndex.Quantizers.RQ;
dto.RQ = ValidateRQ(hnsw.Quantizer as VectorIndex.Quantizers.RQ);
break;
case "none":
dto.SkipDefaultQuantization = true;
Expand Down Expand Up @@ -530,7 +544,7 @@ public static FlatDto ToDto(this VectorIndex.Flat flat)
// dto.SQ = flat.Quantizer as VectorIndex.Quantizers.SQ;
// break;
case "rq":
dto.RQ = flat.Quantizer as VectorIndex.Quantizers.RQ;
dto.RQ = ValidateRQ(flat.Quantizer as VectorIndex.Quantizers.RQ);
break;
}
}
Expand Down Expand Up @@ -615,7 +629,7 @@ public static HFreshDto ToDto(this VectorIndex.HFresh hfresh)
SearchProbe = hfresh.SearchProbe,
RQ = hfresh.Quantizer switch
{
VectorIndex.Quantizers.RQ rq => rq,
VectorIndex.Quantizers.RQ rq => ValidateRQ(rq),
null => null,
_ => throw new WeaviateClientException(
$"HFresh only supports RQ quantization, but got '{hfresh.Quantizer.Type}'."
Expand Down
13 changes: 13 additions & 0 deletions src/Weaviate.Client/Models/VectorIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,19 @@ public record RQ : QuantizerConfigFlat
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool Cache { get; set; }

/// <summary>
/// Gets or sets whether to center the data before quantizing. Requires 4 bits and
/// is immutable once the collection is created (Weaviate 1.39.3 or later).
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool Centering { get; set; }

/// <summary>
/// Gets or sets the number of vectors used to train the centering statistics.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int TrainingLimit { get; set; }

/// <summary>
/// Gets the type identifier for the quantizer.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Weaviate.Client/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ Weaviate.Client.Models.GenerativeConfig.Meta.Temperature.set -> void
Weaviate.Client.Models.GenerativeConfig.Meta.TopP.get -> double?
Weaviate.Client.Models.GenerativeConfig.Meta.TopP.set -> void
Weaviate.Client.Models.GenerativeConfig.Meta.Type.get -> string!
Weaviate.Client.Models.VectorIndex.Quantizers.RQ.Centering.get -> bool
Weaviate.Client.Models.VectorIndex.Quantizers.RQ.Centering.set -> void
Weaviate.Client.Models.VectorIndex.Quantizers.RQ.TrainingLimit.get -> int
Weaviate.Client.Models.VectorIndex.Quantizers.RQ.TrainingLimit.set -> void
Loading