Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.github.jbellis.jvector.graph.SearchResult.NodeScore;
import io.github.jbellis.jvector.graph.diversity.VamanaDiversityProvider;
import io.github.jbellis.jvector.graph.similarity.BuildScoreProvider;
import io.github.jbellis.jvector.graph.VectorValues;
import io.github.jbellis.jvector.graph.similarity.ScoreFunction;
import io.github.jbellis.jvector.graph.similarity.SearchScoreProvider;
import io.github.jbellis.jvector.management.CompressionType;
Expand All @@ -32,7 +33,9 @@
import io.github.jbellis.jvector.quantization.PQVectors;
import io.github.jbellis.jvector.quantization.ProductQuantization;
import io.github.jbellis.jvector.util.*;
import io.github.jbellis.jvector.vector.ByteVectorSimilarityFunction;
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
import io.github.jbellis.jvector.vector.types.ByteSequence;
import io.github.jbellis.jvector.vector.types.VectorFloat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -114,6 +117,11 @@ private static BuildScoreProvider getBuildScoreProvider(RandomAccessVectorValues
}
}

private static BuildScoreProvider getBuildScoreProvider(RandomAccessByteVectorValues vectorValues, ByteVectorSimilarityFunction similarityFunction) {
// Byte vectors do not support PQ/BQ build-time compression; always use the direct provider.
return BuildScoreProvider.byteVectorScoreProvider(vectorValues, similarityFunction);
}

/**
* Reads all the vectors from vector values, builds a graph connecting them by their dense
* ordinals, using the given hyperparameter settings, and returns the resulting graph.
Expand Down Expand Up @@ -407,6 +415,31 @@ public static Builder builder(RandomAccessVectorValues vectorValues, VectorSimil
return builder(vectorValues, similarityFunction, List.of(M));
}

/**
* Entry point for the fluent builder for int8 byte vectors, resolving the {@link BuildScoreProvider}
* from raw byte vectors via {@link BuildScoreProvider#byteVectorScoreProvider}.
*
* @param vectorValues the int8 byte vectors whose relations are represented by the graph
* @param similarityFunction the byte-vector similarity function to score vectors with
* @param maxDegrees the maximum number of connections a node can have in each layer; if fewer entries
* are specified than the number of layers, the last entry is used for all remaining layers.
*/
public static Builder builder(RandomAccessByteVectorValues vectorValues, ByteVectorSimilarityFunction similarityFunction, List<Integer> maxDegrees) {
return new Builder(getBuildScoreProvider(vectorValues, similarityFunction), vectorValues.dimension(), maxDegrees);
}

/**
* Entry point for the fluent builder for int8 byte vectors, for the common case of a single
* (non-hierarchical) max degree.
*
* @param vectorValues the int8 byte vectors whose relations are represented by the graph
* @param similarityFunction the byte-vector similarity function to score vectors with
* @param M the maximum number of connections a node can have
*/
public static Builder builder(RandomAccessByteVectorValues vectorValues, ByteVectorSimilarityFunction similarityFunction, int M) {
return builder(vectorValues, similarityFunction, List.of(M));
}

