- Notifications
You must be signed in to change notification settings - Fork 25.6k
Feature/speed up binary vector decoding #96716
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
Changes from 7 commits
514f832 71a33b2 317ab86 c1c3239 58afec8 580709c b1fa250 31ae51e 7a270d0 497aef3 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| pr: 96716 | ||
| summary: Feature/speed up binary vector decoding | ||
| area: Search | ||
| type: enhancement | ||
| issues: [] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -51,6 +51,7 @@ | |
| | ||
| import java.io.IOException; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.ByteOrder; | ||
| import java.time.ZoneId; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| | @@ -64,6 +65,8 @@ | |
| * A {@link FieldMapper} for indexing a dense vector of floats. | ||
| */ | ||
| public class DenseVectorFieldMapper extends FieldMapper { | ||
| public static final Version MAGNITUDE_STORED_INDEX_VERSION = Version.V_7_5_0; | ||
| public static final Version LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION = Version.V_8_9_0; | ||
| | ||
| public static final String CONTENT_TYPE = "dense_vector"; | ||
| public static short MAX_DIMS_COUNT = 2048; // maximum allowed number of dimensions | ||
| | @@ -353,6 +356,11 @@ public Field parseKnnVector(DocumentParserContext context, DenseVectorFieldMappe | |
| fieldMapper.checkDimensionMatches(index, context); | ||
| return dotProduct; | ||
| } | ||
| | ||
| @Override | ||
| ByteBuffer createByteBuffer(Version indexVersion, int numBytes) { | ||
| return ByteBuffer.wrap(new byte[numBytes]); | ||
| } | ||
| }, | ||
| | ||
| FLOAT(4) { | ||
| | @@ -460,6 +468,13 @@ public Field parseKnnVector(DocumentParserContext context, DenseVectorFieldMappe | |
| checkVectorBounds(vector); | ||
| return dotProduct; | ||
| } | ||
| | ||
| @Override | ||
| ByteBuffer createByteBuffer(Version indexVersion, int numBytes) { | ||
| return indexVersion.onOrAfter(LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION) | ||
| ? ByteBuffer.wrap(new byte[numBytes]).order(ByteOrder.LITTLE_ENDIAN) | ||
| : ByteBuffer.wrap(new byte[numBytes]); | ||
| } | ||
| Contributor 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. 👍 | ||
| }; | ||
| | ||
| final int elementBytes; | ||
| | @@ -483,6 +498,8 @@ public Field parseKnnVector(DocumentParserContext context, DenseVectorFieldMappe | |
| abstract double parseKnnVectorToByteBuffer(DocumentParserContext context, DenseVectorFieldMapper fieldMapper, ByteBuffer byteBuffer) | ||
| throws IOException; | ||
| | ||
| abstract ByteBuffer createByteBuffer(Version indexVersion, int numBytes); | ||
| | ||
| public abstract void checkVectorBounds(float[] vector); | ||
| | ||
| abstract void checkVectorMagnitude( | ||
| | @@ -890,18 +907,18 @@ private Field parseKnnVector(DocumentParserContext context) throws IOException { | |
| private Field parseBinaryDocValuesVector(DocumentParserContext context) throws IOException { | ||
| // encode array of floats as array of integers and store into buf | ||
| // this code is here and not int the VectorEncoderDecoder so not to create extra arrays | ||
| ||
| byte[] bytes = indexCreatedVersion.onOrAfter(Version.V_7_5_0) | ||
| ? new byte[dims * elementType.elementBytes + MAGNITUDE_BYTES] | ||
| : new byte[dims * elementType.elementBytes]; | ||
| int numBytes = indexCreatedVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION) | ||
| ? dims * elementType.elementBytes + MAGNITUDE_BYTES | ||
| : dims * elementType.elementBytes; | ||
| | ||
| ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); | ||
| ByteBuffer byteBuffer = elementType.createByteBuffer(indexCreatedVersion, numBytes); | ||
| double dotProduct = elementType.parseKnnVectorToByteBuffer(context, this, byteBuffer); | ||
| if (indexCreatedVersion.onOrAfter(Version.V_7_5_0)) { | ||
| if (indexCreatedVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION)) { | ||
| // encode vector magnitude at the end | ||
| float vectorMagnitude = (float) Math.sqrt(dotProduct); | ||
| byteBuffer.putFloat(vectorMagnitude); | ||
| } | ||
| return new BinaryDocValuesField(fieldType().name(), new BytesRef(bytes)); | ||
| return new BinaryDocValuesField(fieldType().name(), new BytesRef(byteBuffer.array())); | ||
| } | ||
| | ||
| private void checkDimensionExceeded(int index, DocumentParserContext context) { | ||
| | @@ -1000,7 +1017,7 @@ public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { | |
| if (indexed) { | ||
| return new IndexedSyntheticFieldLoader(); | ||
| } | ||
| return new DocValuesSyntheticFieldLoader(); | ||
| return new DocValuesSyntheticFieldLoader(indexCreatedVersion); | ||
| } | ||
| | ||
| private class IndexedSyntheticFieldLoader implements SourceLoader.SyntheticFieldLoader { | ||
| | @@ -1060,6 +1077,11 @@ public void write(XContentBuilder b) throws IOException { | |
| private class DocValuesSyntheticFieldLoader implements SourceLoader.SyntheticFieldLoader { | ||
| private BinaryDocValues values; | ||
| private boolean hasValue; | ||
| private final Version indexCreatedVersion; | ||
| | ||
| private DocValuesSyntheticFieldLoader(Version indexCreatedVersion) { | ||
| this.indexCreatedVersion = indexCreatedVersion; | ||
| } | ||
| | ||
| @Override | ||
| public Stream<Map.Entry<String, StoredFieldLoader>> storedFieldLoaders() { | ||
| | @@ -1091,6 +1113,9 @@ public void write(XContentBuilder b) throws IOException { | |
| b.startArray(simpleName()); | ||
| BytesRef ref = values.binaryValue(); | ||
| ByteBuffer byteBuffer = ByteBuffer.wrap(ref.bytes, ref.offset, ref.length); | ||
| if (indexCreatedVersion.onOrAfter(LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION)) { | ||
| byteBuffer.order(ByteOrder.LITTLE_ENDIAN); | ||
| } | ||
| for (int dim = 0; dim < dims; dim++) { | ||
| elementType.readAndWriteValue(byteBuffer, b); | ||
| } | ||
| | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -12,14 +12,21 @@ | |
| import org.elasticsearch.Version; | ||
| | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.ByteOrder; | ||
| import java.nio.FloatBuffer; | ||
| | ||
| import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION; | ||
| import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.MAGNITUDE_STORED_INDEX_VERSION; | ||
| | ||
| public final class VectorEncoderDecoder { | ||
| public static final byte INT_BYTES = 4; | ||
| | ||
| private VectorEncoderDecoder() {} | ||
| | ||
| public static int denseVectorLength(Version indexVersion, BytesRef vectorBR) { | ||
| return indexVersion.onOrAfter(Version.V_7_5_0) ? (vectorBR.length - INT_BYTES) / INT_BYTES : vectorBR.length / INT_BYTES; | ||
| return indexVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION) | ||
| ? (vectorBR.length - INT_BYTES) / INT_BYTES | ||
| : vectorBR.length / INT_BYTES; | ||
| } | ||
| | ||
| /** | ||
| | @@ -28,8 +35,10 @@ public static int denseVectorLength(Version indexVersion, BytesRef vectorBR) { | |
| * equal to 7.5.0, since vectors created prior to that do not store the magnitude. | ||
| */ | ||
| public static float decodeMagnitude(Version indexVersion, BytesRef vectorBR) { | ||
| assert indexVersion.onOrAfter(Version.V_7_5_0); | ||
| ByteBuffer byteBuffer = ByteBuffer.wrap(vectorBR.bytes, vectorBR.offset, vectorBR.length); | ||
| assert indexVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION); | ||
| ByteBuffer byteBuffer = indexVersion.onOrAfter(LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION) | ||
| ? ByteBuffer.wrap(vectorBR.bytes, vectorBR.offset, vectorBR.length).order(ByteOrder.LITTLE_ENDIAN) | ||
| : ByteBuffer.wrap(vectorBR.bytes, vectorBR.offset, vectorBR.length); | ||
| return byteBuffer.getFloat(vectorBR.offset + vectorBR.length - INT_BYTES); | ||
| } | ||
| | ||
| | @@ -49,7 +58,7 @@ public static float getMagnitude(Version indexVersion, BytesRef vectorBR, float[ | |
| if (vectorBR == null) { | ||
| throw new IllegalArgumentException(DenseVectorScriptDocValues.MISSING_VECTOR_FIELD_MESSAGE); | ||
| } | ||
| if (indexVersion.onOrAfter(Version.V_7_5_0)) { | ||
| if (indexVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION)) { | ||
| return decodeMagnitude(indexVersion, vectorBR); | ||
| } else { | ||
| return calculateMagnitude(decodedVector); | ||
| | @@ -61,13 +70,20 @@ public static float getMagnitude(Version indexVersion, BytesRef vectorBR, float[ | |
| * @param vectorBR - dense vector encoded in BytesRef | ||
| * @param vector - array of floats where the decoded vector should be stored | ||
| */ | ||
| public static void decodeDenseVector(BytesRef vectorBR, float[] vector) { | ||
| public static void decodeDenseVector(Version indexVersion, BytesRef vectorBR, float[] vector) { | ||
| if (vectorBR == null) { | ||
| throw new IllegalArgumentException(DenseVectorScriptDocValues.MISSING_VECTOR_FIELD_MESSAGE); | ||
| } | ||
| ByteBuffer byteBuffer = ByteBuffer.wrap(vectorBR.bytes, vectorBR.offset, vectorBR.length); | ||
| for (int dim = 0; dim < vector.length; dim++) { | ||
| vector[dim] = byteBuffer.getFloat((dim * Float.BYTES) + vectorBR.offset); | ||
| if (indexVersion.onOrAfter(LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION)) { | ||
| FloatBuffer fb = ByteBuffer.wrap(vectorBR.bytes, vectorBR.offset, vectorBR.length) | ||
| .order(ByteOrder.LITTLE_ENDIAN) | ||
| .asFloatBuffer(); | ||
| fb.get(vector); | ||
| } else { | ||
| ByteBuffer byteBuffer = ByteBuffer.wrap(vectorBR.bytes, vectorBR.offset, vectorBR.length); | ||
| for (int dim = 0; dim < vector.length; dim++) { | ||
| vector[dim] = byteBuffer.getFloat((dim * Float.BYTES) + vectorBR.offset); | ||
| } | ||
| Comment on lines +78 to +86 Contributor 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. Could Member Author 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.
| ||
| } | ||
| } | ||
| | ||
| | ||
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.
Should this be the last version prior to your change's version using the new
TransportVersionconstants?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.
@jdconrad this is a transport/wire serialization thing. its an index version thing. From my understanding index versioning is different. I will see what I can find.
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.
I asked @thecoop and me just using Version here is OK. We may need to update it to
IndexVersiondepending on which commits make it in first :)