Skip to content
Merged
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
28 changes: 3 additions & 25 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ jobs:
cancel-in-progress: false
strategy:
matrix:
jdk: [ 20, 24 ]
# jdk: [ 24 ]
jdk: [ 24 ]
isa: [ isa-avx512f ]
runs-on: ${{ matrix.isa }}
steps:
Expand Down Expand Up @@ -48,20 +47,6 @@ jobs:
distribution: temurin
cache: maven

- name: Verify Panama Vector Support (JDK ${{ matrix.jdk }})
if: matrix.jdk == '20'
run: >-
mvn -B -Pjdk20 -pl jvector-tests -am test
-Dsurefire.failIfNoSpecifiedTests=false
-Dtest=TestVectorizationProvider
-DTest_RequireSpecificVectorizationProvider=PanamaVectorizationProvider

- name: Test Panama Support (JDK ${{ matrix.jdk }})
if: matrix.jdk == '20'
run: >-
mvn -B -Pjdk20 -pl jvector-tests test -am test
-DTest_RequireSpecificVectorizationProvider=PanamaVectorizationProvider

- name: Verify native-access vector support (JDK ${{ matrix.jdk }})
if: matrix.jdk == '24'
run: >-
Expand Down Expand Up @@ -97,7 +82,7 @@ jobs:
cancel-in-progress: true
strategy:
matrix:
jdk: [ 11, 20, 22]
jdk: [ 22 ]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -112,15 +97,8 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt install -y gcc
- name: Compile, run tests, and package (JDK 22)
- name: Compile, run tests, and package
run: mvn -B verify
if: matrix.jdk == '22'
- name: Compile, run tests, and package (JDK 20)
run: mvn -B -Pjdk20 -am -pl jvector-tests test
if: matrix.jdk == '20'
- name: Compile and run tests (JDK 11)
run: mvn -B -Pjdk11 -am -pl jvector-tests test
if: matrix.jdk == '11'
- name: Test Summary
if: always()
uses: test-summary/action@v2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright DataStax, Inc.
*
* Licensed 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 io.github.jbellis.jvector.graph;

import io.github.jbellis.jvector.vector.VectorizationProvider;
import io.github.jbellis.jvector.vector.types.VectorFloat;
import io.github.jbellis.jvector.vector.types.VectorTypeSupport;

import java.nio.ByteBuffer;
import java.util.Objects;