/**
* Entry point for the fluent builder, building from an existing {@link MutableGraphIndex} (e.g. one just
* loaded from disk) rather than constructing a fresh {@link OnHeapGraphIndex}. {@code addHierarchy} is not
Expand Down Expand Up @@ -689,13 +722,17 @@ public static GraphIndexBuilder rescore(GraphIndexBuilder other, BuildScoreProvi
return newBuilder;
}

public ImmutableGraphIndex build(RandomAccessVectorValues ravv) {
var vv = ravv.threadLocalSupplier();
/**
* Builds the graph from any {@link VectorValues} source — works for both
* {@link RandomAccessVectorValues} (float32) and {@link RandomAccessByteVectorValues} (int8).
* The score provider supplied at construction time determines how vectors are compared.
*/
public ImmutableGraphIndex build(VectorValues<?> ravv) {
int size = ravv.size();

simdExecutor.submit(() -> {
IntStream.range(0, size).parallel().forEach(node -> {
addGraphNode(node, vv.get().getVector(node));
addGraphNode(node, scoreProvider.searchProviderFor(node));
});
}).join();

Expand Down Expand Up @@ -846,6 +883,19 @@ public long addGraphNode(int node, VectorFloat<?> vector) {
return addGraphNode(node, ssp);
}

/**
* Inserts a node with the given int8 byte vector into the graph.
*
* @param node the node ID to add
* @param vector the byte vector to add
* @return an estimate of the number of extra bytes used by the graph after adding the given node
* @throws UnsupportedOperationException if this builder was not constructed with a byte-vector score provider
*/
public long addGraphNode(int node, ByteSequence<?> vector) {
var ssp = scoreProvider.searchProviderFor(vector);
return addGraphNode(node, ssp);
}

/**
* Inserts a node with the given vector value to the graph.
*
Expand Down Expand Up @@ -1331,4 +1381,4 @@ public static ImmutableGraphIndex buildAndMergeNewNodes(RandomAccessReader in,
return builder.getGraph();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.types.ByteSequence;

import java.util.List;

/**
* A List-backed implementation of the {@link RandomAccessByteVectorValues} interface.
* <p>
* It is acceptable to provide this class to a GraphBuilder, and then continue
* to add vectors to the backing List as you add to the graph.
* <p>
* This will be as threadsafe as the provided List.
*/
public class ListRandomAccessByteVectorValues implements RandomAccessByteVectorValues {
private final List<ByteSequence<?>> vectors;
private final int dimension;

/**
* Construct a new instance of {@link ListRandomAccessByteVectorValues}.
*
* @param vectors a (potentially mutable) list of byte vectors.
* @param dimension the dimension of the vectors.
*/
public ListRandomAccessByteVectorValues(List<ByteSequence<?>> vectors, int dimension) {
this.vectors = vectors;
this.dimension = dimension;
}

@Override
public int size() {
return vectors.size();
}

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

@Override
public ByteSequence<?> getVector(int nodeId) {
return vectors.get(nodeId);
}

@Override
public boolean isValueShared() {
return false;
}

@Override
public ListRandomAccessByteVectorValues copy() {
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.types.ByteSequence;

/**
* Provides random access to byte (int8) vectors by dense ordinal.
* <p>
* This is the byte-vector parallel to {@link RandomAccessVectorValues}.
* Both extend the common super-interface {@link VectorValues}.
* It is used by graph-based index builders and searchers that operate natively
* on int8 vectors without a float32 round-trip.
*/
public interface RandomAccessByteVectorValues extends VectorValues<ByteSequence<?>> {

/**
* Creates a new copy of this {@link RandomAccessByteVectorValues}.
* Un-shared implementations may simply return {@code this}.
*/
@Override
RandomAccessByteVectorValues copy();
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@
import io.github.jbellis.jvector.vector.types.VectorFloat;

import java.util.function.Supplier;
import java.util.logging.Logger;

/**
* Provides random access to vectors by dense ordinal. This interface is used by graph-based
* Provides random access to float32 vectors by dense ordinal. This interface is used by graph-based
* implementations of KNN search.
* <p>
* For int8 vectors see {@link RandomAccessByteVectorValues}.
* Both extend the common super-interface {@link VectorValues}.
*/
public interface RandomAccessVectorValues {
Logger LOG = Logger.getLogger(RandomAccessVectorValues.class.getName());
public interface RandomAccessVectorValues extends VectorValues<VectorFloat<?>> {

/**
* Return the number of vector values.
Expand All @@ -46,23 +47,9 @@ public interface RandomAccessVectorValues {
* (1) implementing a threadsafe, un-shared RAVV, where `copy` returns `this`, or
* (2) implementing a fixed-size RAVV.
*/
@Override
int size();

/** Return the dimension of the returned vector values */
int dimension();

/**
* Return the vector value indexed at the given ordinal.
*
* <p>For performance, implementations are free to re-use the same object across invocations.
* That is, you will get back the same VectorFloat&lt;?&gt;
* reference (for instance) for every requested ordinal. If you want to use those values across
* calls, you should make a copy.
*
* @param nodeId a valid ordinal, &ge; 0 and &lt; {@link #size()}.
*/
VectorFloat<?> getVector(int nodeId);

@Deprecated
default VectorFloat<?> vectorValue(int targetOrd) {
return getVector(targetOrd);
Expand All @@ -78,36 +65,16 @@ default void getVectorInto(int node, VectorFloat<?> destinationVector, int offse
destinationVector.copyFrom(getVector(node), 0, offset, dimension());
}

/**
* @return true iff the vector returned by `getVector` is shared. A shared vector will
* only be valid until the next call to getVector overwrites it.
*/
boolean isValueShared();

/**
* Creates a new copy of this {@link RandomAccessVectorValues}. This is helpful when you need to
* access different values at once, to avoid overwriting the underlying float vector returned by
* a shared {@link RandomAccessVectorValues#getVector}.
* <p>
* Un-shared implementations may simply return `this`.
*/
@Override
RandomAccessVectorValues copy();

/**
* Returns a supplier of thread-local copies of the RAVV.
*/
default Supplier<RandomAccessVectorValues> threadLocalSupplier() {
if (!isValueShared()) {
return () -> this;
}

if (this instanceof AutoCloseable) {
LOG.warning("RAVV is shared and implements AutoCloseable; threadLocalSupplier() may lead to leaks");
}
var tl = ExplicitThreadLocal.withInitial(this::copy);
return tl::get;
}

/**
* Convenience method to create an ExactScoreFunction for reranking. The resulting function is NOT thread-safe.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.util.ExplicitThreadLocal;

import java.util.function.Supplier;
import java.util.logging.Logger;

/**
* Common super-interface for random access to vectors by dense ordinal.
* <p>
* {@code V} is the vector element type — {@code VectorFloat<?>} for float32 vectors
* (see {@link RandomAccessVectorValues}) and {@code ByteSequence<?>} for int8 vectors
* (see {@link RandomAccessByteVectorValues}).
*/
public interface VectorValues<V> {
Logger LOG = Logger.getLogger(VectorValues.class.getName());

/** Return the number of vector values. */
int size();

/** Return the dimension of the returned vector values. */
int dimension();

/**
* Return the vector value indexed at the given ordinal.
* <p>
* For performance, implementations are free to re-use the same object across invocations.
* If you need to retain the value across calls, make a copy.
*
* @param nodeId a valid ordinal, &ge; 0 and &lt; {@link #size()}.
*/
V getVector(int nodeId);

/**
* @return true iff the vector returned by {@link #getVector} is shared across calls.
* A shared vector is only valid until the next call to {@link #getVector} overwrites it.
*/
boolean isValueShared();

/**
* Creates a new copy of this instance.
* Un-shared implementations may simply return {@code this}.
*/
VectorValues<V> copy();

/**
* Returns a supplier of thread-local copies of this instance.
*/
@SuppressWarnings("unchecked")
default Supplier<VectorValues<V>> threadLocalSupplier() {
if (!isValueShared()) {
return () -> this;
}

if (this instanceof AutoCloseable) {
LOG.warning("VectorValues is shared and implements AutoCloseable; threadLocalSupplier() may lead to leaks");
}
var tl = ExplicitThreadLocal.withInitial(this::copy);
return tl::get;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.github.jbellis.jvector.graph.disk.feature.Feature;
import io.github.jbellis.jvector.graph.disk.feature.FeatureId;
import io.github.jbellis.jvector.graph.disk.feature.FusedFeature;
import io.github.jbellis.jvector.graph.disk.feature.InlineByteVectors;
import io.github.jbellis.jvector.graph.disk.feature.InlineVectors;
import io.github.jbellis.jvector.graph.disk.feature.NVQ;
import io.github.jbellis.jvector.graph.disk.feature.SeparatedFeature;
Expand Down Expand Up @@ -386,6 +387,8 @@ public K build() throws IOException {
int dimension;
if (features.containsKey(FeatureId.INLINE_VECTORS)) {
dimension = ((InlineVectors) features.get(FeatureId.INLINE_VECTORS)).dimension();
} else if (features.containsKey(FeatureId.INLINE_BYTE_VECTORS)) {
dimension = ((InlineByteVectors) features.get(FeatureId.INLINE_BYTE_VECTORS)).dimension();
} else if (features.containsKey(FeatureId.NVQ_VECTORS)) {
dimension = ((NVQ) features.get(FeatureId.NVQ_VECTORS)).dimension();
} else if (features.containsKey(FeatureId.SEPARATED_VECTORS)) {
Expand Down
Loading
Loading