From c067b5e54a690f2b310ae145c95397e760d0b91b Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Tue, 25 Aug 2026 16:01:25 -0300 Subject: [PATCH] fix(quantizers): use the field names the server actually reads Weaviate parses quantizer settings out of the config map by exact key, and returns them under the same names. The client used snake_case for half of them, so rescoreLimit, trainingLimit and bitCompression were dropped on write and came back null on read -- on every index type. A user who set a rescore limit never set one. rescore_limit -> rescoreLimit (BQ, SQ, RQ) training_limit -> trainingLimit (PQ, SQ) bit_compression -> bitCompression (PQ) PQ's encoder needed a shape change rather than a rename: the server nests it as encoder: {type, distribution} while the client had two flat components. PQ.encoderType() and PQ.encoderDistribution() are kept as derived accessors and the builder is unchanged, so only the canonical constructor differs. No alternate names: the snake_case spellings were never valid on the wire, so no stored config uses them. Closes #611 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU --- .../integration/CollectionsITest.java | 32 +++++++++++++++++++ .../v1/api/collections/quantizers/BQ.java | 2 +- .../v1/api/collections/quantizers/PQ.java | 31 ++++++++++++++---- .../v1/api/collections/quantizers/RQ.java | 2 +- .../v1/api/collections/quantizers/SQ.java | 4 +-- .../client6/v1/internal/json/JSONTest.java | 18 ++++++----- 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/it/java/io/weaviate/integration/CollectionsITest.java b/src/it/java/io/weaviate/integration/CollectionsITest.java index 03871d6cf..c4bc1ef19 100644 --- a/src/it/java/io/weaviate/integration/CollectionsITest.java +++ b/src/it/java/io/weaviate/integration/CollectionsITest.java @@ -29,6 +29,7 @@ import io.weaviate.client6.v1.api.collections.config.ShardStatus; import io.weaviate.client6.v1.api.collections.generative.DummyGenerative; import io.weaviate.client6.v1.api.collections.query.BaseQueryOptions; +import io.weaviate.client6.v1.api.collections.quantizers.RQ; import io.weaviate.client6.v1.api.collections.vectorindex.Hnsw; import io.weaviate.client6.v1.api.collections.vectorizers.SelfProvidedVectorizer; import io.weaviate.containers.Container; @@ -266,6 +267,37 @@ public void test_updateQuantization_uncompressed() throws IOException { .returns(Quantization.Kind.BQ, Quantization::_kind); } + /** + * The quantizer settings have to survive a round trip through the server. + * + *

