>From Ali Alsuliman <[email protected]>:
Ali Alsuliman has uploaded this change for review. (
https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21620?usp=email )
Change subject: [ASTERIXDB-3817][COMP][MTD] Enum for quantization
......................................................................
[ASTERIXDB-3817][COMP][MTD] Enum for quantization
- user model changes: no
- storage format changes: no
- interface changes: no
Details:
The vector index `quantization` parameter was a String whose only real
consumer converted it to a bit width:
return QUANTIZATION_SQ4.equals(label) ? 4 : DEFAULT_QUANTIZATION_BITS_SQ8;
That is a per-constant attribute written as a two-branch lookup with a
silent fallback: "SQ16", "sq4", "" or a typo all yield 8. Nothing caught
it either, because once the label is guaranteed non-null there is no
validation left on the read path -- readFields accepted any string, so a
corrupt label would decode the stored embeddings at the wrong bit width
while the sibling `similarity` parameter, typed since the previous
change, rejects an unrecognized value outright.
Introduce VectorQuantization, a sibling of VectorSimilarityMetric in the
same package, carrying the bit width on the constant. VectorIndexParameters
holds it instead of a String, so the bit width comes straight off the
scheme and an unrecognized persisted label is reported rather than
defaulted. Folded into the enum and removed from VectorIndexParameters:
QUANTIZATION_SQ4, QUANTIZATION_SQ8, QUANTIZATION_LABELS,
DEFAULT_QUANTIZATION_BITS_SQ8, quantizationBits(String),
isAllowedQuantization(String) and quantizationList().
The label is unchanged on disk -- label() is name(), which is what was
persisted before -- so existing indexes read back identically. The enum
documents that this makes the constant names a durable contract: add a
constant rather than renaming one, and give label() its own string if a
name ever has to change.
isQuantized() is deliberately left alone. It stays the single predicate the
three tuple-layout decisions key off; when a non-quantized mode becomes
reachable from DDL it becomes `quantization != NONE` rather than the
constant it is today.
Tests:
- VectorIndexParametersTupleTranslatorTest asserts the scheme round-trips
and that its bit width survives, and continues to check that an absent
`quantization` reads back as the default.
- The vector/create-index-vtree-metadata fixture still expects
"quantization": "SQ4" in the persisted record, which is what confirms
the on-disk spelling did not move.
Ext-ref: MB-73194
Co-Authored-By: Claude Opus 5 <[email protected]>
Change-Id: Ie6987f447505231c0614de2efc9af0981b86cf89
---
A
asterixdb/asterix-common/src/main/java/org/apache/asterix/common/vector/VectorQuantization.java
M
asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/VectorIndexDeclUtil.java
M
asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/SecondaryVectorOperationsHelper.java
M
asterixdb/asterix-metadata/src/test/java/org/apache/asterix/metadata/entitytupletranslators/VectorIndexParametersTupleTranslatorTest.java
M
asterixdb/asterix-om/src/main/java/org/apache/asterix/om/vector/VectorIndexParameters.java
5 files changed, 123 insertions(+), 57 deletions(-)
git pull ssh://asterix-gerrit.ics.uci.edu:29418/asterixdb
refs/changes/20/21620/1
diff --git
a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/vector/VectorQuantization.java
b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/vector/VectorQuantization.java
new file mode 100644
index 0000000..4f6dea8
--- /dev/null
+++
b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/vector/VectorQuantization.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.asterix.common.vector;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import org.apache.hyracks.util.annotations.AiProvenance;
+
+/**
+ * The quantization schemes a VTree index can encode its embeddings with,
together with the bit width each
+ * one quantizes to. Sibling of {@link VectorSimilarityMetric}: the single
source of truth for the values the
+ * index {@code quantization} option accepts.
+ * <p>
+ * The bit width is what actually reaches the storage layer — as {@code
OptimizedScalarQuantizationCodec.Params}
+ * and as a field on the local resource — so it belongs on the constant rather
than in a lookup that has to
+ * decide what an unrecognized label means.
+ * <p>
+ * {@link #label()} is the spelling written in DDL and <em>persisted</em> in
the {@code Metadata.Index} record.
+ * It is {@link #name()} today, which means renaming a constant strands
existing indexes: add a constant rather
+ * than renaming one, and if a name ever has to change, give {@code label()}
its own string instead.
+ */
+@AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_5, tool =
AiProvenance.Tool.CLAUDE_CODE_UI, contributionKind =
AiProvenance.ContributionKind.GENERATED)
+public enum VectorQuantization {
+ SQ4(4),
+ SQ8(8);
+
+ private final int bits;
+
+ VectorQuantization(int bits) {
+ this.bits = bits;
+ }
+
+ /** Bits per component this scheme quantizes an embedding to. */
+ public int bits() {
+ return bits;
+ }
+
+ /** The DDL spelling, and the form persisted in index metadata. */
+ public String label() {
+ return name();
+ }
+
+ /**
+ * Resolves a {@code quantization} label, trimmed and case-insensitively.
+ *
+ * @return the matching scheme, or {@code null} if the label is not
recognized.
+ */
+ public static VectorQuantization fromLabel(String label) {
+ if (label == null) {
+ return null;
+ }
+ String normalized = label.trim().toUpperCase(Locale.ROOT);
+ for (VectorQuantization quantization : values()) {
+ if (quantization.label().equals(normalized)) {
+ return quantization;
+ }
+ }
+ return null;
+ }
+
+ /** Comma-separated labels, for "allowed values are ..." diagnostics. */
+ public static String labelList() {
+ return
Arrays.stream(values()).map(VectorQuantization::label).collect(Collectors.joining(",
"));
+ }
+}
diff --git
a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/VectorIndexDeclUtil.java
b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/VectorIndexDeclUtil.java
index cb2fcad..670e726 100644
---
a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/VectorIndexDeclUtil.java
+++
b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/VectorIndexDeclUtil.java
@@ -37,6 +37,7 @@
import org.apache.asterix.common.exceptions.AsterixException;
import org.apache.asterix.common.exceptions.CompilationException;
import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.vector.VectorQuantization;
import org.apache.asterix.common.vector.VectorSimilarityMetric;
import org.apache.asterix.lang.common.expression.RecordConstructor;
import org.apache.asterix.object.base.AdmBigIntNode;
@@ -158,23 +159,22 @@
return metric;
}
- private static String validateQuantization(AdmObjectNode node) throws
CompilationException {
+ /**
+ * Validates {@code quantization} and returns the resolved scheme.
Optional: an absent value takes
+ * {@link VectorIndexParameters#DEFAULT_QUANTIZATION}, since every vector
index is quantized.
+ */
+ private static VectorQuantization validateQuantization(AdmObjectNode node)
throws CompilationException {
IAdmNode qNode = node.get(QUANTIZATION);
if (qNode == null) {
return DEFAULT_QUANTIZATION;
}
- if (qNode.getType() != ATypeTag.STRING) {
+ VectorQuantization quantization =
+ qNode.getType() == ATypeTag.STRING ?
VectorQuantization.fromLabel(((AdmStringNode) qNode).get()) : null;
+ if (quantization == null) {
throw new
CompilationException(ErrorCode.COMPILATION_VECTOR_INDEX_CREATION_FAILED,
- "Invalid `quantization` parameter value. Allowed values: "
- + VectorIndexParameters.quantizationList());
+ "Invalid `quantization` parameter value. Allowed values: "
+ VectorQuantization.labelList());
}
- String normalized = ((AdmStringNode)
qNode).get().trim().toUpperCase(Locale.ROOT);
- if (!VectorIndexParameters.isAllowedQuantization(normalized)) {
- throw new
CompilationException(ErrorCode.COMPILATION_VECTOR_INDEX_CREATION_FAILED,
- "Invalid `quantization` parameter value. Allowed values: "
- + VectorIndexParameters.quantizationList());
- }
- return normalized;
+ return quantization;
}
/**
diff --git
a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/SecondaryVectorOperationsHelper.java
b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/SecondaryVectorOperationsHelper.java
index 9419742..67c9d4a 100644
---
a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/SecondaryVectorOperationsHelper.java
+++
b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/SecondaryVectorOperationsHelper.java
@@ -549,9 +549,8 @@
"Run ANALYZE on the dataset before creating a vector
index.");
}
- // 2. Extract quantization parameters (default label SQ8 matches DDL;
bits 8 for SQ8)
- String qLabel = vectorParameters.getQuantization();
- int bits = VectorIndexParameters.quantizationBits(qLabel);
+ // 2. Extract quantization parameters (the scheme defaults to SQ8 at
DDL time, so bits is always set)
+ int bits = vectorParameters.getQuantization().bits();
// confidence_interval is not a declared WITH parameter (see
VectorIndexParameters), so it was never
// readable from the WITH clause; use the constant directly until it
becomes a real DDL knob.
float confidenceInterval = DEFAULT_CONFIDENCE_INTERVAL;
diff --git
a/asterixdb/asterix-metadata/src/test/java/org/apache/asterix/metadata/entitytupletranslators/VectorIndexParametersTupleTranslatorTest.java
b/asterixdb/asterix-metadata/src/test/java/org/apache/asterix/metadata/entitytupletranslators/VectorIndexParametersTupleTranslatorTest.java
index 89bd755..2432ee1 100644
---
a/asterixdb/asterix-metadata/src/test/java/org/apache/asterix/metadata/entitytupletranslators/VectorIndexParametersTupleTranslatorTest.java
+++
b/asterixdb/asterix-metadata/src/test/java/org/apache/asterix/metadata/entitytupletranslators/VectorIndexParametersTupleTranslatorTest.java
@@ -37,6 +37,7 @@
import org.apache.asterix.common.config.DatasetConfig.IndexType;
import org.apache.asterix.common.metadata.DataverseName;
import org.apache.asterix.common.metadata.MetadataUtil;
+import org.apache.asterix.common.vector.VectorQuantization;
import org.apache.asterix.common.vector.VectorSimilarityMetric;
import org.apache.asterix.metadata.MetadataNode;
import org.apache.asterix.metadata.bootstrap.IndexEntity;
@@ -78,15 +79,16 @@
public void everyParameterRoundTrips() throws AlgebricksException,
IOException {
VectorIndexParameters written =
VectorIndexParameters.builder().setDimension(128).setSimilarity(VectorSimilarityMetric.EUCLIDEAN)
-
.setQuantization(VectorIndexParameters.QUANTIZATION_SQ4).setTrainListFraction(0.375)
-
.setEpsilon(0.625).setNumClusters(7).setCrossPollinationM(3).setRngFactor(1.5).build();
+
.setQuantization(VectorQuantization.SQ4).setTrainListFraction(0.375).setEpsilon(0.625)
+
.setNumClusters(7).setCrossPollinationM(3).setRngFactor(1.5).build();
VectorIndexParameters readBack = roundTrip(written);
Assert.assertEquals(written, readBack);
Assert.assertEquals(128, readBack.getDimension());
Assert.assertEquals(VectorSimilarityMetric.EUCLIDEAN,
readBack.getSimilarity());
- Assert.assertEquals(VectorIndexParameters.QUANTIZATION_SQ4,
readBack.getQuantization());
+ Assert.assertEquals(VectorQuantization.SQ4,
readBack.getQuantization());
+ Assert.assertEquals(4, readBack.getQuantization().bits());
Assert.assertTrue(readBack.isQuantized());
Assert.assertEquals(0.375, readBack.getTrainListFraction(), 0.0);
Assert.assertEquals(0.625, readBack.getEpsilon(), 0.0);
diff --git
a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/vector/VectorIndexParameters.java
b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/vector/VectorIndexParameters.java
index 8140dc4..90dc817 100644
---
a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/vector/VectorIndexParameters.java
+++
b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/vector/VectorIndexParameters.java
@@ -26,6 +26,7 @@
import org.apache.asterix.builders.IARecordBuilder;
import org.apache.asterix.common.exceptions.AsterixException;
import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.vector.VectorQuantization;
import org.apache.asterix.common.vector.VectorSimilarityMetric;
import org.apache.asterix.formats.nontagged.SerializerDeserializerProvider;
import org.apache.asterix.om.base.ADouble;
@@ -79,18 +80,7 @@
public static final String CROSS_POLLINATION_M = "cross_pollination_m";
public static final String RNG_FACTOR = "rng_factor";
- public static final String QUANTIZATION_SQ4 = "SQ4";
- public static final String QUANTIZATION_SQ8 = "SQ8";
- /**
- * Every accepted {@code quantization} label. Both the DDL validator and
the build path check against this
- * one declaration, so they cannot drift apart on which labels exist. A
{@code List} rather than a
- * {@code Set} because {@link #quantizationList()} renders it into a
diagnostic: {@code Set.of} iteration
- * order is randomized per JVM run, which would make that message's
wording vary between runs.
- */
- private static final List<String> QUANTIZATION_LABELS =
List.of(QUANTIZATION_SQ4, QUANTIZATION_SQ8);
- public static final String DEFAULT_QUANTIZATION = QUANTIZATION_SQ8;
- /** Bit width of {@link #DEFAULT_QUANTIZATION} ({@link
#QUANTIZATION_SQ8}); {@link #QUANTIZATION_SQ4} is 4. */
- public static final int DEFAULT_QUANTIZATION_BITS_SQ8 = 8;
+ public static final VectorQuantization DEFAULT_QUANTIZATION =
VectorQuantization.SQ8;
public static final double DEFAULT_TRAIN_LIST_FRACTION = 0.1;
public static final double DEFAULT_EPSILON = 0.25;
public static final int DEFAULT_CROSS_POLLINATION_M = 1;
@@ -107,7 +97,7 @@
private final int dimension;
private final VectorSimilarityMetric similarity;
- private final String quantization;
+ private final VectorQuantization quantization;
private final double trainListFraction;
private final double epsilon;
/** {@code null} when unset: the builder then derives it from the dataset
cardinality at build time. */
@@ -143,12 +133,12 @@
}
/**
- * The quantization label, never {@code null}: it is optional in the
{@code WITH} clause but defaults to
+ * The quantization scheme, never {@code null}: it is optional in the
{@code WITH} clause but defaults to
* {@link #DEFAULT_QUANTIZATION}, and an index record that predates the
parameter reads back as that
- * default too. {@code
SecondaryVectorOperationsHelper#buildCreationJobSpec} relies on this — it
derives
- * the quantization bit width from the label unconditionally, outside any
{@link #isQuantized()} guard.
+ * default too. {@code
SecondaryVectorOperationsHelper#buildCreationJobSpec} relies on this — it takes
the
+ * scheme's bit width unconditionally, outside any {@link #isQuantized()}
guard.
*/
- public String getQuantization() {
+ public VectorQuantization getQuantization() {
return quantization;
}
@@ -203,20 +193,6 @@
return String.join(", ", NAMES);
}
- public static boolean isAllowedQuantization(String label) {
- return QUANTIZATION_LABELS.contains(label);
- }
-
- /** Comma-separated {@code quantization} labels, for "allowed values are
..." diagnostics. */
- public static String quantizationList() {
- return String.join(", ", QUANTIZATION_LABELS);
- }
-
- /** Bit width the given (already normalized) quantization label encodes
at. */
- public static int quantizationBits(String label) {
- return QUANTIZATION_SQ4.equals(label) ? 4 :
DEFAULT_QUANTIZATION_BITS_SQ8;
- }
-
/**
* Serializes this configuration into {@code recordBuilder} as one open
field per present parameter, in
* {@link #NAMES} order. Only {@code num_clusters} is genuinely optional,
and it is written only when set,
@@ -226,7 +202,7 @@
FieldWriter writer = new FieldWriter(recordBuilder);
writer.writeInt(DIMENSION, dimension);
writer.writeString(SIMILARITY, similarity.canonical());
- writer.writeString(QUANTIZATION, quantization);
+ writer.writeString(QUANTIZATION, quantization.label());
writer.writeDouble(TRAIN_LIST_FRACTION, trainListFraction);
writer.writeDouble(EPSILON, epsilon);
if (numClusters != null) {
@@ -263,9 +239,16 @@
builder.setSimilarity(metric);
}
// An index record with no `quantization` field predates the
parameter; it reads back as the default,
- // which is the label its creation job would have quantized with
anyway.
- String quantization = reader.readString(QUANTIZATION);
- if (quantization != null) {
+ // which is the scheme its creation job would have quantized with
anyway. A field that is present but
+ // unrecognized is corrupt metadata, not a defaultable case: silently
falling back to SQ8 would decode
+ // the stored embeddings at the wrong bit width.
+ String quantizationStr = reader.readString(QUANTIZATION);
+ if (quantizationStr != null) {
+ VectorQuantization quantization =
VectorQuantization.fromLabel(quantizationStr);
+ if (quantization == null) {
+ throw new
AsterixException(ErrorCode.COMPILATION_VECTOR_INDEX_CREATION_FAILED,
+ "Unrecognized `" + QUANTIZATION + "` value `" +
quantizationStr + "` in index metadata");
+ }
builder.setQuantization(quantization);
}
Double trainListFraction = reader.readDouble(TRAIN_LIST_FRACTION);
@@ -296,10 +279,9 @@
if (this == o) {
return true;
}
- if (!(o instanceof VectorIndexParameters)) {
+ if (!(o instanceof VectorIndexParameters other)) {
return false;
}
- VectorIndexParameters other = (VectorIndexParameters) o;
return dimension == other.dimension && Objects.equals(similarity,
other.similarity)
&& Objects.equals(quantization, other.quantization)
&& Double.compare(trainListFraction, other.trainListFraction)
== 0
@@ -318,7 +300,7 @@
StringBuilder sb = new StringBuilder("{ ");
sb.append(DIMENSION).append(": ").append(dimension);
sb.append(", ").append(SIMILARITY).append(":
").append(similarity.canonical());
- sb.append(", ").append(QUANTIZATION).append(": ").append(quantization);
+ sb.append(", ").append(QUANTIZATION).append(":
").append(quantization.label());
sb.append(", ").append(TRAIN_LIST_FRACTION).append(":
").append(trainListFraction);
sb.append(", ").append(EPSILON).append(": ").append(epsilon);
if (numClusters != null) {
@@ -338,7 +320,7 @@
private int dimension = -1;
private VectorSimilarityMetric similarity;
- private String quantization = DEFAULT_QUANTIZATION;
+ private VectorQuantization quantization = DEFAULT_QUANTIZATION;
private double trainListFraction = DEFAULT_TRAIN_LIST_FRACTION;
private double epsilon = DEFAULT_EPSILON;
private Integer numClusters;
@@ -358,7 +340,7 @@
return this;
}
- public Builder setQuantization(String quantization) {
+ public Builder setQuantization(VectorQuantization quantization) {
this.quantization = Objects.requireNonNull(quantization,
QUANTIZATION);
return this;
}
--
To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21620?usp=email
To unsubscribe, or for help writing mail filters, visit
https://asterix-gerrit.ics.uci.edu/settings?usp=email
Gerrit-MessageType: newchange
Gerrit-Project: asterixdb
Gerrit-Branch: master
Gerrit-Change-Id: Ie6987f447505231c0614de2efc9af0981b86cf89
Gerrit-Change-Number: 21620
Gerrit-PatchSet: 1
Gerrit-Owner: Ali Alsuliman <[email protected]>