/**
* A {@link RandomAccessVectorValues} backed by a single caller-owned {@link ByteBuffer} that
* holds {@code count × dimension × Float.BYTES} bytes of concatenated IEEE 754 floats.
*
* <p>Vectors are never copied: {@link #getVector(int)} returns a view (via the active
* {@link VectorTypeSupport#wrapFloatVector(ByteBuffer, int, int)}) that aliases the source
* bytes. This lets integrators whose data is already in ByteBuffer form (for example a
* database page cache) feed {@code GraphIndexBuilder} without the usual {@code float[]}
* materialization step.
*
* <p>The buffer is pre-sliced at construction time so later mutation of the caller's buffer
* position / limit does not disturb this RAVV. Mutating the buffer's <em>contents</em> after
* construction is visible to this RAVV — callers are responsible for ensuring writers and
* readers do not race.
*/
public class ByteBufferRandomAccessVectorValues implements RandomAccessVectorValues
{
private static final VectorTypeSupport VTS =
VectorizationProvider.getInstance().getVectorTypeSupport();

private final ByteBuffer data;
private final int count;
private final int dimension;

public ByteBufferRandomAccessVectorValues(ByteBuffer data, int count, int dimension)
{
Objects.requireNonNull(data, "data");
if (count < 0) throw new IllegalArgumentException("count must be >= 0, was " + count);
if (dimension <= 0) throw new IllegalArgumentException("dimension must be > 0, was " + dimension);
long need = (long) count * dimension * Float.BYTES;
if (data.remaining() < need) {
throw new IllegalArgumentException(
"buffer too small: need " + need + " bytes, have " + data.remaining());
}
this.data = data.slice(data.position(), (int) need).order(data.order());
this.count = count;
this.dimension = dimension;
}

@Override
public int size()
{
return count;
}

@Override
public int dimension()
{
return dimension;
}

@Override
public VectorFloat<?> getVector(int targetOrd)
{
if (targetOrd < 0 || targetOrd >= count) {
throw new IndexOutOfBoundsException("ordinal " + targetOrd + " out of [0, " + count + ")");
}
return VTS.wrapFloatVector(data, targetOrd * dimension, dimension);
}

@Override
public boolean isValueShared()
{
// Each getVector returns a distinct view instance — callers may hold onto results.
return false;
}

@Override
public ByteBufferRandomAccessVectorValues copy()
{
return this;
}

/**
* Convenience factory accepting a plain {@code float[]} (copying it into a native-endian
* ByteBuffer). Primarily used by tests that already have {@code float[]} fixtures.
*/
public static ByteBufferRandomAccessVectorValues fromFloats(float[][] vectors, int dimension)
{
Objects.requireNonNull(vectors, "vectors");
if (dimension <= 0) throw new IllegalArgumentException("dimension > 0 required");
int count = vectors.length;
ByteBuffer bb = ByteBuffer.allocate(count * dimension * Float.BYTES)
.order(java.nio.ByteOrder.LITTLE_ENDIAN);
for (float[] v : vectors) {
if (v.length != dimension) {
throw new IllegalArgumentException("vector dimension mismatch: " + v.length + " vs " + dimension);
}
for (float f : v) bb.putFloat(f);
}
bb.rewind();
return new ByteBufferRandomAccessVectorValues(bb, count, dimension);
}

/** @return the live view over the backing data (for tests/integration). */
public ByteBuffer data()
{
return data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@
import io.github.jbellis.jvector.util.ExplicitThreadLocal;
import io.github.jbellis.jvector.util.PhysicalCoreExecutor;
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
import io.github.jbellis.jvector.vector.VectorizationProvider;
import io.github.jbellis.jvector.vector.types.VectorFloat;
import io.github.jbellis.jvector.vector.types.VectorTypeSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
Expand Down Expand Up @@ -593,6 +596,17 @@ public long addGraphNode(int node, VectorFloat<?> vector) {
return addGraphNode(node, ssp);
}

/**
* Zero-copy ByteBuffer overload of {@link #addGraphNode(int, VectorFloat)}. The buffer is
* interpreted as contiguous IEEE 754 floats in its current {@link java.nio.ByteOrder}, so
* callers that already hold vector data as ByteBuffer (for example a database page cache)
* can feed the builder without the usual {@code float[]} materialization step.
*/
public long addGraphNode(int node, ByteBuffer vector) {
VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport();
return addGraphNode(node, vts.wrapFloatVector(vector));
}

/**
* Inserts a node with the given vector value to the graph.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@
import io.github.jbellis.jvector.util.BoundedLongHeap;
import io.github.jbellis.jvector.util.GrowableLongHeap;
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
import io.github.jbellis.jvector.vector.VectorizationProvider;
import io.github.jbellis.jvector.vector.types.VectorFloat;
import io.github.jbellis.jvector.vector.types.VectorTypeSupport;
import org.agrona.collections.Int2ObjectHashMap;
import org.agrona.collections.IntHashSet;

import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;


/**
Expand Down Expand Up @@ -151,6 +154,26 @@ public static SearchResult search(VectorFloat<?> queryVector, int topK, int rera
}
}

/**
* Zero-copy ByteBuffer overload of {@link #search(VectorFloat, int, RandomAccessVectorValues,
* VectorSimilarityFunction, ImmutableGraphIndex, Bits)}. The buffer is interpreted as
* {@code Float.BYTES}-aligned IEEE 754 floats in its current {@link java.nio.ByteOrder};
* no {@code float[]} materialization occurs.
*/
public static SearchResult search(ByteBuffer queryVector, int topK, RandomAccessVectorValues vectors, VectorSimilarityFunction similarityFunction, ImmutableGraphIndex graph, Bits acceptOrds) {
VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport();
return search(vts.wrapFloatVector(queryVector), topK, vectors, similarityFunction, graph, acceptOrds);
}

/**
* Zero-copy ByteBuffer overload with explicit rerankK. See
* {@link #search(ByteBuffer, int, RandomAccessVectorValues, VectorSimilarityFunction, ImmutableGraphIndex, Bits)}.
*/
public static SearchResult search(ByteBuffer queryVector, int topK, int rerankK, RandomAccessVectorValues vectors, VectorSimilarityFunction similarityFunction, ImmutableGraphIndex graph, Bits acceptOrds) {
VectorTypeSupport vts = VectorizationProvider.getInstance().getVectorTypeSupport();
return search(vts.wrapFloatVector(queryVector), topK, rerankK, vectors, similarityFunction, graph, acceptOrds);
}

/**
* Sets the view of the graph to be used by the searcher.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,12 +517,14 @@ int closestCentroidIndex(VectorFloat<?> subvector, int m, VectorFloat<?> codeboo
}

/**
* Extracts the m-th subvector from a single vector.
* Extracts the m-th subvector from a single vector. Returns a zero-copy view via
* {@link VectorFloat#subview(int, int)} — no {@code float[]} allocation per subvector,
* which matters at codebook training time where this is called once per
* {@code (training_vector × subspace)} and would otherwise produce
* {@code N × M} small heap allocations.
*/
static VectorFloat<?> getSubVector(VectorFloat<?> vector, int m, int[][] subvectorSizeAndOffset) {
VectorFloat<?> subvector = vectorTypeSupport.createFloatVector(subvectorSizeAndOffset[m][0]);
subvector.copyFrom(vector, subvectorSizeAndOffset[m][1], 0, subvectorSizeAndOffset[m][0]);
return subvector;
return vector.subview(subvectorSizeAndOffset[m][1], subvectorSizeAndOffset[m][0]);
}

/**
Expand Down
Loading
Loading