+ * They used to be sent under snake_case names ({@code rescore_limit}, + * {@code training_limit}, ...) that Weaviate looks up verbatim and therefore + * never read, so every one of them was dropped on write and came back null. + * Asserting on {@code enabled} alone did not catch it. + */ + @Test + public void test_quantizerSettingsRoundTrip() throws IOException { + // Arrange + var nsThings = ns("Things"); + + var things = client.collections.create(nsThings, + c -> c.vectorConfig(VectorConfig.selfProvided( + self -> self.quantization(Quantization.rq(rq -> rq.rescoreLimit(42).bits(8)))))); + + // Act + var config = things.config.get(); + + // Assert + Assertions.assertThat(config).get() + .extracting(CollectionConfig::vectors) + .extracting("default", InstanceOfAssertFactories.type(VectorConfig.class)) + .extracting(VectorConfig::quantization) + .asInstanceOf(InstanceOfAssertFactories.type(RQ.class)) + .returns(42, RQ::rescoreLimit) + .returns(8, RQ::bits); + } + @Test public void test_updateGenerative() throws IOException { // Arrange diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/BQ.java b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/BQ.java index 9d4cdb691..bcc24d539 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/BQ.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/BQ.java @@ -9,7 +9,7 @@ public record BQ( @SerializedName("enabled") boolean enabled, - @SerializedName("rescore_limit") Integer rescoreLimit, + @SerializedName("rescoreLimit") Integer rescoreLimit, @SerializedName("cache") Boolean cache) implements Quantization { @Override diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/PQ.java b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/PQ.java index a4806c12b..348367fab 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/PQ.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/PQ.java @@ -11,10 +11,28 @@ public record PQ( @SerializedName("enabled") boolean enabled, @SerializedName("centroids") Integer centroids, @SerializedName("segments") Integer segments, - @SerializedName("encoder_type") EncoderType encoderType, - @SerializedName("encoder_distribution") EncoderDistribution encoderDistribution, - @SerializedName("training_limit") Integer trainingLimit, - @SerializedName("bit_compression") Boolean bitCompression) implements Quantization { + /** + * Encoder settings, which the server nests one level deeper as + * {@code encoder: {type, distribution}}. + */ + @SerializedName("encoder") Encoder encoder, + @SerializedName("trainingLimit") Integer trainingLimit, + @SerializedName("bitCompression") Boolean bitCompression) implements Quantization { + + /** Type of the encoder, or {@code null} if it was left at the server default. */ + public EncoderType encoderType() { + return encoder != null ? encoder.type() : null; + } + + /** Encoder distribution, or {@code null} if left at the server default. */ + public EncoderDistribution encoderDistribution() { + return encoder != null ? encoder.distribution() : null; + } + + public record Encoder( + @SerializedName("type") EncoderType type, + @SerializedName("distribution") EncoderDistribution distribution) { + } public enum EncoderType { @SerializedName("kmeans") @@ -53,8 +71,9 @@ public PQ(Builder builder) { builder.enabled, builder.centroids, builder.segments, - builder.encoderType, - builder.encoderDistribution, + builder.encoderType == null && builder.encoderDistribution == null + ? null + : new Encoder(builder.encoderType, builder.encoderDistribution), builder.trainingLimit, builder.bitCompression); } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/RQ.java b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/RQ.java index 43dbfb2e0..dc153c316 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/RQ.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/RQ.java @@ -9,7 +9,7 @@ public record RQ( @SerializedName("enabled") boolean enabled, - @SerializedName("rescore_limit") Integer rescoreLimit, + @SerializedName("rescoreLimit") Integer rescoreLimit, @SerializedName("bits") Integer bits, @SerializedName("cache") Boolean cache) implements Quantization { diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/SQ.java b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/SQ.java index ccd9f7070..7be39afa3 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/SQ.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/quantizers/SQ.java @@ -9,8 +9,8 @@ public record SQ( @SerializedName("enabled") boolean enabled, - @SerializedName("rescore_limit") Integer rescoreLimit, - @SerializedName("training_limit") Integer trainingLimit, + @SerializedName("rescoreLimit") Integer rescoreLimit, + @SerializedName("trainingLimit") Integer trainingLimit, @SerializedName("cache") Boolean cache) implements Quantization { @Override diff --git a/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java b/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java index e7c0d6203..481859e1f 100644 --- a/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java +++ b/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java @@ -1206,11 +1206,13 @@ public static Object[][] testCases() { "pq": { "enabled": true, "centroids": 8, - "encoder_distribution": "normal", - "encoder_type": "tile", + "encoder": { + "type": "tile", + "distribution": "normal" + }, "segments": 16, - "training_limit": 1024, - "bit_compression": true + "trainingLimit": 1024, + "bitCompression": true } } } @@ -1230,8 +1232,8 @@ public static Object[][] testCases() { "vectorIndexConfig": { "sq": { "enabled": true, - "rescore_limit": 10, - "training_limit": 1024, + "rescoreLimit": 10, + "trainingLimit": 1024, "cache": true } } @@ -1251,7 +1253,7 @@ public static Object[][] testCases() { "vectorIndexConfig": { "rq": { "enabled": true, - "rescore_limit": 10, + "rescoreLimit": 10, "bits": 8 } } @@ -1271,7 +1273,7 @@ public static Object[][] testCases() { "vectorIndexConfig": { "bq": { "enabled": true, - "rescore_limit": 10, + "rescoreLimit": 10, "cache": true } }