This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch PHOENIX-7998
in repository https://gitbox.apache.org/repos/asf/phoenix.git

commit be5a8678e7b0d74869ebd90ebe30c60cfc679dc2
Author: Andrew Purtell <[email protected]>
AuthorDate: Tue Oct 6 00:53:26 2026 -0700

    PHOENIX-7998 Phase 15: Little-endian vector encoding and direct SIMD loads
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../function/BsonVectorValueFunction.java          | 28 +++++------
 .../expression/function/ScalarDistanceKernel.java  |  4 +-
 .../phoenix/schema/types/PVectorDataType.java      | 10 ++--
 .../apache/phoenix/schema/types/PVectorDouble.java | 20 +++++---
 .../apache/phoenix/schema/types/PVectorFloat.java  | 24 +++++++---
 .../expression/function/DistanceKernel.java        | 54 +++++++---------------
 .../expression/function/PanamaDistanceKernel.java  | 49 +++++++++++++-------
 .../function/BsonVectorValueFunctionTest.java      | 17 ++++++-
 .../phoenix/schema/types/VectorDataTypeTest.java   |  8 +++-
 .../function/PanamaDistanceKernelParityTest.java   | 51 ++++++++++++++++----
 10 files changed, 160 insertions(+), 105 deletions(-)

diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java
index 3d2fb0faca..346ab10559 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/BsonVectorValueFunction.java
@@ -48,18 +48,20 @@ import org.bson.BsonValue;
 import org.apache.phoenix.thirdparty.com.google.common.base.Preconditions;
 
 /**
- * Function extracting a typed float vector from a BSON document field.
+ * Extracts a float vector from a field of a BSON document.
  * <p>
  * Arguments:
  * <ol>
- * <li>BSON document expression (PBson, PJson, or PVarbinary)</li>
- * <li>Target field path literal</li>
- * <li>Vector dimension literal</li>
+ * <li>The BSON document expression (PBson, PJson, or PVarbinary)</li>
+ * <li>The literal path of the target field</li>
+ * <li>The literal vector dimension</li>
  * </ol>
  * </p>
- * Transcodes BSON Binary subtype 9 (VECTOR) FLOAT32 payloads from little 
endian format into the big
- * endian packed representation of {@link PVectorFloat}. Missing paths or 
documents evaluate to SQL
- * NULL. Structural or dimension mismatches raise {@link IllegalDataException}.
+ * The field must hold a BSON Binary value of subtype 9 (VECTOR) with FLOAT32 
elements. The
+ * little-endian FLOAT32 payload is the same as the {@link PVectorFloat} 
encoding, so the function
+ * returns the payload bytes with no element conversion. A missing document, a 
missing field, or a
+ * BSON null field gives SQL NULL. An incorrect structure, a NaN or infinite 
element, or an
+ * incorrect dimension causes an {@link IllegalDataException}.
  */
 @FunctionParseNode.BuiltInFunction(name = BsonVectorValueFunction.NAME,
     nodeClass = BsonVectorValueParseNode.class,
@@ -213,16 +215,8 @@ public class BsonVectorValueFunction extends 
ScalarFunction {
             + ", found " + payloadLength / (double) Bytes.SIZEOF_FLOAT)
           .build().buildException());
     }
-    // Convert little endian BSON float32 payload to big endian PVectorFloat 
encoding
-    byte[] out = new byte[payloadLength];
-    for (int i = 0; i < payloadLength; i += Bytes.SIZEOF_FLOAT) {
-      int src = VECTOR_HEADER_SIZE + i;
-      out[i] = data[src + 3];
-      out[i + 1] = data[src + 2];
-      out[i + 2] = data[src + 1];
-      out[i + 3] = data[src];
-    }
-    ptr.set(out);
+    // The little-endian FLOAT32 payload is the same as the PVectorFloat 
encoding
+    ptr.set(data, VECTOR_HEADER_SIZE, payloadLength);
     return true;
   }
 
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java
index 64ac24423e..611979e8df 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/function/ScalarDistanceKernel.java
@@ -17,7 +17,7 @@
  */
 package org.apache.phoenix.expression.function;
 
