Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/it/java/io/weaviate/containers/Weaviate.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
85 changes: 85 additions & 0 deletions src/it/java/io/weaviate/integration/CollectionsITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
Expand Down Expand Up @@ -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.
*
* <p>
* 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".
*
* <p>
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<VectorConfig.Kind, Object> {
public enum Kind implements JsonEnum<Kind> {
Expand Down Expand Up @@ -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"));
}

Expand All @@ -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();
}

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,14 +47,16 @@ public Dynamic(Builder builder) {
this(
builder.hnsw,
builder.flat,
builder.threshold);
builder.threshold,
builder.distance);
}

public static class Builder implements ObjectBuilder<Dynamic> {

private Hnsw hnsw;
private Flat flat;
private Long threshold;
private Distance distance;

public Builder hnsw(Hnsw hnsw) {
this.hnsw = hnsw;
Expand All @@ -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);
Expand Down Expand Up @@ -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));

Expand All @@ -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();
}
Expand Down
Loading
Loading