-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix #1775: use optimized CqlVector<Float> codec to improve performance #1801
Open
tatu-at-datastax
wants to merge
13
commits into
main
Choose a base branch
from
tatu/1775-driver-perf-opt-float-vector
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
900a4ac
Fix #1775: use optimized CqlVector<Float> codec to improve performance
tatu-at-datastax e4cee7e
minor clean up
tatu-at-datastax b47953f
Register optimized codec (likely leads to codec errors now on returni…
tatu-at-datastax b1ab819
Fix binding problems wrt new codec
tatu-at-datastax 5eddc9b
Merge branch 'main' into tatu/1775-driver-perf-opt-float-vector
tatu-at-datastax 4124404
Test code without codec (temporarily)
tatu-at-datastax 24c02c4
Start converting float[] codec for JSON-to-CQL cases
tatu-at-datastax 8d44f54
Fix unit tests
tatu-at-datastax c4a8132
Convert one more place to more efficient vector encoding
tatu-at-datastax 3b7848d
undo change; QueryBuilder must use CqlVector<Float>, not float[]
tatu-at-datastax 667661f
Merge branch 'main' into tatu/1775-driver-perf-opt-float-vector
tatu-at-datastax a9f4254
Merge branch 'main' into tatu/1775-driver-perf-opt-float-vector
tatu-at-datastax f216eae
Merge branch 'main' into tatu/1775-driver-perf-opt-float-vector
tatu-at-datastax File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
180 changes: 180 additions & 0 deletions
180
...sgv2/jsonapi/service/cqldriver/executor/optvector/SubtypeOnlyFloatVectorToArrayCodec.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package io.stargate.sgv2.jsonapi.service.cqldriver.executor.optvector; | ||
|
||
import com.datastax.oss.driver.api.core.ProtocolVersion; | ||
import com.datastax.oss.driver.api.core.type.DataType; | ||
import com.datastax.oss.driver.api.core.type.DataTypes; | ||
import com.datastax.oss.driver.api.core.type.VectorType; | ||
import com.datastax.oss.driver.api.core.type.codec.TypeCodec; | ||
import com.datastax.oss.driver.api.core.type.reflect.GenericType; | ||
import com.datastax.oss.driver.internal.core.type.codec.FloatCodec; | ||
import com.datastax.oss.driver.shaded.guava.common.base.Splitter; | ||
import com.datastax.oss.driver.shaded.guava.common.collect.Iterators; | ||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import edu.umd.cs.findbugs.annotations.Nullable; | ||
import java.nio.ByteBuffer; | ||
import java.util.Arrays; | ||
import java.util.Iterator; | ||
import java.util.Objects; | ||
|
||
/** | ||
* Implementation of {@link TypeCodec} which translates CQL vectors into float arrays. Difference | ||
* between this and {@link | ||
* com.datastax.oss.driver.internal.core.type.codec.extras.vector.FloatVectorToArrayCodec} is that | ||
* we don't concern ourselves with the dimensionality specified in the input CQL type. This codec | ||
* just reads all the bytes, tries to deserislize them consecutively into subtypes and then returns | ||
* the result. Serialiation is similar: we take the input array, serialize each element and return | ||
* the result. | ||
*/ | ||
public class SubtypeOnlyFloatVectorToArrayCodec implements TypeCodec<float[]> { | ||
|
||
private static final int ELEMENT_SIZE = 4; | ||
|
||
@NonNull protected final VectorType cqlType; | ||
@NonNull protected final GenericType<float[]> javaType; | ||
|
||
private final FloatCodec floatCodec = new FloatCodec(); | ||
|
||
private static final SubtypeOnlyFloatVectorToArrayCodec INSTANCE = | ||
new SubtypeOnlyFloatVectorToArrayCodec(DataTypes.FLOAT); | ||
|
||
private SubtypeOnlyFloatVectorToArrayCodec(@NonNull DataType subType) { | ||
cqlType = new SubtypeOnlyVectorType(Objects.requireNonNull(subType, "subType cannot be null")); | ||
javaType = GenericType.of(float[].class); | ||
} | ||
|
||
public static TypeCodec<float[]> instance() { | ||
return INSTANCE; | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public GenericType<float[]> getJavaType() { | ||
return javaType; | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public DataType getCqlType() { | ||
return cqlType; | ||
} | ||
|
||
@Override | ||
public boolean accepts(@NonNull Class<?> javaClass) { | ||
return float[].class.equals(javaClass); | ||
} | ||
|
||
@Override | ||
public boolean accepts(@NonNull Object value) { | ||
return value instanceof float[]; | ||
} | ||
|
||
@Override | ||
public boolean accepts(@NonNull DataType value) { | ||
if (!(value instanceof VectorType)) { | ||
return false; | ||
} | ||
VectorType valueVectorType = (VectorType) value; | ||
return this.cqlType.getElementType().equals(valueVectorType.getElementType()); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public ByteBuffer encode(@Nullable float[] array, @NonNull ProtocolVersion protocolVersion) { | ||
if (array == null) { | ||
return null; | ||
} | ||
int length = array.length; | ||
int totalSize = length * ELEMENT_SIZE; | ||
ByteBuffer output = ByteBuffer.allocate(totalSize); | ||
for (int i = 0; i < length; i++) { | ||
serializeElement(output, array, i, protocolVersion); | ||
} | ||
output.flip(); | ||
return output; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public float[] decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { | ||
if (bytes == null || bytes.remaining() == 0) { | ||
throw new IllegalArgumentException( | ||
"Input ByteBuffer must not be null and must have non-zero remaining bytes"); | ||
} | ||
// TODO: Do we want to treat this as an error? We could also just ignore any extraneous bytes | ||
// if they appear. | ||
if (bytes.remaining() % ELEMENT_SIZE != 0) { | ||
throw new IllegalArgumentException( | ||
String.format("Input ByteBuffer should have a multiple of %d bytes", ELEMENT_SIZE)); | ||
} | ||
ByteBuffer input = bytes.duplicate(); | ||
int elementCount = input.remaining() / 4; | ||
float[] array = new float[elementCount]; | ||
for (int i = 0; i < elementCount; i++) { | ||
deserializeElement(input, array, i, protocolVersion); | ||
} | ||
return array; | ||
} | ||
|
||
/** | ||
* Write the {@code index}th element of {@code array} to {@code output}. | ||
* | ||
* @param output The ByteBuffer to write to. | ||
* @param array The array to read from. | ||
* @param index The element index. | ||
* @param protocolVersion The protocol version to use. | ||
*/ | ||
protected void serializeElement( | ||
@NonNull ByteBuffer output, | ||
@NonNull float[] array, | ||
int index, | ||
@NonNull ProtocolVersion protocolVersion) { | ||
output.putFloat(array[index]); | ||
} | ||
|
||
/** | ||
* Read the {@code index}th element of {@code array} from {@code input}. | ||
* | ||
* @param input The ByteBuffer to read from. | ||
* @param array The array to write to. | ||
* @param index The element index. | ||
* @param protocolVersion The protocol version to use. | ||
*/ | ||
protected void deserializeElement( | ||
@NonNull ByteBuffer input, | ||
@NonNull float[] array, | ||
int index, | ||
@NonNull ProtocolVersion protocolVersion) { | ||
array[index] = input.getFloat(); | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public String format(@Nullable float[] value) { | ||
return value == null ? "NULL" : Arrays.toString(value); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public float[] parse(@Nullable String str) { | ||
/* TODO: Logic below requires a double traversal through the input String but there's no other obvious way to | ||
* get the size. It's still probably worth the initial pass through in order to avoid having to deal with | ||
* resizing ops. Fortunately we're only dealing with the format/parse pair here so this shouldn't impact | ||
* general performance much. */ | ||
if ((str == null) || str.isEmpty()) { | ||
throw new IllegalArgumentException("Cannot create float array from null or empty string"); | ||
} | ||
Iterable<String> strIterable = | ||
Splitter.on(", ").trimResults().split(str.substring(1, str.length() - 1)); | ||
float[] rv = new float[Iterators.size(strIterable.iterator())]; | ||
Iterator<String> strIterator = strIterable.iterator(); | ||
for (int i = 0; i < rv.length; ++i) { | ||
String strVal = strIterator.next(); | ||
// TODO: String.isBlank() should be included here but it's only available with Java11+ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we are using Java 21, why don't we use String.isBlank() here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code was first written for Java CQL driver (by Brett). I can change that, thanks. |
||
if (strVal == null || strVal.isEmpty()) { | ||
throw new IllegalArgumentException("Null element observed in float array string"); | ||
} | ||
rv[i] = floatCodec.parse(strVal).floatValue(); | ||
} | ||
return rv; | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
.../io/stargate/sgv2/jsonapi/service/cqldriver/executor/optvector/SubtypeOnlyVectorType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package io.stargate.sgv2.jsonapi.service.cqldriver.executor.optvector; | ||
|
||
import com.datastax.oss.driver.api.core.detach.AttachmentPoint; | ||
import com.datastax.oss.driver.api.core.type.DataType; | ||
import com.datastax.oss.driver.api.core.type.VectorType; | ||
import com.datastax.oss.driver.internal.core.type.DefaultVectorType; | ||
import java.util.Objects; | ||
|
||
/** | ||
* An implementation of {@link VectorType} which is only concerned with the subtype of the vector. | ||
* Useful if you want to describe a call of vector types that do not differ by subtype but do differ | ||
* by dimension. | ||
*/ | ||
public class SubtypeOnlyVectorType extends DefaultVectorType { | ||
private static final int NO_DIMENSION = -1; | ||
|
||
public SubtypeOnlyVectorType(DataType subtype) { | ||
super(subtype, NO_DIMENSION); | ||
} | ||
|
||
@Override | ||
public int getDimensions() { | ||
throw new UnsupportedOperationException("Subtype-only vectors do not support dimensions"); | ||
} | ||
|
||
/* ============== General class implementation ============== */ | ||
@Override | ||
public boolean equals(Object o) { | ||
if (o == this) { | ||
return true; | ||
} | ||
return (o instanceof VectorType that) && that.getElementType().equals(getElementType()); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return super.hashCode() ^ Objects.hashCode(getElementType()); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return String.format("(Subtype-only) Vector(%s)", getElementType()); | ||
} | ||
|
||
@Override | ||
public boolean isDetached() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public void attach(AttachmentPoint attachmentPoint) { | ||
// nothing to do | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how common this exception is. If this is a common error, should we use Error V2 instead of
IllegalArgumentException
? Have the same question for otherIllegalArgumentException
s. Or all theseIllegalArgumentException
s will be caught eventually and converted to our own errors? I see there is caught inVectorCodec
and convert it toToCQLCodecException
, but not sure if it covers all the cases.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This codec is used by Java CQL Driver, it's not a Data API codec. But I think code we use for java driver will have to catch exceptions anyway. Problem itself should not be possible to occur by any external calls; it is more an assertion (and as such not sure there is a way to easily test).