-import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.schema.types.PVectorFloat;
 
 /**
  * Reference scalar distance kernels for packed float bytes and {@code 
float[]} arrays. The kernels
@@ -30,7 +30,7 @@ final class ScalarDistanceKernel {
   }
 
   private static float read(byte[] buf, int off, int i) {
-    return Bytes.toFloat(buf, off + i * Bytes.SIZEOF_FLOAT);
+    return PVectorFloat.readElement(buf, off, i);
   }
 
   /**
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
index 8127decd1c..2d25e4a61b 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDataType.java
@@ -27,12 +27,12 @@ import org.apache.phoenix.schema.SortOrder;
 import org.apache.phoenix.util.ByteUtil;
 
 /**
- * Base class for fixed-dimension vector types {@code VECTOR(FLOAT, N)} and
+ * Base class for the fixed-dimension vector types {@code VECTOR(FLOAT, N)} and
  * {@code VECTOR(DOUBLE, N)}.
  * <p>
- * Vectors are serialized as dense, contiguous IEEE 754 big-endian values 
without header metadata or
- * null masks. Vector dimension is stored in column max length. Vectors do not 
support primary key
- * usage or ordering comparisons.
+ * The serialized form of a vector is a dense sequence of little-endian IEEE 
754 values. It has no
+ * header and no null mask. The column max length holds the vector dimension. 
A vector column cannot
+ * be part of a primary key, and vectors do not support order comparisons.
  * @param <T> the primitive array type of the vector, {@code float[]} or 
{@code double[]}
  */
 public abstract class PVectorDataType<T> extends PDataType<T> {
@@ -65,7 +65,7 @@ public abstract class PVectorDataType<T> extends PDataType<T> 
{
   /** Parses one element from its string form. */
   protected abstract Number parseElement(String s);
 
-  /** Packs a vector into a byte buffer as contiguous big-endian IEEE 754 
values. */
+  /** Writes a vector into a byte buffer as a dense sequence of little-endian 
IEEE 754 values. */
   protected abstract void pack(T vector, byte[] buf, int offset);
 
   /** Unpacks a vector from a packed byte range that the writer encoded in the 
given sort order. */
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
index b6219ec928..cf48fe182b 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorDouble.java
@@ -22,8 +22,9 @@ import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.phoenix.schema.SortOrder;
 
 /**
- * Data type representing fixed-dimension double-precision floating point 
vectors,
- * {@code VECTOR(DOUBLE, N)}. Stored as packed big-endian IEEE 754 values.
+ * Data type for fixed-dimension vectors of double-precision floating point 
values,
+ * {@code VECTOR(DOUBLE, N)}. The serialized form is a packed sequence of 
little-endian IEEE 754
+ * values, as in {@link PVectorFloat}.
  */
 public class PVectorDouble extends PVectorDataType<double[]> {
 
@@ -35,18 +36,22 @@ public class PVectorDouble extends 
PVectorDataType<double[]> {
 
   /** Reads the element at the given index from packed vector bytes in ASC 
sort order. */
   public static double readElement(byte[] buf, int offset, int index) {
-    return Bytes.toDouble(buf, offset + index * Bytes.SIZEOF_DOUBLE);
+    return Double.longBitsToDouble(readBits(buf, offset, index));
   }
 
   /** Reads the element at the given index from packed vector bytes in the 
given sort order. */
   public static double readElement(byte[] buf, int offset, int index, 
SortOrder sortOrder) {
-    long b = Bytes.toLong(buf, offset + index * Bytes.SIZEOF_DOUBLE);
+    long b = readBits(buf, offset, index);
     if (sortOrder == SortOrder.DESC) {
       b ^= 0xFFFFFFFFFFFFFFFFL;
     }
     return Double.longBitsToDouble(b);
   }
 
+  private static long readBits(byte[] buf, int offset, int index) {
+    return Long.reverseBytes(Bytes.toLong(buf, offset + index * 
Bytes.SIZEOF_DOUBLE));
+  }
+
   /** Reads the element at the given index from a packed vector pointer in ASC 
sort order. */
   public static double readElement(ImmutableBytesWritable ptr, int index) {
     return readElement(ptr.get(), ptr.getOffset(), index);
@@ -67,8 +72,8 @@ public class PVectorDouble extends PVectorDataType<double[]> {
   }
 
   /**
-   * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 
754 values.
-   * @throws IllegalArgumentException if the buffer is too small to contain 
the vector
+   * Writes a vector into a byte buffer as a packed sequence of little-endian 
IEEE 754 values.
+   * @throws IllegalArgumentException if the buffer cannot hold the vector at 
the given offset
    */
   public static void writeElements(double[] vector, byte[] buf, int offset) {
     if (buf.length < offset + (long) vector.length * Bytes.SIZEOF_DOUBLE) {
@@ -76,7 +81,8 @@ public class PVectorDouble extends PVectorDataType<double[]> {
         + (offset + vector.length * Bytes.SIZEOF_DOUBLE) + " bytes, have " + 
buf.length);
     }
     for (int i = 0; i < vector.length; i++) {
-      Bytes.putDouble(buf, offset + i * Bytes.SIZEOF_DOUBLE, vector[i]);
+      Bytes.putLong(buf, offset + i * Bytes.SIZEOF_DOUBLE,
+        Long.reverseBytes(Double.doubleToRawLongBits(vector[i])));
     }
   }
 
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
index 814e48c044..a0c9eadef1 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/types/PVectorFloat.java
@@ -22,8 +22,13 @@ import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.phoenix.schema.SortOrder;
 
 /**
- * Data type representing fixed-dimension single-precision floating point 
vectors,
- * {@code VECTOR(FLOAT, N)}. Stored as packed big-endian IEEE 754 values.
+ * Data type for fixed-dimension vectors of single-precision floating point 
values,
+ * {@code VECTOR(FLOAT, N)}.
+ * <p>
+ * The serialized form is a packed sequence of little-endian IEEE 754 values. 
Because x86_64 and
+ * arm64 use this byte order, SIMD kernels can load elements directly from 
cell bytes. BSON FLOAT32
+ * vector payloads use the same encoding, so they need no conversion. The 
encoding does not keep the
+ * sort order of values, so a byte comparison of two encoded vectors does not 
give a numeric order.
  */
 public class PVectorFloat extends PVectorDataType<float[]> {
 
@@ -35,18 +40,22 @@ public class PVectorFloat extends PVectorDataType<float[]> {
 
   /** Reads the element at the given index from packed vector bytes in ASC 
sort order. */
   public static float readElement(byte[] buf, int offset, int index) {
-    return Bytes.toFloat(buf, offset + index * Bytes.SIZEOF_FLOAT);
+    return Float.intBitsToFloat(readBits(buf, offset, index));
   }
 
   /** Reads the element at the given index from packed vector bytes in the 
given sort order. */
   public static float readElement(byte[] buf, int offset, int index, SortOrder 
sortOrder) {
-    int b = Bytes.toInt(buf, offset + index * Bytes.SIZEOF_FLOAT);
+    int b = readBits(buf, offset, index);
     if (sortOrder == SortOrder.DESC) {
       b ^= 0xFFFFFFFF;
     }
     return Float.intBitsToFloat(b);
   }
 
+  private static int readBits(byte[] buf, int offset, int index) {
+    return Integer.reverseBytes(Bytes.toInt(buf, offset + index * 
Bytes.SIZEOF_FLOAT));
+  }
+
   /** Reads the element at the given index from a packed vector pointer in ASC 
sort order. */
   public static float readElement(ImmutableBytesWritable ptr, int index) {
     return readElement(ptr.get(), ptr.getOffset(), index);
@@ -67,8 +76,8 @@ public class PVectorFloat extends PVectorDataType<float[]> {
   }
 
   /**
-   * Writes a vector into a byte buffer as packed contiguous big-endian IEEE 
754 values.
-   * @throws IllegalArgumentException if the buffer is too small to contain 
the vector
+   * Writes a vector into a byte buffer as a packed sequence of little-endian 
IEEE 754 values.
+   * @throws IllegalArgumentException if the buffer cannot hold the vector at 
the given offset
    */
   public static void writeElements(float[] vector, byte[] buf, int offset) {
     if (buf.length < offset + (long) vector.length * Bytes.SIZEOF_FLOAT) {
@@ -76,7 +85,8 @@ public class PVectorFloat extends PVectorDataType<float[]> {
         + (offset + vector.length * Bytes.SIZEOF_FLOAT) + " bytes, have " + 
buf.length);
     }
     for (int i = 0; i < vector.length; i++) {
-      Bytes.putFloat(buf, offset + i * Bytes.SIZEOF_FLOAT, vector[i]);
+      Bytes.putInt(buf, offset + i * Bytes.SIZEOF_FLOAT,
+        Integer.reverseBytes(Float.floatToRawIntBits(vector[i])));
     }
   }
 
diff --git 
a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java
 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java
index 6fff9f6cb8..22ba674adc 100644
--- 
a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java
+++ 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/DistanceKernel.java
@@ -17,23 +17,18 @@
  */
 package org.apache.phoenix.expression.function;
 
-import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.schema.types.PVectorFloat;
 
 /**
- * Java 21 multi-release implementation of {@code DistanceKernel}. Dispatches 
to
- * {@link PanamaDistanceKernel} when {@code jdk.incubator.vector} is 
available, falling back to
- * {@link ScalarDistanceKernel}.
- * <p>
- * Decodes packed big-endian float vectors into thread local float buffers 
prior to SIMD execution
- * to satisfy Vector API buffer alignment and endianness requirements.
+ * Java 21 multi-release implementation of {@code DistanceKernel}. It uses
+ * {@link PanamaDistanceKernel} if {@code jdk.incubator.vector} is available 
and passes a self-test.
+ * If not, it uses {@link ScalarDistanceKernel}. Both kernels read operands 
directly from packed
+ * vector bytes.
  */
 final class DistanceKernel {
 
   private static final boolean SIMD = isPanamaAvailable();
 
-  private static final ThreadLocal<float[][]> SCRATCH =
-    ThreadLocal.withInitial(() -> new float[][] { new float[0], new float[0] 
});
-
   private DistanceKernel() {
   }
 
@@ -44,45 +39,28 @@ final class DistanceKernel {
   private static boolean isPanamaAvailable() {
     try {
       Class.forName("jdk.incubator.vector.FloatVector");
-      return PanamaDistanceKernel.dotProduct(new float[] { 1f }, new float[] { 
1f }, 1) == 1.0;
+      byte[] one = PVectorFloat.INSTANCE.toBytes(new float[] { 1f });
+      return PanamaDistanceKernel.dotProduct(one, 0, one, 0, 1) == 1.0;
     } catch (Throwable t) {
       return false;
     }
   }
 
-  private static float[] decode(int slot, byte[] buf, int off, int dim) {
-    float[][] scratch = SCRATCH.get();
-    float[] out = scratch[slot];
-    if (out.length < dim) {
-      out = new float[dim];
-      scratch[slot] = out;
-    }
-    for (int i = 0; i < dim; i++) {
-      out[i] = Bytes.toFloat(buf, off + i * Bytes.SIZEOF_FLOAT);
-    }
-    return out;
-  }
-
   static double l2DistanceSquared(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
-    if (!SIMD) {
-      return ScalarDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, 
bound);
-    }
-    return PanamaDistanceKernel.l2DistanceSquared(decode(0, a, aOff, dim), 
decode(1, b, bOff, dim),
-      dim, bound);
+    return SIMD
+      ? PanamaDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, bound)
+      : ScalarDistanceKernel.l2DistanceSquared(a, aOff, b, bOff, dim, bound);
   }
 
   static double dotProduct(byte[] a, int aOff, byte[] b, int bOff, int dim) {
-    if (!SIMD) {
-      return ScalarDistanceKernel.dotProduct(a, aOff, b, bOff, dim);
-    }
-    return PanamaDistanceKernel.dotProduct(decode(0, a, aOff, dim), decode(1, 
b, bOff, dim), dim);
+    return SIMD
+      ? PanamaDistanceKernel.dotProduct(a, aOff, b, bOff, dim)
+      : ScalarDistanceKernel.dotProduct(a, aOff, b, bOff, dim);
   }
 
   static double cosineDistance(byte[] a, int aOff, byte[] b, int bOff, int 
dim) {
-    if (!SIMD) {
-      return ScalarDistanceKernel.cosineDistance(a, aOff, b, bOff, dim);
-    }
-    return PanamaDistanceKernel.cosineDistance(decode(0, a, aOff, dim), 
decode(1, b, bOff, dim),
-      dim);
+    return SIMD
+      ? PanamaDistanceKernel.cosineDistance(a, aOff, b, bOff, dim)
+      : ScalarDistanceKernel.cosineDistance(a, aOff, b, bOff, dim);
   }
 }
diff --git 
a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java
 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java
index 386f1a4f86..5534d276e5 100644
--- 
a/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java
+++ 
b/phoenix-core-client/src/main/java21/org/apache/phoenix/expression/function/PanamaDistanceKernel.java
@@ -17,27 +17,43 @@
  */
 package org.apache.phoenix.expression.function;
 
+import jdk.incubator.vector.ByteVector;
 import jdk.incubator.vector.FloatVector;
 import jdk.incubator.vector.VectorOperators;
 import jdk.incubator.vector.VectorSpecies;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.schema.types.PVectorFloat;
 
 /**
- * SIMD distance kernels leveraging the Panama Vector API over {@code float[]} 
operands.
+ * SIMD distance kernels that use the Panama Vector API on packed 
little-endian float vectors.
  * <p>
- * Accumulates intermediate lane sums in single precision across fixed size 
blocks of
- * {@link #BLOCK_STRIDES} vector strides before reducing to double precision. 
Block level reduction
- * bounds floating point accumulation drift relative to {@link 
ScalarDistanceKernel} while enabling
- * periodic upper bound pruning without per-stride reduction overhead.
+ * The kernels load operands directly from their {@code byte[]} encodings, at 
any offset, as byte
+ * vectors. Then they reinterpret the bytes as float lanes. The Vector API 
defines this
+ * reinterpretation as little-endian on all platforms, which agrees with the 
{@link PVectorFloat}
+ * encoding. Because of this, the kernels do not decode or copy the operands.
+ * <p>
+ * Each kernel adds lane sums in single precision across a block of {@link 
#BLOCK_STRIDES} vector
+ * strides. Then it adds the block result to a double-precision total. This 
limits the rounding
+ * error relative to {@link ScalarDistanceKernel}. It also lets the L2 kernel 
compare the total with
+ * the upper bound after each block and stop early if the total is larger. The 
cost of a reduction
+ * occurs once for each block, not once for each stride.
  */
 final class PanamaDistanceKernel {
 
   private static final VectorSpecies<Float> SPECIES = 
FloatVector.SPECIES_PREFERRED;
+  private static final VectorSpecies<Byte> BYTE_SPECIES =
+    VectorSpecies.of(byte.class, SPECIES.vectorShape());
   static final int BLOCK_STRIDES = 8;
 
   private PanamaDistanceKernel() {
   }
 
-  static double l2DistanceSquared(float[] a, float[] b, int dim, double bound) 
{
+  private static FloatVector load(byte[] buf, int off, int i) {
+    return ByteVector.fromArray(BYTE_SPECIES, buf, off + i * 
Bytes.SIZEOF_FLOAT)
+      .reinterpretAsFloats();
+  }
+
+  static double l2DistanceSquared(byte[] a, int aOff, byte[] b, int bOff, int 
dim, double bound) {
     int step = SPECIES.length();
     int upper = SPECIES.loopBound(dim);
     int blockLen = step * BLOCK_STRIDES;
@@ -47,8 +63,7 @@ final class PanamaDistanceKernel {
       int blockEnd = Math.min(upper, i + blockLen);
       FloatVector acc = FloatVector.zero(SPECIES);
       for (; i < blockEnd; i += step) {
-        FloatVector diff =
-          FloatVector.fromArray(SPECIES, a, 
i).sub(FloatVector.fromArray(SPECIES, b, i));
+        FloatVector diff = load(a, aOff, i).sub(load(b, bOff, i));
         acc = diff.fma(diff, acc);
       }
       total += acc.reduceLanes(VectorOperators.ADD);
@@ -57,7 +72,7 @@ final class PanamaDistanceKernel {
       }
     }
     for (; i < dim; i++) {
-      double diff = a[i] - b[i];
+      double diff = PVectorFloat.readElement(a, aOff, i) - 
PVectorFloat.readElement(b, bOff, i);
       total += diff * diff;
       if (total > bound) {
         return Double.MAX_VALUE;
@@ -66,7 +81,7 @@ final class PanamaDistanceKernel {
     return total;
   }
 
-  static double dotProduct(float[] a, float[] b, int dim) {
+  static double dotProduct(byte[] a, int aOff, byte[] b, int bOff, int dim) {
     int step = SPECIES.length();
     int upper = SPECIES.loopBound(dim);
     int blockLen = step * BLOCK_STRIDES;
@@ -76,17 +91,17 @@ final class PanamaDistanceKernel {
       int blockEnd = Math.min(upper, i + blockLen);
       FloatVector acc = FloatVector.zero(SPECIES);
       for (; i < blockEnd; i += step) {
-        acc = FloatVector.fromArray(SPECIES, a, 
i).fma(FloatVector.fromArray(SPECIES, b, i), acc);
+        acc = load(a, aOff, i).fma(load(b, bOff, i), acc);
       }
       total += acc.reduceLanes(VectorOperators.ADD);
     }
     for (; i < dim; i++) {
-      total += (double) a[i] * b[i];
+      total += (double) PVectorFloat.readElement(a, aOff, i) * 
PVectorFloat.readElement(b, bOff, i);
     }
     return total;
   }
 
-  static double cosineDistance(float[] a, float[] b, int dim) {
+  static double cosineDistance(byte[] a, int aOff, byte[] b, int bOff, int 
dim) {
     int step = SPECIES.length();
     int upper = SPECIES.loopBound(dim);
     int blockLen = step * BLOCK_STRIDES;
@@ -100,8 +115,8 @@ final class PanamaDistanceKernel {
       FloatVector vNormA = FloatVector.zero(SPECIES);
       FloatVector vNormB = FloatVector.zero(SPECIES);
       for (; i < blockEnd; i += step) {
-        FloatVector va = FloatVector.fromArray(SPECIES, a, i);
-        FloatVector vb = FloatVector.fromArray(SPECIES, b, i);
+        FloatVector va = load(a, aOff, i);
+        FloatVector vb = load(b, bOff, i);
         vDot = va.fma(vb, vDot);
         vNormA = va.fma(va, vNormA);
         vNormB = vb.fma(vb, vNormB);
@@ -111,8 +126,8 @@ final class PanamaDistanceKernel {
       normB += vNormB.reduceLanes(VectorOperators.ADD);
     }
     for (; i < dim; i++) {
-      double ai = a[i];
-      double bi = b[i];
+      double ai = PVectorFloat.readElement(a, aOff, i);
+      double bi = PVectorFloat.readElement(b, bOff, i);
       dot += ai * bi;
       normA += ai * ai;
       normB += bi * bi;
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java
index a461806b77..a9789cd42e 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/expression/function/BsonVectorValueFunctionTest.java
@@ -104,7 +104,7 @@ public class BsonVectorValueFunctionTest {
 
   @Test
   public void testExtractsSpecEncodedVector() throws Exception {
-    // Verify extraction and big-endian transcoding against IEEE 754 float 
values.
+    // Make sure that extraction gives the original IEEE 754 float values.
     float[] expected = new float[] { 1.5f, -2.5f, 3.0e-3f };
     BsonDocument doc = new BsonDocument("search", new 
BsonDocument("embedding", vector(expected)));
     BsonVectorValueFunction f = func(doc, "search.embedding", 3);
@@ -115,6 +115,21 @@ public class BsonVectorValueFunctionTest {
     assertArrayEquals(expected, eval(f), DELTA);
   }
 
+  @Test
+  public void testOutputIsBsonPayloadUnchanged() throws Exception {
+    // The BSON FLOAT32 payload is the PVectorFloat encoding, so the output 
must be the same bytes.
+    float[] values = new float[] { 1.5f, -2.5f, 3.0e-3f, Float.MIN_VALUE };
+    BsonBinary binary = vector(values);
+    BsonVectorValueFunction f = func(new BsonDocument("v", binary), "v", 
values.length);
+    ImmutableBytesWritable ptr = new ImmutableBytesWritable();
+    assertTrue(f.evaluate(null, ptr));
+    byte[] data = binary.getData();
+    assertArrayEquals(
+      Arrays.copyOfRange(data, BsonVectorValueFunction.VECTOR_HEADER_SIZE, 
data.length),
+      ptr.copyBytes());
+    assertArrayEquals(PVectorFloat.INSTANCE.toBytes(values), ptr.copyBytes());
+  }
+
   @Test
   public void testExtractsFromRawDocumentAsStoredInCell() throws Exception {
     // The function must also read a RawBsonDocument, the form of a document 
in a stored cell
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
index b32aea37c9..9b3fd8dcf4 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/schema/types/VectorDataTypeTest.java
@@ -26,6 +26,8 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.phoenix.coprocessor.generated.PTableProtos;
@@ -354,7 +356,8 @@ public class VectorDataTypeTest {
     PVectorFloat.writeElements(original, encoded, 0);
 
     for (int i = 0; i < original.length; i++) {
-      int rawBits = Bytes.toInt(encoded, i * Bytes.SIZEOF_FLOAT);
+      int rawBits =
+        ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN).getInt(i * 
Bytes.SIZEOF_FLOAT);
       assertEquals("Element " + i + " bit pattern mismatch", 
Float.floatToRawIntBits(original[i]),
         rawBits);
     }
@@ -371,7 +374,8 @@ public class VectorDataTypeTest {
     PVectorDouble.writeElements(original, encoded, 0);
 
     for (int i = 0; i < original.length; i++) {
-      long rawBits = Bytes.toLong(encoded, i * Bytes.SIZEOF_DOUBLE);
+      long rawBits =
+        ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN).getLong(i * 
Bytes.SIZEOF_DOUBLE);
       assertEquals("Element " + i + " bit pattern mismatch",
         Double.doubleToRawLongBits(original[i]), rawBits);
     }
diff --git 
a/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java
 
b/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java
index 70a22febf5..069f04c697 100644
--- 
a/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java
+++ 
b/phoenix-core/src/test/java21/org/apache/phoenix/expression/function/PanamaDistanceKernelParityTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import java.util.Random;
+import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.phoenix.schema.types.PVectorFloat;
 import org.junit.Test;
 
@@ -33,6 +34,8 @@ public class PanamaDistanceKernelParityTest {
 
   private static final int[] DIMS = { 1, 3, 7, 8, 15, 16, 64, 100, 128, 129, 
768, 1536 };
   private static final double TOLERANCE = 1e-5;
+  /** A value can start at any offset in a cell, so the kernels must load from 
any offset. */
+  private static final int[] OFFSETS = { 0, 1, 3, 4 };
 
   private static float[] random(Random rng, int dim) {
     float[] v = new float[dim];
@@ -42,9 +45,32 @@ public class PanamaDistanceKernelParityTest {
     return v;
   }
 
+  /** Packs {@code v} at {@code off} between random bytes. A kernel must not 
read these bytes. */
+  private static byte[] pack(Random rng, float[] v, int off) {
+    byte[] buf = new byte[off + v.length * Bytes.SIZEOF_FLOAT + 64];
+    rng.nextBytes(buf);
+    PVectorFloat.writeElements(v, buf, off);
+    return buf;
+  }
+
   private static void assertClose(String what, double expected, double actual) 
{
-    double scale = Math.max(1.0, Math.abs(expected));
-    assertEquals(what, expected, actual, TOLERANCE * scale);
+    assertClose(what, expected, actual, Math.abs(expected));
+  }
+
+  private static void assertClose(String what, double expected, double actual, 
double magnitude) {
+    assertEquals(what, expected, actual, TOLERANCE * Math.max(1.0, magnitude));
+  }
+
+  /**
+   * Returns the sum of the absolute term values of a dot product. 
Single-precision rounding error
+   * scales with this sum, not with the result, because cancellation can make 
the result very small.
+   */
+  private static double termMagnitude(float[] a, float[] b) {
+    double sum = 0.0;
+    for (int i = 0; i < a.length; i++) {
+      sum += Math.abs((double) a[i] * b[i]);
+    }
+    return sum;
   }
 
   @Test
@@ -60,14 +86,18 @@ public class PanamaDistanceKernelParityTest {
       for (int trial = 0; trial < 50; trial++) {
         float[] a = random(rng, dim);
         float[] b = random(rng, dim);
-        String ctx = "dim=" + dim + " trial=" + trial;
+        int aOff = OFFSETS[trial % OFFSETS.length];
+        int bOff = OFFSETS[(trial / OFFSETS.length) % OFFSETS.length];
+        byte[] pa = pack(rng, a, aOff);
+        byte[] pb = pack(rng, b, bOff);
+        String ctx = "dim=" + dim + " trial=" + trial + " aOff=" + aOff + " 
bOff=" + bOff;
         assertClose("l2sq " + ctx,
           ScalarDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE),
-          PanamaDistanceKernel.l2DistanceSquared(a, b, dim, Double.MAX_VALUE));
+          PanamaDistanceKernel.l2DistanceSquared(pa, aOff, pb, bOff, dim, 
Double.MAX_VALUE));
         assertClose("dot " + ctx, ScalarDistanceKernel.dotProduct(a, b, dim),
-          PanamaDistanceKernel.dotProduct(a, b, dim));
+          PanamaDistanceKernel.dotProduct(pa, aOff, pb, bOff, dim), 
termMagnitude(a, b));
         assertClose("cosine " + ctx, ScalarDistanceKernel.cosineDistance(a, b, 
dim),
-          PanamaDistanceKernel.cosineDistance(a, b, dim));
+          PanamaDistanceKernel.cosineDistance(pa, aOff, pb, bOff, dim));
       }
     }
   }
@@ -87,7 +117,8 @@ public class PanamaDistanceKernelParityTest {
         Math.sqrt(ScalarDistanceKernel.l2DistanceSquared(a, b, dim, 
Double.MAX_VALUE)),
         VectorDistanceUtil.l2DistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
       assertClose("ip " + ctx, -ScalarDistanceKernel.dotProduct(a, b, dim),
-        VectorDistanceUtil.innerProductDistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
+        VectorDistanceUtil.innerProductDistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE),
+        termMagnitude(a, b));
       assertClose("cosine " + ctx, ScalarDistanceKernel.cosineDistance(a, b, 
dim),
         VectorDistanceUtil.cosineDistanceWithBound(pa, 0, pb, 0, dim, 
Double.MAX_VALUE));
     }
@@ -99,11 +130,13 @@ public class PanamaDistanceKernelParityTest {
     for (int dim : DIMS) {
       float[] a = random(rng, dim);
       float[] b = random(rng, dim);
+      byte[] pa = pack(rng, a, 1);
+      byte[] pb = pack(rng, b, 0);
       double exact = ScalarDistanceKernel.l2DistanceSquared(a, b, dim, 
Double.MAX_VALUE);
       assertEquals(Double.MAX_VALUE,
-        PanamaDistanceKernel.l2DistanceSquared(a, b, dim, exact * 0.5), 0.0);
+        PanamaDistanceKernel.l2DistanceSquared(pa, 1, pb, 0, dim, exact * 
0.5), 0.0);
       assertClose("dim=" + dim, exact,
-        PanamaDistanceKernel.l2DistanceSquared(a, b, dim, exact * 1.5));
+        PanamaDistanceKernel.l2DistanceSquared(pa, 1, pb, 0, dim, exact * 
1.5));
     }
   }
 }

Reply via email to