diff --git a/src/it/java/io/weaviate/containers/Weaviate.java b/src/it/java/io/weaviate/containers/Weaviate.java index 881825def..b9f07ae28 100644 --- a/src/it/java/io/weaviate/containers/Weaviate.java +++ b/src/it/java/io/weaviate/containers/Weaviate.java @@ -282,6 +282,15 @@ public Builder enableAutoSchema(boolean enable) { return this; } + /** + * Enable asynchronous vector indexing. Required by the server to create + * "dynamic" vector indexes. + */ + public Builder enableAsyncIndexing(boolean enable) { + environment.put("ASYNC_INDEXING", Boolean.toString(enable)); + return this; + } + public Builder enableAnonymousAccess(boolean enable) { environment.put("AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED", Boolean.toString(enable)); return this; diff --git a/src/it/java/io/weaviate/integration/CollectionsITest.java b/src/it/java/io/weaviate/integration/CollectionsITest.java index 03871d6cf..dbd35f499 100644 --- a/src/it/java/io/weaviate/integration/CollectionsITest.java +++ b/src/it/java/io/weaviate/integration/CollectionsITest.java @@ -29,6 +29,9 @@ 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.Dynamic; +import io.weaviate.client6.v1.api.collections.vectorindex.Flat; import io.weaviate.client6.v1.api.collections.vectorindex.Hnsw; import io.weaviate.client6.v1.api.collections.vectorizers.SelfProvidedVectorizer; import io.weaviate.containers.Container; @@ -37,6 +40,13 @@ public class CollectionsITest extends ConcurrentTest { private static WeaviateClient client = Container.WEAVIATE.getClient(); + /** + * "dynamic" vector indexes are only accepted by a server started with + * ASYNC_INDEXING=true, which the shared container is not. + */ + private static final Weaviate asyncIndexing = Weaviate.custom() + .enableAsyncIndexing(true).build(); + @Test public void testCreateGetDelete() throws IOException { var collectionName = ns("Things"); @@ -266,6 +276,81 @@ 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. + */ + /** + * A dynamic index keeps its quantizer inside "hnsw"/"flat". + * + *

+ * The client used to write it beside them, where the server never looks, so a + * dynamic index created through this client came back unquantized -- and even + * one quantized by other means read as {@code quantization() == null}. + */ + @Test + public void test_dynamicIndexQuantizationRoundTrip() throws IOException { + // Arrange: "dynamic" is only accepted by a server started with + // ASYNC_INDEXING=true, which the shared container is not -- asynchronous + // indexing would make freshly inserted vectors searchable only eventually, + // and the other suites rely on them being searchable at once. + var asyncClient = asyncIndexing.getClient(); + var nsThings = ns("Things"); + + var things = asyncClient.collections.create(nsThings, + c -> c.vectorConfig(VectorConfig.selfProvided( + self -> self + .vectorIndex(Dynamic.of(idx -> idx + .hnsw(Hnsw.of(hnsw -> hnsw.ef(64))) + .flat(Flat.of(flat -> flat.vectorCacheMaxObjects(1000))) + .threshold(5000))) + .quantization(Quantization.rq(rq -> rq.rescoreLimit(20).bits(8)))))); + + // Act + var config = things.config.get(); + + // Assert: the server kept it, and we can read it back off the dynamic index. + Assertions.assertThat(config).get() + .extracting(CollectionConfig::vectors) + .extracting("default", InstanceOfAssertFactories.type(VectorConfig.class)) + .satisfies(vector -> { + Assertions.assertThat(vector.vectorIndex()) + .as("index type").isInstanceOf(Dynamic.class); + Assertions.assertThat(vector.quantization()) + .as("quantizer nested under hnsw").isNotNull() + .asInstanceOf(InstanceOfAssertFactories.type(RQ.class)) + .returns(20, RQ::rescoreLimit) + .returns(8, RQ::bits); + }); + } + + @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/VectorConfig.java b/src/main/java/io/weaviate/client6/v1/api/collections/VectorConfig.java index 4fa6ae6d0..ca7d47c08 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/VectorConfig.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/VectorConfig.java @@ -6,6 +6,7 @@ import java.util.function.Function; import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.TypeAdapter; @@ -50,6 +51,7 @@ import io.weaviate.client6.v1.internal.ObjectBuilder; import io.weaviate.client6.v1.internal.TaggedUnion; import io.weaviate.client6.v1.internal.json.JsonEnum; +import io.weaviate.client6.v1.internal.json.QuantizerJson; public interface VectorConfig extends TaggedUnion { public enum Kind implements JsonEnum { @@ -1815,8 +1817,10 @@ public void write(JsonWriter out, VectorConfig value) throws IOException { vectorIndex.getAsJsonObject().add("vectorizer", vectorizer); if (value.quantization() != null && !config.getAsJsonObject().get("quantization").isJsonNull()) { - vectorIndex.getAsJsonObject() - .get("vectorIndexConfig").getAsJsonObject() + QuantizerJson.host( + vectorIndex.getAsJsonObject().get("vectorIndexConfig").getAsJsonObject(), + vectorIndex.getAsJsonObject().get("vectorIndexType"), + true) .add(value.quantization()._kind().jsonValue(), config.getAsJsonObject().remove("quantization")); } @@ -1833,19 +1837,22 @@ public VectorConfig read(JsonReader in) throws IOException { vectorIndexConfig = jsonObject.get("vectorIndexConfig").getAsJsonObject(); } + // A dynamic index keeps its quantizer one level deeper, inside "hnsw"/"flat". + var quantizers = QuantizerJson.host(vectorIndexConfig, jsonObject.get("vectorIndexType"), false); + String quantizationKind = null; for (var kind : new String[] { Quantization.Kind.BQ.jsonValue(), Quantization.Kind.PQ.jsonValue(), Quantization.Kind.SQ.jsonValue(), Quantization.Kind.RQ.jsonValue() }) { - if (vectorIndexConfig.has(kind) - && vectorIndexConfig.get(kind).getAsJsonObject().get("enabled").getAsBoolean()) { + if (quantizers.has(kind) + && quantizers.get(kind).getAsJsonObject().get("enabled").getAsBoolean()) { quantizationKind = kind; } } - if (quantizationKind == null && vectorIndexConfig.has(Quantization.Kind.UNCOMPRESSED.jsonValue()) - && vectorIndexConfig.get(Quantization.Kind.UNCOMPRESSED.jsonValue()).getAsBoolean()) { + if (quantizationKind == null && quantizers.has(Quantization.Kind.UNCOMPRESSED.jsonValue()) + && quantizers.get(Quantization.Kind.UNCOMPRESSED.jsonValue()).getAsBoolean()) { quantizationKind = Quantization.Kind.UNCOMPRESSED.jsonValue(); } @@ -1880,9 +1887,9 @@ public VectorConfig read(JsonReader in) throws IOException { // Each individual vectorizer has a `Quantization quantization` field. // We need to specify the kind in order for // Quantization.CustomTypeAdapterFactory to be able to find the right adapter. - if (quantizationKind != null && vectorIndexConfig.has(quantizationKind)) { + if (quantizationKind != null && quantizers.has(quantizationKind)) { JsonObject quantization = new JsonObject(); - quantization.add(quantizationKind, vectorIndexConfig.get(quantizationKind)); + quantization.add(quantizationKind, quantizers.get(quantizationKind)); concreteVectorizer.add("quantization", quantization); } else { concreteVectorizer.add("quantization", null); diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/config/UpdateCollectionRequest.java b/src/main/java/io/weaviate/client6/v1/api/collections/config/UpdateCollectionRequest.java index ea79f8f7c..210f7b550 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/config/UpdateCollectionRequest.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/config/UpdateCollectionRequest.java @@ -16,6 +16,7 @@ import io.weaviate.client6.v1.api.collections.VectorConfig; import io.weaviate.client6.v1.internal.ObjectBuilder; import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.json.QuantizerJson; import io.weaviate.client6.v1.internal.rest.Endpoint; import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; @@ -42,9 +43,13 @@ public record UpdateCollectionRequest(CollectionConfig updated, CollectionConfig var vectorName = origVector.getKey(); var origQuantization = origVector.getValue().quantization(); if (vectors.has(vectorName) && origQuantization != null) { - vectors - .get(vectorName).getAsJsonObject() - .get("vectorIndexConfig").getAsJsonObject() + var vector = vectors.get(vectorName).getAsJsonObject(); + // A dynamic index keeps its quantizer settings inside "hnsw"/"flat", + // so the flag has to land at the same depth the server reads it from. + QuantizerJson.host( + vector.get("vectorIndexConfig").getAsJsonObject(), + vector.get("vectorIndexType"), + true) .addProperty(Quantization.Kind.UNCOMPRESSED.jsonValue(), origQuantization.isUncompressed()); } } 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/main/java/io/weaviate/client6/v1/api/collections/vectorindex/Dynamic.java b/src/main/java/io/weaviate/client6/v1/api/collections/vectorindex/Dynamic.java index 63d822674..823c47594 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/vectorindex/Dynamic.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/vectorindex/Dynamic.java @@ -20,7 +20,9 @@ public record Dynamic( @SerializedName("hnsw") Hnsw hnsw, @SerializedName("flat") Flat flat, - @SerializedName("threshold") Long threshold) + @SerializedName("threshold") Long threshold, + /** Distance metric, which a dynamic index carries at its own level. */ + @SerializedName("distance") Distance distance) implements VectorIndex { @Override @@ -45,7 +47,8 @@ public Dynamic(Builder builder) { this( builder.hnsw, builder.flat, - builder.threshold); + builder.threshold, + builder.distance); } public static class Builder implements ObjectBuilder { @@ -53,6 +56,7 @@ public static class Builder implements ObjectBuilder { private Hnsw hnsw; private Flat flat; private Long threshold; + private Distance distance; public Builder hnsw(Hnsw hnsw) { this.hnsw = hnsw; @@ -69,6 +73,11 @@ public Builder threshold(long threshold) { return this; } + public Builder distance(Distance distance) { + this.distance = distance; + return this; + } + @Override public Dynamic build() { return new Dynamic(this); @@ -99,6 +108,9 @@ public void write(JsonWriter out, Dynamic value) throws IOException { var dynamic = new JsonObject(); dynamic.addProperty("threshold", value.threshold); + if (value.distance != null) { + dynamic.add("distance", gson.toJsonTree(value.distance)); + } dynamic.add("hnsw", hnswAdapter.toJsonTree(value.hnsw)); dynamic.add("flat", flatAdapter.toJsonTree(value.flat)); @@ -111,8 +123,12 @@ public Dynamic read(JsonReader in) throws IOException { var hnsw = hnswAdapter.fromJsonTree(jsonObject.get("hnsw")); var flat = flatAdapter.fromJsonTree(jsonObject.get("flat")); - var threshold = jsonObject.get("threshold").getAsLong(); - return new Dynamic(hnsw, flat, threshold); + // Both are optional in the response: a dropped index has neither. + var threshold = jsonObject.has("threshold") && jsonObject.get("threshold").isJsonPrimitive() + ? jsonObject.get("threshold").getAsLong() + : null; + Distance distance = gson.fromJson(jsonObject.get("distance"), Distance.class); + return new Dynamic(hnsw, flat, threshold, distance); } }.nullSafe(); } diff --git a/src/main/java/io/weaviate/client6/v1/internal/json/QuantizerJson.java b/src/main/java/io/weaviate/client6/v1/internal/json/QuantizerJson.java new file mode 100644 index 000000000..6331b1daa --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/internal/json/QuantizerJson.java @@ -0,0 +1,76 @@ +package io.weaviate.client6.v1.internal.json; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.weaviate.client6.v1.api.collections.Quantization; +import io.weaviate.client6.v1.api.collections.VectorIndex; + +/** + * Locating the quantizer inside a {@code vectorIndexConfig} payload. + * + *

+ * Shared by the read/write sides of {@code VectorConfig} and by the collection + * update request, which all have to agree on where the quantizer keys live. + */ +public final class QuantizerJson { + /** Prevent public initialization. */ + private QuantizerJson() { + } + + /** + * The object inside {@code vectorIndexConfig} which holds the quantizer keys. + * + *

+ * For {@code hnsw} and {@code flat} that is {@code vectorIndexConfig} itself, + * but a {@code dynamic} index is + * {distance, threshold, hnsw: {...}, flat: {...}} and each + * sub-index carries its own quantizer, so the keys sit one level deeper. + * + *

+ * A vector config has a single quantization slot, so a dynamic index is read + * from {@code hnsw} when it has a quantizer and from {@code flat} otherwise, + * and written to {@code hnsw} -- which is also the only sub-index accepting + * every quantizer type, {@code flat} being limited to {@code bq}. Giving + * {@code hnsw} and {@code flat} different quantizers is not + * expressible through this client. + * + * @param create when true the nested object is created if the payload does not + * have one yet; when false an empty object is returned instead, + * so the caller simply finds nothing. + */ + public static JsonObject host(JsonObject vectorIndexConfig, JsonElement vectorIndexType, boolean create) { + if (!isDynamic(vectorIndexType)) { + return vectorIndexConfig; + } + + for (var subIndex : new String[] { "hnsw", "flat" }) { + var nested = vectorIndexConfig.get(subIndex); + if (nested != null && nested.isJsonObject() && (create || hasQuantizer(nested.getAsJsonObject()))) { + return nested.getAsJsonObject(); + } + } + + if (create) { + var hnsw = new JsonObject(); + vectorIndexConfig.add("hnsw", hnsw); + return hnsw; + } + return new JsonObject(); + } + + private static boolean isDynamic(JsonElement vectorIndexType) { + return vectorIndexType != null + && vectorIndexType.isJsonPrimitive() + && VectorIndex.Kind.DYNAMIC.jsonValue().equals(vectorIndexType.getAsString()); + } + + private static boolean hasQuantizer(JsonObject subIndex) { + for (var kind : Quantization.Kind.values()) { + if (subIndex.has(kind.jsonValue())) { + return true; + } + } + return false; + } +} diff --git a/src/test/java/io/weaviate/client6/v1/api/collections/DynamicQuantizationTest.java b/src/test/java/io/weaviate/client6/v1/api/collections/DynamicQuantizationTest.java new file mode 100644 index 000000000..b902e1019 --- /dev/null +++ b/src/test/java/io/weaviate/client6/v1/api/collections/DynamicQuantizationTest.java @@ -0,0 +1,112 @@ +package io.weaviate.client6.v1.api.collections; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import io.weaviate.client6.v1.api.collections.vectorindex.Distance; +import io.weaviate.client6.v1.api.collections.vectorindex.Dynamic; +import io.weaviate.client6.v1.internal.json.JSON; + +/** + * Reading a quantizer off a dynamic index. + * + *

+ * The round-trip cases live in {@code JSONTest}; these cover the shapes the + * server can return but this client never writes, since a single + * {@code quantization()} slot always writes to {@code hnsw}. + */ +public class DynamicQuantizationTest { + + private static String dynamic(String hnswExtra, String flatExtra) { + return """ + { + "vectorIndexType": "dynamic", + "vectorizer": {"none": {}}, + "vectorIndexConfig": { + "threshold": 10000, + "hnsw": {"ef": -1%s}, + "flat": {"vectorCacheMaxObjects": 1000000%s} + } + } + """.formatted(hnswExtra, flatExtra); + } + + /** The reported case: a dynamic index with RQ enabled on hnsw. */ + @Test + public void test_readsQuantizerFromHnsw() { + var config = JSON.deserialize( + dynamic(", \"rq\": {\"enabled\": true, \"bits\": 8, \"rescoreLimit\": 20}", ""), + VectorConfig.class); + + Assertions.assertThat(config.quantization()) + .isNotNull() + .returns(Quantization.Kind.RQ, Quantization::_kind); + Assertions.assertThat(config.quantization().asRQ().rescoreLimit()).isEqualTo(20); + Assertions.assertThat(config.quantization().asRQ().bits()).isEqualTo(8); + } + + /** flat carries its own quantizer, and only bq is valid there. */ + @Test + public void test_readsQuantizerFromFlatWhenHnswHasNone() { + var config = JSON.deserialize( + dynamic("", ", \"bq\": {\"enabled\": true, \"rescoreLimit\": 5}"), + VectorConfig.class); + + Assertions.assertThat(config.quantization()) + .isNotNull() + .returns(Quantization.Kind.BQ, Quantization::_kind); + Assertions.assertThat(config.quantization().asBQ().rescoreLimit()).isEqualTo(5); + } + + /** + * hnsw and flat can carry different quantizers, which one slot cannot + * represent. hnsw wins; see QuantizerJson. + */ + @Test + public void test_prefersHnswWhenBothCarryOne() { + var config = JSON.deserialize( + dynamic(", \"rq\": {\"enabled\": true, \"bits\": 8}", ", \"bq\": {\"enabled\": true}"), + VectorConfig.class); + + Assertions.assertThat(config.quantization()) + .returns(Quantization.Kind.RQ, Quantization::_kind); + } + + @Test + public void test_noQuantizerIsNull() { + var config = JSON.deserialize(dynamic("", ""), VectorConfig.class); + + Assertions.assertThat(config.quantization()).isNull(); + } + + /** A disabled quantizer reads as absent, same as for hnsw and flat indexes. */ + @Test + public void test_disabledQuantizerIsNull() { + var config = JSON.deserialize( + dynamic(", \"rq\": {\"enabled\": false, \"bits\": 8}", ""), + VectorConfig.class); + + Assertions.assertThat(config.quantization()).isNull(); + } + + /** The dynamic level carries its own distance, which used to be dropped. */ + @Test + public void test_readsDistanceAtDynamicLevel() { + var config = JSON.deserialize(""" + { + "vectorIndexType": "dynamic", + "vectorizer": {"none": {}}, + "vectorIndexConfig": { + "threshold": 10000, + "distance": "l2-squared", + "hnsw": {"ef": -1}, + "flat": {"vectorCacheMaxObjects": 1000000} + } + } + """, VectorConfig.class); + + Assertions.assertThat(config.vectorIndex().asDynamic()) + .returns(Distance.L2_SQUARED, Dynamic::distance) + .returns(10000L, Dynamic::threshold); + } +} 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..d4de18ba2 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 @@ -1188,6 +1188,34 @@ public static Object[][] testCases() { } """, }, + // A dynamic index nests its quantizer inside "hnsw"/"flat" -- the client + // used to put it beside them, where the server never looks, and could not + // read it back either. + { + VectorConfig.class, + SelfProvidedVectorizer.of(none -> none + .vectorIndex(Dynamic.of(idx -> idx + .hnsw(Hnsw.of(hnsw -> hnsw.ef(1))) + .flat(Flat.of(flat -> flat.vectorCacheMaxObjects(100))) + .threshold(5) + .distance(Distance.COSINE))) + .quantization(Quantization.rq(rq -> rq.rescoreLimit(20).bits(8)))), + """ + { + "vectorIndexType": "dynamic", + "vectorizer": {"none": {}}, + "vectorIndexConfig": { + "flat": {"vectorCacheMaxObjects": 100}, + "hnsw": { + "ef": 1, + "rq": {"enabled": true, "rescoreLimit": 20, "bits": 8} + }, + "threshold": 5, + "distance": "cosine" + } + } + """, + }, { VectorConfig.class, SelfProvidedVectorizer.of(none -> none @@ -1206,11 +1234,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 +1260,8 @@ public static Object[][] testCases() { "vectorIndexConfig": { "sq": { "enabled": true, - "rescore_limit": 10, - "training_limit": 1024, + "rescoreLimit": 10, + "trainingLimit": 1024, "cache": true } } @@ -1251,7 +1281,7 @@ public static Object[][] testCases() { "vectorIndexConfig": { "rq": { "enabled": true, - "rescore_limit": 10, + "rescoreLimit": 10, "bits": 8 } } @@ -1271,7 +1301,7 @@ public static Object[][] testCases() { "vectorIndexConfig": { "bq": { "enabled": true, - "rescore_limit": 10, + "rescoreLimit": 10, "cache": true } }