From b2ecaba104e510661af7e952c29963eee0eb7329 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Thu, 8 Oct 2026 13:39:47 -1000 Subject: [PATCH] Add source-specific CagraIndex builders Deprecates the existing single CagraIndex.Builder, which accepts incompatible parameters and sources, and replaces it with four builders, one per index source, each accepting only the parameters that apply to that source. The old Builder now rejects incompatible inputs that it previously ignored silently. When no index parameters are given, both the deprecated and the new builders now use the CagraIndexParams.Builder defaults. The new fromSerialized leaves the stream open, which is more idiomatic for Java, while the deprecated from(InputStream) still closes it. Fixes #2672 --- .../nvidia/cuvs/examples/CagraExample.java | 14 +- .../com/nvidia/cuvs/examples/HnswExample.java | 2 +- .../java-api-com-nvidia-cuvs-cagraindex.md | 450 +++++++++++++++++- ...va-api-com-nvidia-cuvs-spi-cuvsprovider.md | 2 +- fern/pages/java_guidelines.md | 20 +- ...-com-nvidia-cuvs-lucene-gpusearchparams.md | 3 +- ...lucene-api-com-nvidia-cuvs-lucene-utils.md | 3 +- fern/pages/neighbors/cagra.md | 41 +- fern/scripts/generate_api_reference.py | 54 ++- .../com/nvidia/cuvs/CagraIndexBenchmarks.java | 8 +- .../main/java/com/nvidia/cuvs/CagraIndex.java | 295 +++++++++++- .../nvidia/cuvs/internal/CagraIndexImpl.java | 449 ++++++++++++++++- .../nvidia/cuvs/CagraAceBuildAndSearchIT.java | 4 +- .../test/java/com/nvidia/cuvs/CagraBbqIT.java | 222 +++++++-- .../nvidia/cuvs/CagraBuildAndSearchIT.java | 41 +- .../com/nvidia/cuvs/CagraIndexBuilderIT.java | 268 +++++++++++ .../cuvs/CagraIndexDeprecatedBuilderIT.java | 256 ++++++++++ .../cuvs/CagraMultiThreadStabilityIT.java | 2 +- .../com/nvidia/cuvs/CagraRandomizedIT.java | 6 +- .../com/nvidia/cuvs/FilterBitsetHandleIT.java | 2 +- .../com/nvidia/cuvs/HnswBuildAndSearchIT.java | 2 +- .../com/nvidia/cuvs/HnswRandomizedIT.java | 4 +- .../cuvs/MultiPartitionCagraSearchIT.java | 2 +- .../cuvs/lucene/AcceleratedHNSWUtils.java | 2 +- .../cuvs/lucene/CuVS2510GPUVectorsReader.java | 4 +- .../cuvs/lucene/CuVS2510GPUVectorsWriter.java | 2 +- .../Lucene99AcceleratedHNSWVectorsWriter.java | 2 +- ...ratedHNSWBinaryQuantizedVectorsWriter.java | 2 +- ...ratedHNSWScalarQuantizedVectorsWriter.java | 2 +- 29 files changed, 1977 insertions(+), 187 deletions(-) create mode 100644 java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexBuilderIT.java create mode 100644 java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexDeprecatedBuilderIT.java diff --git a/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/CagraExample.java b/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/CagraExample.java index e6af0f12c7..c81acfab6b 100644 --- a/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/CagraExample.java +++ b/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/CagraExample.java @@ -53,18 +53,22 @@ public static void main(String[] args) throws Throwable { // Create the index with the dataset CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(vectors) + .fromDataset(vectors) .withIndexParams(indexParams) .build(); // Saving the index on to the disk. String indexFileName = UUID.randomUUID().toString() + ".cag"; - index.serialize(new FileOutputStream(indexFileName)); + try (FileOutputStream outputStream = new FileOutputStream(indexFileName)) { + index.serialize(outputStream); + } - // Loading a CAGRA index from disk. + // Loading a CAGRA index from disk. Loading reads the stream but leaves closing it to us. File indexFile = new File(indexFileName); - InputStream inputStream = new FileInputStream(indexFile); - CagraIndex loadedIndex = CagraIndex.newBuilder(resources).from(inputStream).build(); + CagraIndex loadedIndex; + try (InputStream inputStream = new FileInputStream(indexFile)) { + loadedIndex = CagraIndex.newBuilder(resources).fromSerialized(inputStream).build(); + } // Configure search parameters CagraSearchParams searchParams = new CagraSearchParams.Builder().build(); diff --git a/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/HnswExample.java b/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/HnswExample.java index 39b1e9dc3f..80c5158617 100644 --- a/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/HnswExample.java +++ b/examples/java/cuvs-java/src/main/java/com/nvidia/cuvs/examples/HnswExample.java @@ -72,7 +72,7 @@ public static void main(String[] args) throws Throwable { // Create the index with the dataset CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(vectors) + .fromDataset(vectors) .withIndexParams(indexParams) .build(); diff --git a/fern/pages/java_api/java-api-com-nvidia-cuvs-cagraindex.md b/fern/pages/java_api/java-api-com-nvidia-cuvs-cagraindex.md index c89f50b924..8be56f7271 100644 --- a/fern/pages/java_api/java-api-com-nvidia-cuvs-cagraindex.md +++ b/fern/pages/java_api/java-api-com-nvidia-cuvs-cagraindex.md @@ -388,7 +388,9 @@ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:300`_ static Builder newBuilder(CuVSResources cuvsResources) ``` -Creates a new Builder with an instance of `CuVSResources`. +Creates a new Builder with an instance of `CuVSResources`. Pick what the index is created +from with one of the `from*` methods of `Builder`, then call `build()` on the +builder it returns. **Parameters** @@ -400,9 +402,9 @@ Creates a new Builder with an instance of `CuVSResources`. | Type | Description | | --- | --- | -| `UnsupportedOperationException` | if the provider does not cuvs | +| `UnsupportedOperationException` | if cuVS is not available, for example without a GPU or without the native library | -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:308`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:311`_ ### merge @@ -428,7 +430,7 @@ A new merged CAGRA index | --- | --- | | `Throwable` | if an error occurs during the merge operation | -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:320`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:323`_ ### merge @@ -455,7 +457,7 @@ A new merged CAGRA index | --- | --- | | `Throwable` | if an error occurs during the merge operation | -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:332`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:335`_ ### merge @@ -491,7 +493,7 @@ A new merged CAGRA index | `IllegalArgumentException` | if `rowFilter` has a bit set beyond the last row, or if it is non-null but keeps no rows at all | | `Throwable` | if an error occurs during the merge operation | -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:356`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:359`_ ### isPaddedDataset @@ -517,16 +519,184 @@ copy it into padded storage it already occupies, and one that is not has to go t true when the rows are already padded the way CAGRA requires -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:384`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:387`_ + +### fromDataset + +```java +default FromDatasetBuilder fromDataset(CuVSMatrix dataset) +``` + +Builds the CAGRA graph from dense vectors. + +The index takes ownership of `dataset` when `FromDatasetBuilder#build()` +returns, and closes it when the index is closed. If `build()` throws, the caller still +owns it. + +**Parameters** + +| Name | Description | +| --- | --- | +| `dataset` | the vectors to index | + +**Returns** + +a builder for the optional inputs + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:427`_ + +### fromDataset + +```java +default FromDatasetBuilder fromDataset(float[][] vectors) +``` + +Builds the CAGRA graph from dense vectors held in a Java array. +`FromDatasetBuilder#build()` checks them and copies them into a matrix that the index +owns, so changes made to the array before then reach the index. + +**Parameters** + +| Name | Description | +| --- | --- | +| `vectors` | the vectors to index, one row per vector, all of the same length | + +**Returns** + +a builder for the optional inputs + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:439`_ + +### fromBbq + +```java +default FromBbqBuilder fromBbq(BbqQuantizer quantizer) +``` + +Builds the CAGRA graph from one BBQ encoding of the vectors, used on both sides of every +distance. Only nn-descent graph construction is supported. + +The index stores views over the quantizer's matrices rather than copying them, so they +must stay open for as long as the index is in use. Unless a dense dataset is attached with +`FromBbqBuilder#withDenseDataset(CuVSMatrix)`, the index can't be searched until +`CagraIndex#updateDataset(PaddedDatasetView)` or +`CagraIndex#updateDataset(PaddedDataset)` attaches one. + +**Parameters** + +| Name | Description | +| --- | --- | +| `quantizer` | the encoded vectors | + +**Returns** + +a builder for the optional inputs + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:456`_ + +### fromBbq + +```java +default FromBbqBuilder fromBbq(BbqQuantizer quantizer, BbqQuantizer other) +``` + +Builds the CAGRA graph from two BBQ encodings of the same vectors at different precisions. +They can be given in either order: cuVS uses the lower-precision one for the stored side of +every distance and the other one for the query side. The supported pairs of layouts are +`PACKED_1B` with `PACKED_4B`, `TRANSPOSED_2B` or `TRANSPOSED_4B`, and +`TRANSPOSED_2B` with `TRANSPOSED_4B`. + +Otherwise this behaves like `#fromBbq(BbqQuantizer)`, and both quantizers must stay +open for as long as the index is in use. + +**Parameters** + +| Name | Description | +| --- | --- | +| `quantizer` | one encoding of the vectors | +| `other` | the other encoding, with a different layout | + +**Returns** + +a builder for the optional inputs + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalArgumentException` | if both quantizers use the same layout | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:475`_ + +### fromGraph + +```java +default FromGraphBuilder fromGraph( CagraIndexParams.CuvsDistanceType metric, CuVSMatrix graph, CuVSMatrix dataset) +``` + +Creates an index around a graph built earlier and the dataset it was built from. No graph is +built. + +The index takes ownership of `dataset` when `FromGraphBuilder#build()` +returns. It never owns `graph`: a graph in host memory is copied, but one in device +memory is used in place, so it must stay open for as long as the index is in use. The graph +returned by `CagraIndex#getGraph()` is a view into its index's own device memory, so +passing it directly ties the new index to that one, which must then stay open too. Pass a +copy made with `CuVSMatrix#toHost()` to keep the two independent. + +**Parameters** + +| Name | Description | +| --- | --- | +| `metric` | the distance the graph was built for | +| `graph` | the graph, one row of neighbor indices per vector | +| `dataset` | the vectors the graph was built from, in device memory | + +**Returns** + +a builder for the optional inputs + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalArgumentException` | if `dataset` is not in device memory | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:496`_ + +### fromSerialized + +```java +default FromSerializedBuilder fromSerialized(InputStream inputStream) +``` + +Loads an index written by `CagraIndex#serialize(OutputStream)`, with its graph and +dataset. `FromSerializedBuilder#build()` reads `inputStream` to its end, but +closing it is left to the caller. The index owns the loaded dataset unless +`FromSerializedBuilder#withOutputDataset(DeserializeDataset)` hands it to the caller. + +**Parameters** + +| Name | Description | +| --- | --- | +| `inputStream` | the serialized index | + +**Returns** + +a builder for the optional inputs + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:510`_ ### from ```java -Builder from(InputStream inputStream) +@Deprecated(since = "26.12", forRemoval = true) Builder from(InputStream inputStream) ``` +**Deprecated.** Use `#fromSerialized(InputStream)`. + Sets an instance of InputStream typically used when index deserialization is -needed. +needed. Unlike `#fromSerialized(InputStream)`, `#build()` closes it. **Parameters** @@ -538,17 +708,20 @@ needed. an instance of this Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:401`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:522`_ ### from ```java -Builder from(InputStream inputStream, DeserializeDataset outDataset) +@Deprecated(since = "26.12", forRemoval = true) Builder from(InputStream inputStream, DeserializeDataset outDataset) ``` +**Deprecated.** Use `#fromSerialized(InputStream)` and `FromSerializedBuilder#withOutputDataset(DeserializeDataset)`. + Sets an input stream and an empty caller-owned output handle for explicit dataset deserialization. The concrete output type must match the dataset layout stored in the -serialized index. Keep `outDataset` alive while the built index is in use. +serialized index. Keep `outDataset` alive while the built index is in use. Unlike +`#fromSerialized(InputStream)`, `#build()` closes `inputStream`. **Parameters** @@ -561,25 +734,29 @@ serialized index. Keep `outDataset` alive while the built index is in use. an instance of this Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:412`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:537`_ ### from ```java -Builder from(CuVSMatrix graph) +@Deprecated(since = "26.12", forRemoval = true) Builder from(CuVSMatrix graph) ``` +**Deprecated.** Use `#fromGraph(CagraIndexParams.CuvsDistanceType, CuVSMatrix, CuVSMatrix)`, which takes the metric directly. + Sets a CAGRA graph instance to re-create an index from a previously built graph. -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:418`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:547`_ ### withDataset ```java -Builder withDataset(float[][] vectors) +@Deprecated(since = "26.12", forRemoval = true) Builder withDataset(float[][] vectors) ``` +**Deprecated.** Use `#fromDataset(float[][])`. + Sets the dataset vectors for building the `CagraIndex`. **Parameters** @@ -592,14 +769,16 @@ Sets the dataset vectors for building the `CagraIndex`. an instance of this Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:426`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:557`_ ### withDataset ```java -Builder withDataset(CuVSMatrix dataset) +@Deprecated(since = "26.12", forRemoval = true) Builder withDataset(CuVSMatrix dataset) ``` +**Deprecated.** Use `#fromDataset(CuVSMatrix)`. To create an index from a graph or from BBQ quantizers, pass the dataset to `#fromGraph(CagraIndexParams.CuvsDistanceType, CuVSMatrix, CuVSMatrix)` or `FromBbqBuilder#withDenseDataset(CuVSMatrix)`. + Sets the dataset for building the `CagraIndex`. The caller retains ownership until a build that uses this dataset returns successfully. @@ -617,14 +796,16 @@ with the caller. an instance of this Builder -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:439`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:574`_ ### withBbqDataset ```java -Builder withBbqDataset(BbqQuantizer... quantizers) +@Deprecated(since = "26.12", forRemoval = true) Builder withBbqDataset(BbqQuantizer... quantizers) ``` +**Deprecated.** Use `#fromBbq(BbqQuantizer)` or `#fromBbq(BbqQuantizer, BbqQuantizer)`. + Builds the graph from one or two encoded BBQ representations. An optional dense dataset supplied with `#withDataset(CuVSMatrix)` is attached before search; otherwise call `CagraIndex#updateDataset(PaddedDatasetView)` or @@ -635,14 +816,16 @@ stay open for as long as the index is in use. A dense dataset passed to `#withDataset(CuVSMatrix)` is owned by the index, as it is for a non-BBQ build, and is closed with it. -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:452`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:591`_ ### withIndexParams ```java -Builder withIndexParams(CagraIndexParams cagraIndexParameters) +@Deprecated(since = "26.12", forRemoval = true) Builder withIndexParams(CagraIndexParams cagraIndexParameters) ``` +**Deprecated.** Use `FromDatasetBuilder#withIndexParams(CagraIndexParams)` or `FromBbqBuilder#withIndexParams(CagraIndexParams)`. An index created from a graph takes its metric as an argument of `#fromGraph(CagraIndexParams.CuvsDistanceType, CuVSMatrix, CuVSMatrix)`. + Registers an instance of configured `CagraIndexParams` with this Builder. @@ -656,20 +839,235 @@ Builder. An instance of this Builder. -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:461`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:605`_ ### build ```java -CagraIndex build() throws Throwable +@Deprecated(since = "26.12", forRemoval = true) CagraIndex build() throws Throwable ``` -Builds and returns an instance of CagraIndex. +**Deprecated.** Call `build()` on the builder returned by one of the `from*` methods. + +Builds and returns an instance of CagraIndex. With a dataset alone, `#withIndexParams` +is optional, and without it the index is built with the defaults of +`CagraIndexParams.Builder`. With BBQ quantizers it is optional too, and without it the +index is built with those defaults and the quantizers' metric. With a graph it is required, +because the index takes its metric from it. With a stream to load it is not allowed, +because the loaded index has its own. **Returns** an instance of CagraIndex -_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:468`_ +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:620`_ + +### withIndexParams + +```java +FromDatasetBuilder withIndexParams(CagraIndexParams indexParams) +``` + +Sets the build parameters. Without this call the index is built with the defaults of +`CagraIndexParams.Builder`. + +**Parameters** + +| Name | Description | +| --- | --- | +| `indexParams` | the build parameters | + +**Returns** + +this builder + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if called more than once | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:643`_ + +### build + +```java +CagraIndex build() throws Throwable +``` + +Builds the index. A builder creates one index, so this can be called only once, +even if it fails. To try again, start a new builder with +`CagraIndex#newBuilder(CuVSResources)`. + +**Returns** + +the new index + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if `build()` was already called, whether or not it succeeded | +| `IllegalArgumentException` | if the array given to `Builder#fromDataset(float[][])` is empty or its rows differ in length | +| `Throwable` | if the build fails | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:657`_ + +### withDenseDataset + +```java +FromBbqBuilder withDenseDataset(CuVSMatrix dataset) +``` + +Attaches full-precision vectors once the graph is built, so that the index can be searched +straight away. The index takes ownership of `dataset` when `#build()` returns, +and closes it when the index is closed. If `build()` throws, the caller still owns it. + +**Parameters** + +| Name | Description | +| --- | --- | +| `dataset` | the full-precision vectors, in the same order as the encoded ones | + +**Returns** + +this builder + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if called more than once | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:675`_ + +### withIndexParams + +```java +FromBbqBuilder withIndexParams(CagraIndexParams indexParams) +``` + +Sets the build parameters. Their graph build algorithm must be +`CagraIndexParams.CagraGraphBuildAlgo#NN_DESCENT`, or +`CagraIndexParams.CagraGraphBuildAlgo#AUTO_SELECT`, which picks nn-descent, and their +metric must be the one the quantizers were encoded for. Without this call the index is built +with the defaults of `CagraIndexParams.Builder` and the quantizers' metric. + +**Parameters** + +| Name | Description | +| --- | --- | +| `indexParams` | the build parameters | + +**Returns** + +this builder + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalArgumentException` | if the parameters ask for another graph build algorithm, or for a metric other than the quantizers' | +| `IllegalStateException` | if called more than once | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:690`_ + +### build + +```java +CagraIndex build() throws Throwable +``` + +Builds the index. A builder creates one index, so this can be called only once, +even if it fails. To try again, start a new builder with +`CagraIndex#newBuilder(CuVSResources)`. + +**Returns** + +the new index + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if `build()` was already called, whether or not it succeeded | +| `Throwable` | if the build fails | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:702`_ + +### build + +```java +CagraIndex build() throws Throwable +``` + +Creates the index. A builder creates one index, so this can be called only once, +even if it fails. To try again, start a new builder with +`CagraIndex#newBuilder(CuVSResources)`. + +**Returns** + +the new index + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if `build()` was already called, whether or not it succeeded | +| `Throwable` | if the index can't be created | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:721`_ + +### withOutputDataset + +```java +FromSerializedBuilder withOutputDataset(DeserializeDataset outDataset) +``` + +Hands the loaded dataset to the caller instead of the index. `outDataset` must be +empty, and its type must match the layout of the dataset stored in the serialized index: a +`PaddedDataset` or a `StandardDataset`. The index uses the dataset in place, so +keep `outDataset` open for as long as the index is in use. + +**Parameters** + +| Name | Description | +| --- | --- | +| `outDataset` | an empty handle that receives the dataset | + +**Returns** + +this builder + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if called more than once | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:739`_ + +### build + +```java +CagraIndex build() throws Throwable +``` + +Loads the index. A builder creates one index, so this can be called only once, +even if it fails. To try again, start a new builder with +`CagraIndex#newBuilder(CuVSResources)`. + +**Returns** + +the loaded index + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if `build()` was already called, whether or not it succeeded | +| `Throwable` | if the index can't be loaded | + +_Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:751`_ _Source: `java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java:26`_ diff --git a/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md b/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md index 6230de4ee7..678c5fc1c7 100644 --- a/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md +++ b/fern/pages/java_api/java-api-com-nvidia-cuvs-spi-cuvsprovider.md @@ -336,7 +336,7 @@ CagraIndex mergeCagraIndexes(CagraIndex[] indexes, CagraIndexParams mergeParams, ``` Merges multiple CAGRA indexes into a single index, keeping only the rows selected by -`rowFilter`. See CagraIndexParams, BitSet) for the +`rowFilter`. See `CagraIndex#merge(CagraIndex[], CagraIndexParams, BitSet)` for the meaning of the filter. **Parameters** diff --git a/fern/pages/java_guidelines.md b/fern/pages/java_guidelines.md index 4c535562f0..d583999a9a 100644 --- a/fern/pages/java_guidelines.md +++ b/fern/pages/java_guidelines.md @@ -40,17 +40,21 @@ The Java bindings should call the NVIDIA cuVS C APIs rather than C++ or CUDA imp Use `AutoCloseable` for resources, matrices, indexes, temporary native allocations, and native handle wrappers. Examples and tests should use try-with-resources whenever practical: ```java -try (CuVSResources resources = CuVSResources.create(); - CuVSMatrix dataset = loadDatasetMatrix(); - CagraIndex index = - CagraIndex.newBuilder(resources) - .withDataset(dataset) - .withIndexParams(indexParams) - .build()) { - SearchResults results = index.search(query); +try (CuVSResources resources = CuVSResources.create()) { + // The index takes ownership of the dataset and closes it. + CuVSMatrix dataset = loadDatasetMatrix(); + try (CagraIndex index = + CagraIndex.newBuilder(resources) + .fromDataset(dataset) + .withIndexParams(indexParams) + .build()) { + SearchResults results = index.search(query); + } } ``` +An object that takes ownership of another, as an index does of the dataset it is built from, closes it, so the caller doesn't close it again. + When an implementation creates a native handle, make the matching destroy call visible in `close()`. Close resources in the reverse order they were acquired, and guard against double-close or use-after-close when an object can outlive a native allocation. ### Native Boundary diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams.md index 8b9608337d..0f51c2447e 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams.md @@ -147,8 +147,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUSearchParams. public int getBuildQuality() ``` -Get the build quality handed to cuVS' build heuristic. Only consulted under the \{@link -Strategy#HEURISTIC\} strategy. +Get the build quality handed to cuVS' build heuristic. Only consulted under the `Strategy#HEURISTIC` strategy. **Returns** diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-utils.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-utils.md index b992bee630..09d931559c 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-utils.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-utils.md @@ -22,8 +22,7 @@ static RuntimeException handleThrowable(Throwable t) throws IOException A utility method that rethrows known throwable types without changing their identity. -In particular, `Error` instances must not be converted to a \{@link -RuntimeException\}; callers rely on errors retaining their original type and stack trace. +In particular, `Error` instances must not be converted to a `RuntimeException`; callers rely on errors retaining their original type and stack trace. This method never returns normally; its return type exists solely so callers can write `throw handleThrowable(t);`, letting the compiler verify that the enclosing statement diff --git a/fern/pages/neighbors/cagra.md b/fern/pages/neighbors/cagra.md index fa1d04cb9b..20dea7ce39 100644 --- a/fern/pages/neighbors/cagra.md +++ b/fern/pages/neighbors/cagra.md @@ -70,8 +70,9 @@ index = cagra.build(index_params, dataset) ```java -try (CuVSResources resources = CuVSResources.create(); - CuVSMatrix dataset = loadDatasetMatrix()) { +try (CuVSResources resources = CuVSResources.create()) { + // The index takes ownership of the dataset and closes it. + CuVSMatrix dataset = loadDatasetMatrix(); CagraIndexParams indexParams = new CagraIndexParams.Builder() .withCagraGraphBuildAlgo(CagraGraphBuildAlgo.NN_DESCENT) @@ -80,7 +81,7 @@ try (CuVSResources resources = CuVSResources.create(); try (CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(indexParams) .build()) { // Use index for search or serialization. @@ -461,22 +462,23 @@ loaded_index = cagra.load("/tmp/cuvs-cagra.bin") ```java -try (CuVSResources resources = CuVSResources.create(); - CuVSMatrix dataset = loadDatasetMatrix(); - CagraIndex index = - CagraIndex.newBuilder(resources) - .withDataset(dataset) - .build()) { - - try (FileOutputStream output = new FileOutputStream("/tmp/cuvs-cagra.bin")) { +try (CuVSResources resources = CuVSResources.create()) { + // The index takes ownership of the dataset and closes it. + CuVSMatrix dataset = loadDatasetMatrix(); + try (CagraIndex index = + CagraIndex.newBuilder(resources) + .fromDataset(dataset) + .build(); + FileOutputStream output = new FileOutputStream("/tmp/cuvs-cagra.bin")) { index.serialize(output); } - try (FileInputStream input = new FileInputStream("/tmp/cuvs-cagra.bin")) { - CagraIndex loadedIndex = - CagraIndex.newBuilder(resources) - .from(input) - .build(); + try (FileInputStream input = new FileInputStream("/tmp/cuvs-cagra.bin"); + CagraIndex loadedIndex = + CagraIndex.newBuilder(resources) + .fromSerialized(input) + .build()) { + // Use loadedIndex for search. } } ``` @@ -673,8 +675,9 @@ import com.nvidia.cuvs.HnswQuery; import com.nvidia.cuvs.HnswSearchParams; import com.nvidia.cuvs.SearchResults; -try (CuVSResources resources = CuVSResources.create(); - CuVSMatrix dataset = loadDatasetMatrix()) { +try (CuVSResources resources = CuVSResources.create()) { + // The index takes ownership of the dataset and closes it. + CuVSMatrix dataset = loadDatasetMatrix(); int dim = (int) dataset.columns(); int M = 32; int efConstruction = 200; @@ -690,7 +693,7 @@ try (CuVSResources resources = CuVSResources.create(); try (CagraIndex cagraIndex = CagraIndex.newBuilder(resources) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(cagraParams) .build()) { HnswIndexParams hnswParams = diff --git a/fern/scripts/generate_api_reference.py b/fern/scripts/generate_api_reference.py index 3cf916d868..a115f05a00 100755 --- a/fern/scripts/generate_api_reference.py +++ b/fern/scripts/generate_api_reference.py @@ -239,6 +239,7 @@ class JavaDoc: params: list[DoxygenParam] = field(default_factory=list) returns: str = "" throws: list[DoxygenParam] = field(default_factory=list) + deprecated: str = "" @dataclass @@ -3083,6 +3084,7 @@ def generate_jvm_api_pages( member.doc.summary or member.doc.params or member.doc.returns + or member.doc.deprecated ): lines.append("") lines.extend([f"_Source: `{klass.source}:{member.line}`_", ""]) @@ -3166,18 +3168,21 @@ def parse_java_members(text: str, class_name: str) -> list[JavaMember]: members: list[JavaMember] = [] for match in COMMENT_RE.finditer(text): signature, line = java_signature_at(text, match.end()) - if "(" not in signature: + # Annotations stay in the signature on the page, but an argument list + # such as @Deprecated(since = "...") must not be taken for the member. + declaration = strip_java_annotations(signature) + if "(" not in declaration: continue - if re.search(r"\b(class|interface|enum|record)\b", signature): + if re.search(r"\b(class|interface|enum|record)\b", declaration): continue - if re.search(r"\b(if|for|while|switch|catch)\s*\(", signature): + if re.search(r"\b(if|for|while|switch|catch)\s*\(", declaration): continue # The pages advertise "Public Members"; drop explicitly private ones. # Package-private and interface members are left alone because an # interface method with no modifier is implicitly public. - if re.match(r"\s*private\b", signature): + if re.match(r"\s*private\b", declaration): continue - name_match = re.search(r"([A-Za-z_]\w*)\s*\(", signature) + name_match = re.search(r"([A-Za-z_]\w*)\s*\(", declaration) if not name_match: continue name = name_match.group(1) @@ -3188,6 +3193,13 @@ def parse_java_members(text: str, class_name: str) -> list[JavaMember]: return members +def strip_java_annotations(signature: str) -> str: + """Returns the declaration that follows the annotations on a member.""" + while match := re.match(r"@\w+(?:\s*\([^)]*\))?\s*", signature): + signature = signature[match.end() :] + return signature + + def clean_doxygen_comment(raw: str) -> str: if raw.lstrip().startswith("///"): lines = [re.sub(r"^\s*/// ?", "", line) for line in raw.splitlines()] @@ -4718,7 +4730,8 @@ def java_signature_at(text: str, start: int) -> tuple[str, int]: char = text[idx] update_depth(depth, char) if char in "{;" and not depth["("] and not depth["["]: - signature = text[start:idx].strip() + # A line comment after an annotation is not part of the declaration. + signature = re.sub(r"//[^\n]*", "", text[start:idx]).strip() signature = re.sub(r"\s+", " ", signature) return signature, line idx += 1 @@ -4745,6 +4758,14 @@ def parse_javadoc(raw: str) -> JavaDoc: lines = [ re.sub(r"^\s*\* ?", "", line).rstrip() for line in body.splitlines() ] + # Join the inline tags that wrap onto another line, so that each one is + # cleaned up as a whole. A
{@code ...} block is left alone: its line
+    # breaks matter, and the code in it can contain braces.
+    lines = re.sub(
+        r"(?)\{@[^}]*\}",
+        lambda m: re.sub(r"\s*\n\s*", " ", m.group(0)),
+        "\n".join(lines),
+    ).splitlines()
     doc = JavaDoc()
     summary_lines: list[str] = []
     active: DoxygenParam | None = None
@@ -4777,9 +4798,16 @@ def parse_javadoc(raw: str) -> JavaDoc:
             active = None
             active_kind = "return"
             continue
+        deprecated_match = re.match(r"@deprecated\s*(.*)", stripped)
+        if deprecated_match:
+            doc.deprecated = deprecated_match.group(1).strip()
+            active = None
+            active_kind = "deprecated"
+            continue
         if stripped.startswith("@"):
+            # A tag this page doesn't render, along with its continuation lines.
             active = None
-            active_kind = ""
+            active_kind = "ignored"
             continue
         if active is not None and active_kind in {"param", "throws"}:
             active.description = append_doxygen_line(
@@ -4787,16 +4815,20 @@ def parse_javadoc(raw: str) -> JavaDoc:
             )
         elif active_kind == "return":
             doc.returns = append_doxygen_line(doc.returns, stripped)
-        else:
+        elif active_kind == "deprecated":
+            doc.deprecated = append_doxygen_line(doc.deprecated, stripped)
+        elif active_kind != "ignored":
             summary_lines.append(stripped)
     doc.summary = "\n".join(trim_blank_lines(summary_lines)).strip()
     return doc
 
 
 def clean_javadoc_text(text: str) -> str:
-    text = re.sub(r"\{@code\s+([^}]+)\}", r"`\1`", text)
+    text = re.sub(r"\{@code\s+([^}]+?)\s*\}", r"`\1`", text)
+    # A method's parameter list may contain spaces, so it belongs to the
+    # target rather than starting the label.
     text = re.sub(
-        r"\{@link\s+([^}\s]+)(?:\s+([^}]+))?\}",
+        r"\{@link\s+([^}\s(]+(?:\([^)]*\))?)(?:\s+([^}]+))?\}",
         lambda m: m.group(2) or f"`{m.group(1)}`",
         text,
     )
@@ -4808,6 +4840,8 @@ def clean_javadoc_text(text: str) -> str:
 
 def render_javadoc(doc: JavaDoc) -> list[str]:
     lines: list[str] = []
+    if doc.deprecated:
+        lines.extend([f"**Deprecated.** {escape_text(doc.deprecated)}", ""])
     if doc.summary:
         lines.extend(escape_text(line) for line in doc.summary.splitlines())
         lines.append("")
diff --git a/java/benchmarks/src/main/java/com/nvidia/cuvs/CagraIndexBenchmarks.java b/java/benchmarks/src/main/java/com/nvidia/cuvs/CagraIndexBenchmarks.java
index d128c30aa0..c927ebf30b 100644
--- a/java/benchmarks/src/main/java/com/nvidia/cuvs/CagraIndexBenchmarks.java
+++ b/java/benchmarks/src/main/java/com/nvidia/cuvs/CagraIndexBenchmarks.java
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
+ * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
 
@@ -111,7 +111,7 @@ public void testIndexingAndSerializeToFile() throws Throwable {
 
             // Create the index with the dataset
             try (CagraIndex index = CagraIndex.newBuilder(resources)
-                .withDataset(arrayDataset)
+                .fromDataset(arrayDataset)
                 .withIndexParams(indexParams)
                 .build()) {
 
@@ -142,7 +142,7 @@ public void testIndexingFromHeap(Blackhole blackhole) throws Throwable {
 
             // Create the index with the dataset
             CagraIndex index = CagraIndex.newBuilder(resources)
-                .withDataset(arrayDataset)
+                .fromDataset(arrayDataset)
                 .withIndexParams(indexParams)
                 .build();
             blackhole.consume(index);
@@ -163,7 +163,7 @@ public void testIndexingFromMemorySegment(Blackhole blackhole) throws Throwable
 
             // Create the index with the dataset
             CagraIndex index = CagraIndex.newBuilder(resources)
-                .withDataset(fromMemorySegment(memorySegmentDataset, size, dims))
+                .fromDataset(fromMemorySegment(memorySegmentDataset, size, dims))
                 .withIndexParams(indexParams)
                 .build();
             blackhole.consume(index);
diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java
index 27be666aac..876014aac2 100644
--- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java
+++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java
@@ -301,10 +301,13 @@ default void serializeToHNSW(OutputStream outputStream, Path tempFile) throws Th
   CuVSResources getCuVSResources();
 
   /**
-   * Creates a new Builder with an instance of {@link CuVSResources}.
+   * Creates a new Builder with an instance of {@link CuVSResources}. Pick what the index is created
+   * from with one of the {@code from*} methods of {@link Builder}, then call {@code build()} on the
+   * builder it returns.
    *
    * @param cuvsResources an instance of {@link CuVSResources}
-   * @throws UnsupportedOperationException if the provider does not cuvs
+   * @throws UnsupportedOperationException if cuVS is not available, for example without a GPU or
+   *     without the native library
    */
   static Builder newBuilder(CuVSResources cuvsResources) {
     Objects.requireNonNull(cuvsResources);
@@ -388,34 +391,161 @@ static boolean isPaddedDataset(CuVSMatrix dataset) {
   }
 
   /**
-   * Builder helps configure and create an instance of {@link CagraIndex}.
+   * Builder helps configure and create an instance of {@link CagraIndex} from a variety of sources.
+   *
+   * 

Call one of the {@code from*} methods on the builder returned by + * {@link CagraIndex#newBuilder(CuVSResources)}. Each one takes the inputs that kind of index + * requires and returns a builder for the optional ones, whose {@code build()} creates the index: + * + *

{@code
+   * CagraIndex index =
+   *     CagraIndex.newBuilder(resources).fromDataset(dataset).withIndexParams(params).build();
+   * CagraIndex loaded = CagraIndex.newBuilder(resources).fromSerialized(inputStream).build();
+   * }
+ * + *

A builder creates one index: a second {@code from*} call, or a {@code from*} call combined + * with the deprecated setters, throws {@link IllegalStateException}. The builders the + * {@code from*} methods return take each input once. + * + *

Each {@link com.nvidia.cuvs.spi.CuVSProvider} returns its own implementation of this + * interface from {@link com.nvidia.cuvs.spi.CuVSProvider#newCagraIndexBuilder(CuVSResources)}. + * The {@code from*} methods are {@code default} methods that throw + * {@link UnsupportedOperationException}, so that implementations written before they existed + * still compile; the built-in provider implements all of them. */ interface Builder { + /** + * Builds the CAGRA graph from dense vectors. + * + *

The index takes ownership of {@code dataset} when {@link FromDatasetBuilder#build()} + * returns, and closes it when the index is closed. If {@code build()} throws, the caller still + * owns it. + * + * @param dataset the vectors to index + * @return a builder for the optional inputs + */ + default FromDatasetBuilder fromDataset(CuVSMatrix dataset) { + throw notImplemented("fromDataset"); + } + + /** + * Builds the CAGRA graph from dense vectors held in a Java array. + * {@link FromDatasetBuilder#build()} checks them and copies them into a matrix that the index + * owns, so changes made to the array before then reach the index. + * + * @param vectors the vectors to index, one row per vector, all of the same length + * @return a builder for the optional inputs + */ + default FromDatasetBuilder fromDataset(float[][] vectors) { + throw notImplemented("fromDataset"); + } + + /** + * Builds the CAGRA graph from one BBQ encoding of the vectors, used on both sides of every + * distance. Only nn-descent graph construction is supported. + * + *

The index stores views over the quantizer's matrices rather than copying them, so they + * must stay open for as long as the index is in use. Unless a dense dataset is attached with + * {@link FromBbqBuilder#withDenseDataset(CuVSMatrix)}, the index can't be searched until + * {@link CagraIndex#updateDataset(PaddedDatasetView)} or + * {@link CagraIndex#updateDataset(PaddedDataset)} attaches one. + * + * @param quantizer the encoded vectors + * @return a builder for the optional inputs + */ + default FromBbqBuilder fromBbq(BbqQuantizer quantizer) { + throw notImplemented("fromBbq"); + } + + /** + * Builds the CAGRA graph from two BBQ encodings of the same vectors at different precisions. + * They can be given in either order: cuVS uses the lower-precision one for the stored side of + * every distance and the other one for the query side. The supported pairs of layouts are + * {@code PACKED_1B} with {@code PACKED_4B}, {@code TRANSPOSED_2B} or {@code TRANSPOSED_4B}, and + * {@code TRANSPOSED_2B} with {@code TRANSPOSED_4B}. + * + *

Otherwise this behaves like {@link #fromBbq(BbqQuantizer)}, and both quantizers must stay + * open for as long as the index is in use. + * + * @param quantizer one encoding of the vectors + * @param other the other encoding, with a different layout + * @return a builder for the optional inputs + * @throws IllegalArgumentException if both quantizers use the same layout + */ + default FromBbqBuilder fromBbq(BbqQuantizer quantizer, BbqQuantizer other) { + throw notImplemented("fromBbq"); + } + + /** + * Creates an index around a graph built earlier and the dataset it was built from. No graph is + * built. + * + *

The index takes ownership of {@code dataset} when {@link FromGraphBuilder#build()} + * returns. It never owns {@code graph}: a graph in host memory is copied, but one in device + * memory is used in place, so it must stay open for as long as the index is in use. The graph + * returned by {@link CagraIndex#getGraph()} is a view into its index's own device memory, so + * passing it directly ties the new index to that one, which must then stay open too. Pass a + * copy made with {@link CuVSMatrix#toHost()} to keep the two independent. + * + * @param metric the distance the graph was built for + * @param graph the graph, one row of neighbor indices per vector + * @param dataset the vectors the graph was built from, in device memory + * @return a builder for the optional inputs + * @throws IllegalArgumentException if {@code dataset} is not in device memory + */ + default FromGraphBuilder fromGraph( + CagraIndexParams.CuvsDistanceType metric, CuVSMatrix graph, CuVSMatrix dataset) { + throw notImplemented("fromGraph"); + } + + /** + * Loads an index written by {@link CagraIndex#serialize(OutputStream)}, with its graph and + * dataset. {@link FromSerializedBuilder#build()} reads {@code inputStream} to its end, but + * closing it is left to the caller. The index owns the loaded dataset unless + * {@link FromSerializedBuilder#withOutputDataset(DeserializeDataset)} hands it to the caller. + * + * @param inputStream the serialized index + * @return a builder for the optional inputs + */ + default FromSerializedBuilder fromSerialized(InputStream inputStream) { + throw notImplemented("fromSerialized"); + } + /** * Sets an instance of InputStream typically used when index deserialization is - * needed. + * needed. Unlike {@link #fromSerialized(InputStream)}, {@link #build()} closes it. * * @param inputStream an instance of {@link InputStream} * @return an instance of this Builder + * @deprecated Use {@link #fromSerialized(InputStream)}. */ + @Deprecated(since = "26.12", forRemoval = true) Builder from(InputStream inputStream); /** * Sets an input stream and an empty caller-owned output handle for explicit dataset * deserialization. The concrete output type must match the dataset layout stored in the - * serialized index. Keep {@code outDataset} alive while the built index is in use. + * serialized index. Keep {@code outDataset} alive while the built index is in use. Unlike + * {@link #fromSerialized(InputStream)}, {@link #build()} closes {@code inputStream}. * * @param inputStream an instance of {@link InputStream} * @param outDataset an empty {@link PaddedDataset} or {@link StandardDataset} * @return an instance of this Builder + * @deprecated Use {@link #fromSerialized(InputStream)} and + * {@link FromSerializedBuilder#withOutputDataset(DeserializeDataset)}. */ + @Deprecated(since = "26.12", forRemoval = true) Builder from(InputStream inputStream, DeserializeDataset outDataset); /** * Sets a CAGRA graph instance to re-create an index from a * previously built graph. + * + * @deprecated Use {@link #fromGraph(CagraIndexParams.CuvsDistanceType, CuVSMatrix, + * CuVSMatrix)}, which takes the metric directly. */ + @Deprecated(since = "26.12", forRemoval = true) Builder from(CuVSMatrix graph); /** @@ -423,7 +553,9 @@ interface Builder { * * @param vectors a two-dimensional float array * @return an instance of this Builder + * @deprecated Use {@link #fromDataset(float[][])}. */ + @Deprecated(since = "26.12", forRemoval = true) Builder withDataset(float[][] vectors); /** @@ -436,7 +568,11 @@ interface Builder { * * @param dataset a {@link CuVSMatrix} object containing the vectors * @return an instance of this Builder + * @deprecated Use {@link #fromDataset(CuVSMatrix)}. To create an index from a graph or from + * BBQ quantizers, pass the dataset to {@link #fromGraph(CagraIndexParams.CuvsDistanceType, + * CuVSMatrix, CuVSMatrix)} or {@link FromBbqBuilder#withDenseDataset(CuVSMatrix)}. */ + @Deprecated(since = "26.12", forRemoval = true) Builder withDataset(CuVSMatrix dataset); /** @@ -449,7 +585,11 @@ interface Builder { * stay open for as long as the index is in use. A dense dataset passed to * {@link #withDataset(CuVSMatrix)} is owned by the index, as it is for a non-BBQ build, and is * closed with it. + * + * @deprecated Use {@link #fromBbq(BbqQuantizer)} or {@link #fromBbq(BbqQuantizer, + * BbqQuantizer)}. */ + @Deprecated(since = "26.12", forRemoval = true) Builder withBbqDataset(BbqQuantizer... quantizers); /** @@ -458,13 +598,156 @@ interface Builder { * * @param cagraIndexParameters An instance of CagraIndexParams. * @return An instance of this Builder. + * @deprecated Use {@link FromDatasetBuilder#withIndexParams(CagraIndexParams)} or + * {@link FromBbqBuilder#withIndexParams(CagraIndexParams)}. An index created from a graph + * takes its metric as an argument of {@link #fromGraph(CagraIndexParams.CuvsDistanceType, + * CuVSMatrix, CuVSMatrix)}. */ + @Deprecated(since = "26.12", forRemoval = true) Builder withIndexParams(CagraIndexParams cagraIndexParameters); /** - * Builds and returns an instance of CagraIndex. + * Builds and returns an instance of CagraIndex. With a dataset alone, {@link #withIndexParams} + * is optional, and without it the index is built with the defaults of + * {@link CagraIndexParams.Builder}. With BBQ quantizers it is optional too, and without it the + * index is built with those defaults and the quantizers' metric. With a graph it is required, + * because the index takes its metric from it. With a stream to load it is not allowed, + * because the loaded index has its own. * * @return an instance of CagraIndex + * @deprecated Call {@code build()} on the builder returned by one of the {@code from*} + * methods. + */ + @Deprecated(since = "26.12", forRemoval = true) + CagraIndex build() throws Throwable; + + private UnsupportedOperationException notImplemented(String method) { + return new UnsupportedOperationException( + getClass().getName() + " does not implement CagraIndex.Builder." + method); + } + } + + /** + * Builds a {@link CagraIndex} from dense vectors. Returned by + * {@link Builder#fromDataset(CuVSMatrix)} and {@link Builder#fromDataset(float[][])}. + */ + interface FromDatasetBuilder { + + /** + * Sets the build parameters. Without this call the index is built with the defaults of + * {@link CagraIndexParams.Builder}. + * + * @param indexParams the build parameters + * @return this builder + * @throws IllegalStateException if called more than once + */ + FromDatasetBuilder withIndexParams(CagraIndexParams indexParams); + + /** + * Builds the index. A builder creates one index, so this can be called only once, + * even if it fails. To try again, start a new builder with + * {@link CagraIndex#newBuilder(CuVSResources)}. + * + * @return the new index + * @throws IllegalStateException if {@code build()} was already called, whether or not it + * succeeded + * @throws IllegalArgumentException if the array given to {@link Builder#fromDataset(float[][])} + * is empty or its rows differ in length + * @throws Throwable if the build fails + */ + CagraIndex build() throws Throwable; + } + + /** + * Builds a {@link CagraIndex} from BBQ-encoded vectors. Returned by + * {@link Builder#fromBbq(BbqQuantizer)} and {@link Builder#fromBbq(BbqQuantizer, BbqQuantizer)}. + */ + interface FromBbqBuilder { + + /** + * Attaches full-precision vectors once the graph is built, so that the index can be searched + * straight away. The index takes ownership of {@code dataset} when {@link #build()} returns, + * and closes it when the index is closed. If {@code build()} throws, the caller still owns it. + * + * @param dataset the full-precision vectors, in the same order as the encoded ones + * @return this builder + * @throws IllegalStateException if called more than once + */ + FromBbqBuilder withDenseDataset(CuVSMatrix dataset); + + /** + * Sets the build parameters. Their graph build algorithm must be + * {@link CagraIndexParams.CagraGraphBuildAlgo#NN_DESCENT}, or + * {@link CagraIndexParams.CagraGraphBuildAlgo#AUTO_SELECT}, which picks nn-descent, and their + * metric must be the one the quantizers were encoded for. Without this call the index is built + * with the defaults of {@link CagraIndexParams.Builder} and the quantizers' metric. + * + * @param indexParams the build parameters + * @return this builder + * @throws IllegalArgumentException if the parameters ask for another graph build algorithm, or + * for a metric other than the quantizers' + * @throws IllegalStateException if called more than once + */ + FromBbqBuilder withIndexParams(CagraIndexParams indexParams); + + /** + * Builds the index. A builder creates one index, so this can be called only once, + * even if it fails. To try again, start a new builder with + * {@link CagraIndex#newBuilder(CuVSResources)}. + * + * @return the new index + * @throws IllegalStateException if {@code build()} was already called, whether or not it + * succeeded + * @throws Throwable if the build fails + */ + CagraIndex build() throws Throwable; + } + + /** + * Creates a {@link CagraIndex} around a graph built earlier. Returned by + * {@link Builder#fromGraph(CagraIndexParams.CuvsDistanceType, CuVSMatrix, CuVSMatrix)}. + */ + interface FromGraphBuilder { + + /** + * Creates the index. A builder creates one index, so this can be called only once, + * even if it fails. To try again, start a new builder with + * {@link CagraIndex#newBuilder(CuVSResources)}. + * + * @return the new index + * @throws IllegalStateException if {@code build()} was already called, whether or not it + * succeeded + * @throws Throwable if the index can't be created + */ + CagraIndex build() throws Throwable; + } + + /** + * Loads a serialized {@link CagraIndex}. Returned by {@link Builder#fromSerialized(InputStream)}. + */ + interface FromSerializedBuilder { + + /** + * Hands the loaded dataset to the caller instead of the index. {@code outDataset} must be + * empty, and its type must match the layout of the dataset stored in the serialized index: a + * {@link PaddedDataset} or a {@link StandardDataset}. The index uses the dataset in place, so + * keep {@code outDataset} open for as long as the index is in use. + * + * @param outDataset an empty handle that receives the dataset + * @return this builder + * @throws IllegalStateException if called more than once + */ + FromSerializedBuilder withOutputDataset(DeserializeDataset outDataset); + + /** + * Loads the index. A builder creates one index, so this can be called only once, + * even if it fails. To try again, start a new builder with + * {@link CagraIndex#newBuilder(CuVSResources)}. + * + * @return the loaded index + * @throws IllegalStateException if {@code build()} was already called, whether or not it + * succeeded + * @throws Throwable if the index can't be loaded */ CagraIndex build() throws Throwable; } diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java index 491c5213fc..bac4a7f2c3 100644 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java @@ -90,11 +90,8 @@ private CagraIndexImpl( * * @param inputStream an instance of stream to read the index bytes from * @param resources an instance of {@link CuVSResources} + * @param outDataset the caller's handle for the loaded dataset, or null to let the index own it */ - private CagraIndexImpl(InputStream inputStream, CuVSResources resources) throws Throwable { - this(inputStream, resources, null); - } - private CagraIndexImpl( InputStream inputStream, CuVSResources resources, CagraIndex.DeserializeDataset outDataset) throws Throwable { @@ -834,6 +831,7 @@ private IndexReference fromGraph( CuVSMatrixInternal dataset) { try (var localArena = Arena.ofConfined()) { var index = createCagraIndex(); + boolean success = false; try (var resourcesAccess = resources.access()) { long cuvsRes = resourcesAccess.handle(); @@ -843,6 +841,12 @@ private IndexReference fromGraph( checkCuVSError( cuvsCagraIndexFromArgs(cuvsRes, metric.value, graphTensor, datasetTensor, index), "cuvsCagraIndexFromArgs"); + success = true; + } finally { + // Ownership transfers only on success, so on any other exit the index is ours. + if (!success) { + quietly(() -> checkCuVSError(cuvsCagraIndexDestroy(index), "cuvsCagraIndexDestroy")); + } } return new IndexReference(index, dataset); } @@ -894,7 +898,7 @@ public void serializeToHNSW(OutputStream outputStream, Path tempFile, int buffer * Gets an instance of {@link IndexReference} by deserializing a CAGRA index * using an {@link InputStream}. * - * @param inputStream an instance of {@link InputStream} + * @param inputStream an instance of {@link InputStream}, read to its end but left open * @return an instance of {@link IndexReference} */ private IndexReference deserialize( @@ -915,8 +919,7 @@ private IndexReference deserialize( MemorySegment index = createCagraIndex(); MemorySegment dataset = MemorySegment.NULL; - try (inputStream; - var outputStream = Files.newOutputStream(tmpIndexFile); + try (var outputStream = Files.newOutputStream(tmpIndexFile); var arena = Arena.ofConfined()) { inputStream.transferTo(outputStream); @@ -1336,15 +1339,34 @@ private static CloseableRMMAllocation allocateRowFilter( } /** - * Builder helps configure and create an instance of {@link CagraIndex}. + * The {@link CagraIndex.Builder} of the built-in provider. + * + *

Its {@code from*} methods hand out the builders below. The deprecated setters keep their + * old behavior, last call winning, and {@link #build()} hands what they recorded to the same + * builders, after rejecting any input the chosen mode would ignore. */ + @SuppressWarnings("removal") // implements the deprecated setters public static class Builder implements CagraIndex.Builder { + private static final String LOADING = "loading a serialized index with from(InputStream)"; + private static final String FROM_GRAPH = "creating an index from a graph with from(CuVSMatrix)"; + + private final CuVSResources cuvsResources; + + /** The {@code from*} method that started an index, if any. */ + private String started; + + /** The first deprecated method called, if any. */ + private String firstDeprecatedCall; + private CuVSMatrix dataset; + + /** Whether {@link #dataset} was converted here from an array, which makes it this builder's. */ + private boolean datasetFromArray; + private InputStream inputStream; private CagraIndex.DeserializeDataset outDataset; private CagraIndexParams cagraIndexParams; - private final CuVSResources cuvsResources; private CuVSMatrix graph; private BbqQuantizer[] bbqQuantizers; @@ -1352,8 +1374,100 @@ public Builder(CuVSResources cuvsResources) { this.cuvsResources = cuvsResources; } + @Override + public CagraIndex.FromDatasetBuilder fromDataset(CuVSMatrix dataset) { + return start( + "fromDataset", + new FromDatasetBuilderImpl( + cuvsResources, Objects.requireNonNull(dataset, "dataset"), null)); + } + + @Override + public CagraIndex.FromDatasetBuilder fromDataset(float[][] vectors) { + return start( + "fromDataset", + new FromDatasetBuilderImpl( + cuvsResources, null, Objects.requireNonNull(vectors, "vectors"))); + } + + @Override + public CagraIndex.FromBbqBuilder fromBbq(BbqQuantizer quantizer) { + var quantizers = List.of(Objects.requireNonNull(quantizer, "quantizer")); + return start("fromBbq", new FromBbqBuilderImpl(cuvsResources, quantizers)); + } + + @Override + public CagraIndex.FromBbqBuilder fromBbq(BbqQuantizer quantizer, BbqQuantizer other) { + var quantizers = + List.of( + Objects.requireNonNull(quantizer, "quantizer"), + Objects.requireNonNull(other, "other")); + return start("fromBbq", new FromBbqBuilderImpl(cuvsResources, quantizers)); + } + + @Override + public CagraIndex.FromGraphBuilder fromGraph( + CagraIndexParams.CuvsDistanceType metric, CuVSMatrix graph, CuVSMatrix dataset) { + return start( + "fromGraph", + new FromGraphBuilderImpl( + cuvsResources, + Objects.requireNonNull(metric, "metric"), + Objects.requireNonNull(graph, "graph"), + Objects.requireNonNull(dataset, "dataset"))); + } + + @Override + public CagraIndex.FromSerializedBuilder fromSerialized(InputStream inputStream) { + return start( + "fromSerialized", + new FromSerializedBuilderImpl( + cuvsResources, Objects.requireNonNull(inputStream, "inputStream"))); + } + + /** + * Hands out a builder that has already checked its inputs, so that a rejected call leaves this + * builder free to start another index. + */ + private T start(String method, T builder) { + if (started != null) { + throw new IllegalStateException( + "This builder already started an index with " + + started + + "; call CagraIndex.newBuilder again for another one"); + } + if (firstDeprecatedCall != null) { + throw new IllegalStateException( + method + + " can't be combined with the deprecated " + + firstDeprecatedCall + + "; pass every input to " + + method + + " and the builder it returns"); + } + started = method; + return builder; + } + + private void deprecatedCall(String method) { + if (started != null) { + throw new IllegalStateException( + "The deprecated " + + method + + " can't follow " + + started + + "; pass every input to " + + started + + " and the builder it returns"); + } + if (firstDeprecatedCall == null) { + firstDeprecatedCall = method; + } + } + @Override public Builder from(InputStream inputStream) { + deprecatedCall("from(InputStream)"); this.inputStream = inputStream; this.outDataset = null; return this; @@ -1361,63 +1475,358 @@ public Builder from(InputStream inputStream) { @Override public Builder from(InputStream inputStream, CagraIndex.DeserializeDataset outDataset) { + Objects.requireNonNull(outDataset); + deprecatedCall("from(InputStream, DeserializeDataset)"); this.inputStream = inputStream; - this.outDataset = Objects.requireNonNull(outDataset); + this.outDataset = outDataset; return this; } @Override public Builder from(CuVSMatrix graph) { + deprecatedCall("from(CuVSMatrix)"); this.graph = graph; return this; } @Override public Builder withDataset(float[][] vectors) { - this.dataset = CuVSMatrix.ofArray(vectors); + deprecatedCall("withDataset(float[][])"); + // Copied right away, as it always was, so later changes to the array don't reach the index. + replaceDataset(CuVSMatrix.ofArray(vectors), true); return this; } @Override public Builder withDataset(CuVSMatrix dataset) { - this.dataset = dataset; + deprecatedCall("withDataset(CuVSMatrix)"); + replaceDataset(dataset, false); return this; } + private void replaceDataset(CuVSMatrix dataset, boolean fromArray) { + // Once replaced, a matrix this builder converted and no index took can't be used, so it is + // closed. + if (datasetFromArray) { + quietly(this.dataset::close); + } + this.dataset = dataset; + this.datasetFromArray = fromArray; + } + @Override public Builder withBbqDataset(BbqQuantizer... quantizers) { + deprecatedCall("withBbqDataset"); this.bbqQuantizers = quantizers == null ? null : quantizers.clone(); return this; } @Override public Builder withIndexParams(CagraIndexParams cagraIndexParameters) { + deprecatedCall("withIndexParams"); this.cagraIndexParams = cagraIndexParameters; return this; } + /** + * Builds what the deprecated setters describe, through the builders the {@code from*} methods + * return. The mode is picked as it always has been: a stream wins over a graph, which wins over + * BBQ quantizers, which win over a plain dataset. The inputs the picked mode ignores used to be + * dropped silently, and are rejected now. Missing parameters default to those of {@link + * CagraIndexParams.Builder}, as for the new builders: the native defaults this used to fall back + * on ask for IVF-PQ without any IVF-PQ parameters, which could not build. + */ @Override - public CagraIndexImpl build() throws Throwable { + public CagraIndex build() throws Throwable { + deprecatedCall("build()"); + // A failed build keeps the dataset, so that the builder can be retried, as it always could. + CagraIndex index = buildFromRecordedInputs(); + // The index owns a converted matrix now, so replacing it must not close it. + datasetFromArray = false; + return index; + } + + private CagraIndex buildFromRecordedInputs() throws Throwable { if (inputStream != null) { - return outDataset == null - ? new CagraIndexImpl(inputStream, cuvsResources) - : new CagraIndexImpl(inputStream, cuvsResources, outDataset); + // Unlike fromSerialized, the deprecated from(InputStream) closes the stream, as it always + // has, including when an input is rejected. If only closing fails, the caller gets the + // exception instead of the index, so the index is released, again as it always was. + var stream = inputStream; + CagraIndex index = null; + try (stream) { + rejectIfSet(graph, "from(CuVSMatrix)", LOADING, "fromSerialized(inputStream)"); + rejectIfSet(dataset, "withDataset", LOADING, "fromSerialized(inputStream)"); + rejectIfSet(bbqQuantizers, "withBbqDataset", LOADING, "fromSerialized(inputStream)"); + rejectIfSet(cagraIndexParams, "withIndexParams", LOADING, "fromSerialized(inputStream)"); + var builder = new FromSerializedBuilderImpl(cuvsResources, stream); + if (outDataset != null) { + builder.withOutputDataset(outDataset); + } + index = builder.build(); + } catch (Throwable t) { + if (index != null) { + quietly(index::close); + } + throw t; + } + return index; } else if (graph != null) { + // withIndexParams stays allowed: the metric comes from it, and the rest of the parameters + // can't be told apart from their defaults. + rejectIfSet( + bbqQuantizers, "withBbqDataset", FROM_GRAPH, "fromGraph(metric, graph, dataset)"); if (cagraIndexParams == null || dataset == null) { throw new IllegalArgumentException( "In order to reconstruct a CAGRA index from a graph, " + "you must specify the original dataset and the metric used."); } - return new CagraIndexImpl( - cagraIndexParams.getCuvsDistanceType(), graph, dataset, cuvsResources); + return new FromGraphBuilderImpl( + cuvsResources, cagraIndexParams.getCuvsDistanceType(), graph, dataset) + .build(); } else if (bbqQuantizers != null) { - return new CagraIndexImpl(cagraIndexParams, dataset, bbqQuantizers, cuvsResources); + if (bbqQuantizers.length < 1 || bbqQuantizers.length > 2) { + throw new IllegalArgumentException("BBQ build requires one or two quantizers"); + } + var builder = new FromBbqBuilderImpl(cuvsResources, List.of(bbqQuantizers)); + if (dataset != null) { + builder.withDenseDataset(dataset); + } + if (cagraIndexParams != null) { + builder.withIndexParams(cagraIndexParams); + } + return builder.build(); } else if (dataset != null) { - return new CagraIndexImpl(cagraIndexParams, dataset, cuvsResources); + var builder = new FromDatasetBuilderImpl(cuvsResources, dataset, null); + if (cagraIndexParams != null) { + builder.withIndexParams(cagraIndexParams); + } + return builder.build(); } else { throw new IllegalArgumentException("dataset must be provided"); } } + + private static void rejectIfSet(Object value, String setter, String mode, String replacement) { + if (value != null) { + throw new IllegalArgumentException( + setter + + " has no effect when " + + mode + + ". Remove it, or use CagraIndex.newBuilder(resources)." + + replacement + + ", which takes only the inputs that apply."); + } + } + } + + /** Rejects a second call of an optional setter, whose value is never null once set. */ + private static void checkNotSet(Object current, String setter) { + if (current != null) { + throw new IllegalStateException( + setter + " was already called; a builder takes each input once"); + } + } + + private static void checkFirstBuild(boolean built) { + if (built) { + throw new IllegalStateException("build() was already called; a builder creates one index"); + } + } + + private static final class FromDatasetBuilderImpl implements CagraIndex.FromDatasetBuilder { + private final CuVSResources resources; + private final CuVSMatrix dataset; + private final float[][] vectors; + private CagraIndexParams indexParams; + private boolean built; + + /** Takes exactly one of {@code dataset} and {@code vectors}. */ + FromDatasetBuilderImpl(CuVSResources resources, CuVSMatrix dataset, float[][] vectors) { + this.resources = resources; + this.dataset = dataset; + this.vectors = vectors; + } + + @Override + public CagraIndex.FromDatasetBuilder withIndexParams(CagraIndexParams indexParams) { + Objects.requireNonNull(indexParams, "indexParams"); + checkNotSet(this.indexParams, "withIndexParams"); + this.indexParams = indexParams; + return this; + } + + @Override + public CagraIndex build() throws Throwable { + checkFirstBuild(built); + built = true; + CagraIndexParams params = + indexParams != null ? indexParams : new CagraIndexParams.Builder().build(); + if (vectors == null) { + return new CagraIndexImpl(params, dataset, resources); + } + // Checked and converted here rather than in fromDataset, so that nothing is allocated for a + // builder that is never built, and the check sees the array as it is copied. + checkVectors(vectors); + CuVSMatrix matrix = CuVSMatrix.ofArray(vectors); + boolean success = false; + try { + var index = new CagraIndexImpl(params, matrix, resources); + success = true; + return index; + } finally { + // The index owns the matrix on success; on failure it is ours, and the caller never saw it. + if (!success) { + quietly(matrix::close); + } + } + } + + /** + * Rejects an array that can't be copied. The copy takes its width from the first row, so a + * shorter row would fail with an index error and a longer one would be silently truncated. + */ + private static void checkVectors(float[][] vectors) { + if (vectors.length == 0) { + throw new IllegalArgumentException("vectors should not be empty"); + } + int dimensions = Objects.requireNonNull(vectors[0], "vectors[0]").length; + for (int i = 1; i < vectors.length; i++) { + if (Objects.requireNonNull(vectors[i], "vectors[" + i + "]").length != dimensions) { + throw new IllegalArgumentException( + "Every vector must have the same length, but vectors[0] has " + + dimensions + + " and vectors[" + + i + + "] has " + + vectors[i].length); + } + } + } + } + + private static final class FromBbqBuilderImpl implements CagraIndex.FromBbqBuilder { + private final CuVSResources resources; + private final List quantizers; + private CuVSMatrix denseDataset; + private CagraIndexParams indexParams; + private boolean built; + + FromBbqBuilderImpl(CuVSResources resources, List quantizers) { + // cuVS looks the quantizers of a pair up by layout, so a second one with the same layout + // could never be used. + if (quantizers.size() == 2 + && quantizers.get(0).getLayout() == quantizers.get(1).getLayout()) { + throw new IllegalArgumentException( + "Both quantizers use the " + + quantizers.get(0).getLayout() + + " layout, but a pair of BBQ encodings needs two different layouts"); + } + this.resources = resources; + this.quantizers = quantizers; + } + + @Override + public CagraIndex.FromBbqBuilder withDenseDataset(CuVSMatrix dataset) { + Objects.requireNonNull(dataset, "dataset"); + checkNotSet(this.denseDataset, "withDenseDataset"); + this.denseDataset = dataset; + return this; + } + + @Override + public CagraIndex.FromBbqBuilder withIndexParams(CagraIndexParams indexParams) { + Objects.requireNonNull(indexParams, "indexParams"); + checkNotSet(this.indexParams, "withIndexParams"); + // AUTO_SELECT resolves to nn-descent for a BBQ dataset. + var algo = indexParams.getCagraGraphBuildAlgo(); + if (algo != CagraGraphBuildAlgo.NN_DESCENT && algo != CagraGraphBuildAlgo.AUTO_SELECT) { + throw new IllegalArgumentException( + "A BBQ build supports only NN_DESCENT graph construction, but the parameters ask for " + + algo); + } + var metric = quantizers.get(0).getMetric(); + if (indexParams.getCuvsDistanceType() != metric) { + throw new IllegalArgumentException( + "The parameters ask for the " + + indexParams.getCuvsDistanceType() + + " metric, but the quantizers were encoded for " + + metric); + } + this.indexParams = indexParams; + return this; + } + + @Override + public CagraIndex build() throws Throwable { + checkFirstBuild(built); + built = true; + CagraIndexParams params = + indexParams != null + ? indexParams + : new CagraIndexParams.Builder().withMetric(quantizers.get(0).getMetric()).build(); + return new CagraIndexImpl( + params, denseDataset, quantizers.toArray(BbqQuantizer[]::new), resources); + } + } + + private static final class FromGraphBuilderImpl implements CagraIndex.FromGraphBuilder { + private final CuVSResources resources; + private final CagraIndexParams.CuvsDistanceType metric; + private final CuVSMatrix graph; + private final CuVSMatrix dataset; + private boolean built; + + FromGraphBuilderImpl( + CuVSResources resources, + CagraIndexParams.CuvsDistanceType metric, + CuVSMatrix graph, + CuVSMatrix dataset) { + // cuvsCagraIndexFromArgs only takes a device dataset; catch a host one before native code. + if (!(dataset instanceof CuVSDeviceMatrix)) { + throw new IllegalArgumentException( + "Creating an index from a graph needs the dataset in device memory; copy it there with" + + " toDevice(resources)"); + } + this.resources = resources; + this.metric = metric; + this.graph = graph; + this.dataset = dataset; + } + + @Override + public CagraIndex build() throws Throwable { + checkFirstBuild(built); + built = true; + return new CagraIndexImpl(metric, graph, dataset, resources); + } + } + + private static final class FromSerializedBuilderImpl implements CagraIndex.FromSerializedBuilder { + private final CuVSResources resources; + private final InputStream inputStream; + private CagraIndex.DeserializeDataset outDataset; + private boolean built; + + FromSerializedBuilderImpl(CuVSResources resources, InputStream inputStream) { + this.resources = resources; + this.inputStream = inputStream; + } + + @Override + public CagraIndex.FromSerializedBuilder withOutputDataset( + CagraIndex.DeserializeDataset outDataset) { + Objects.requireNonNull(outDataset, "outDataset"); + checkNotSet(this.outDataset, "withOutputDataset"); + this.outDataset = outDataset; + return this; + } + + @Override + public CagraIndex build() throws Throwable { + checkFirstBuild(built); + built = true; + return new CagraIndexImpl(inputStream, resources, outDataset); + } } /** diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraAceBuildAndSearchIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraAceBuildAndSearchIT.java index 1089c5b7f1..602dc4383b 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraAceBuildAndSearchIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraAceBuildAndSearchIT.java @@ -111,7 +111,7 @@ public void testAceInMemoryBuild() throws Throwable { // Build the index with ACE try (CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(indexParams) .build(); var hostVectors = CuVSMatrix.ofArray(dataset); @@ -179,7 +179,7 @@ public void testAceDiskBasedBuild() throws Throwable { // Build the index with ACE in disk mode try (CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(indexParams) .build()) { diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBbqIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBbqIT.java index 31e659cb96..bb0a65b1e0 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBbqIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBbqIT.java @@ -21,7 +21,7 @@ import org.slf4j.LoggerFactory; /** - * CAGRA indices built from BBQ quantizers via {@link CagraIndex.Builder#withBbqDataset}. + * CAGRA indices built from BBQ quantizers via {@link CagraIndex.Builder#fromBbq(BbqQuantizer)}. * *

{@code gpuSearchBaselineWithoutBbq} is a control: the same search over a graph plain CAGRA * built from the same vectors. If it regresses alongside the BBQ case, look at the search @@ -99,13 +99,8 @@ public void gpuSearchBaselineWithoutBbq() throws Throwable { CuVSMatrix dense = newDenseDataset(resources); try (CagraIndex index = CagraIndex.newBuilder(resources) - .withIndexParams( - new CagraIndexParams.Builder() - .withMetric(CuvsDistanceType.L2Expanded) - .withGraphDegree(32) - .withIntermediateGraphDegree(64) - .build()) - .withDataset(dense) + .fromDataset(dense) + .withIndexParams(params()) .build(); CuVSMatrix queryVectors = CuVSMatrix.ofArray(queries)) { CagraQuery query = @@ -155,6 +150,32 @@ public void graphOnlyBbqBuildProducesUsableGraph() throws Throwable { } } + /** + * Without parameters a BBQ build uses the quantizers' metric, not the L2Expanded default of + * {@link CagraIndexParams.Builder}: nn-descent requires its metric to match the quantizers', so + * an inner-product quantizer only builds if the metric was carried over. The rest of the + * parameters keep their defaults, which shows in the graph degree. + */ + @Test + public void buildsWithTheQuantizersMetricByDefault() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create(); + Quantizer quantizer = newQuantizer(resources, CuvsDistanceType.InnerProduct); + CagraIndex index = CagraIndex.newBuilder(resources).fromBbq(quantizer.quantizer()).build(); + CuVSMatrix graph = index.getGraph()) { + assertEquals("one adjacency row per vector", ROWS, graph.size()); + assertEquals( + "the default graph degree", + new CagraIndexParams.Builder().build().getGraphDegree(), + graph.columns()); + for (int[] neighbors : readAdjacency(graph)) { + for (int neighbor : neighbors) { + assertTrue( + "neighbor id " + neighbor + " is out of range", neighbor >= 0 && neighbor < ROWS); + } + } + } + } + /** * A dense dataset handed to the builder is owned by the resulting index, exactly as it is for a * non-BBQ build, so closing the index releases it. Host matrices are arena-backed, which makes @@ -213,24 +234,65 @@ public void quantizerMustRejectMissingCentroidNormSq() throws Throwable { } } - /** A build takes one or two encoded representations; anything else is a usage error. */ + /** + * Two encodings of the same vectors build in either order, because cuVS picks the role of each + * one from its layout. The 2-bit encoding decodes to the same values as the 1-bit one, so the + * asymmetric distances equal the symmetric 1-bit ones and recall is held to the same floor. + */ + @Test + public void asymmetricPairBuildsInEitherOrder() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create(); + Quantizer oneBit = newQuantizer(resources); + Quantizer twoBit = newTransposed2bQuantizer(resources); + CuVSMatrix queryVectors = CuVSMatrix.ofArray(queries)) { + for (boolean oneBitFirst : new boolean[] {true, false}) { + var first = oneBitFirst ? oneBit : twoBit; + var second = oneBitFirst ? twoBit : oneBit; + // Owned by the index once built, so it is not closed here. + CuVSMatrix dense = newDenseDataset(resources); + try (CagraIndex index = + CagraIndex.newBuilder(resources) + .fromBbq(first.quantizer(), second.quantizer()) + .withDenseDataset(dense) + .withIndexParams(params()) + .build()) { + CagraQuery query = + new CagraQuery.Builder(resources) + .withTopK(TOP_K) + .withSearchParams(searchParams()) + .withQueryVectors(queryVectors) + .withMapping(SearchResults.IDENTITY_MAPPING) + .build(); + assertRecall( + first.quantizer().getLayout() + " + " + second.quantizer().getLayout(), + index.search(query)); + } + } + } + } + + /** + * The deprecated varargs setter is the only way left to pass a number of quantizers other than + * one or two, so the check behind it stays covered until the setter is removed. + */ @Test - public void buildRejectsAnUnusableQuantizerCount() throws Throwable { + @SuppressWarnings("removal") + public void deprecatedSetterRejectsAnUnusableQuantizerCount() throws Throwable { try (CuVSResources resources = CheckedCuVSResources.create(); Quantizer quantizer = newQuantizer(resources)) { var q = quantizer.quantizer(); assertThrows( "a build needs at least one representation", IllegalArgumentException.class, - () -> bbqIndex(resources, new BbqQuantizer[] {}, null)); + () -> CagraIndex.newBuilder(resources).withBbqDataset().build()); assertThrows( "three representations are more than the native side accepts", IllegalArgumentException.class, - () -> bbqIndex(resources, new BbqQuantizer[] {q, q, q}, null)); + () -> CagraIndex.newBuilder(resources).withBbqDataset(q, q, q).build()); assertThrows( "a null representation should be caught before it reaches native code", NullPointerException.class, - () -> bbqIndex(resources, new BbqQuantizer[] {q, null}, null)); + () -> CagraIndex.newBuilder(resources).withBbqDataset(q, null).build()); } } @@ -262,8 +324,7 @@ public void hostQuantizerComponentIsRejected() throws Throwable { .build(); var failure = assertThrows( - IllegalArgumentException.class, - () -> bbqIndex(resources, new BbqQuantizer[] {withHostComponent}, null)); + IllegalArgumentException.class, () -> bbqIndex(resources, withHostComponent, null)); assertTrue( "the message should name the offending component, but was: " + failure.getMessage(), failure.getMessage().contains("rowNorm")); @@ -351,7 +412,7 @@ public void explicitlyContiguousCodesMatrixIsAccepted() throws Throwable { .withMetric(CuvsDistanceType.L2Expanded) .withCentroidNormSq(q.getCentroidNormSq()) .build(); - try (CagraIndex index = bbqIndex(resources, new BbqQuantizer[] {withStridedCodes}, null); + try (CagraIndex index = bbqIndex(resources, withStridedCodes, null); CuVSMatrix graph = index.getGraph()) { assertEquals("one adjacency row per vector", ROWS, graph.size()); } @@ -397,11 +458,11 @@ public void paddedCodesMatrixMustBeRejectedClearly() throws Throwable { IllegalArgumentException.class, () -> CagraIndex.newBuilder(resources) + .fromBbq(padded) .withIndexParams( new CagraIndexParams.Builder() .withMetric(CuvsDistanceType.L2Expanded) .build()) - .withBbqDataset(padded) .build()); assertTrue( "the message should name the offending component, but was: " + failure.getMessage(), @@ -477,25 +538,27 @@ private static float squaredDistance(float[] a, float[] b) { private static CagraIndex bbqIndex( CuVSResources resources, Quantizer quantizer, CuVSMatrix dataset) throws Throwable { - return bbqIndex(resources, new BbqQuantizer[] {quantizer.quantizer()}, dataset); + return bbqIndex(resources, quantizer.quantizer(), dataset); } private static CagraIndex bbqIndex( - CuVSResources resources, BbqQuantizer[] quantizers, CuVSMatrix dataset) throws Throwable { - var params = - new CagraIndexParams.Builder() - .withMetric(CuvsDistanceType.L2Expanded) - .withGraphDegree(32) - .withIntermediateGraphDegree(64) - .build(); - var builder = - CagraIndex.newBuilder(resources).withIndexParams(params).withBbqDataset(quantizers); + CuVSResources resources, BbqQuantizer quantizer, CuVSMatrix dataset) throws Throwable { + var builder = CagraIndex.newBuilder(resources).fromBbq(quantizer).withIndexParams(params()); if (dataset != null) { - builder = builder.withDataset(dataset); + builder.withDenseDataset(dataset); } return builder.build(); } + /** The same graph shape for every build here, with and without BBQ. */ + private static CagraIndexParams params() { + return new CagraIndexParams.Builder() + .withMetric(CuvsDistanceType.L2Expanded) + .withGraphDegree(32) + .withIntermediateGraphDegree(64) + .build(); + } + private float[][] randomVectors(int count) { float[][] vectors = new float[count][DIM]; for (float[] vector : vectors) { @@ -551,6 +614,67 @@ public void close() { * searches score candidates against the dense dataset rather than against the codes. */ private Quantizer newQuantizer(CuVSResources resources) { + return newQuantizer(resources, CuvsDistanceType.L2Expanded); + } + + private Quantizer newQuantizer(CuVSResources resources, CuvsDistanceType metric) { + OneBitCodes oneBit = encodeOneBit(metric); + float[] delta = new float[ROWS]; + float[] sumDelta = new float[ROWS]; + for (int i = 0; i < ROWS; i++) { + // One step for 1-bit codes, so each component lands on 0 or 1. + delta[i] = oneBit.upper()[i] - oneBit.lower()[i]; + sumDelta[i] = delta[i] * oneBit.sums()[i]; + } + return upload( + resources, + BbqQuantizer.CodeLayout.PACKED_1B, + oneBit.codes(), + oneBit.sums(), + delta, + sumDelta, + oneBit); + } + + /** + * A TRANSPOSED_2B encoding of {@link #dataset}, derived from the 1-bit one instead of quantized + * afresh. On the same interval, the 2-bit code {@code 3c} decodes to exactly the value the 1-bit + * code {@code c} does, because the 2-bit step is a third of the 1-bit one. Both bit planes of + * {@code 3c} equal {@code c}, so each row is the 1-bit row twice, plane after plane, as + * {@code pack_codes} in the C++ reference lays them out. + */ + private Quantizer newTransposed2bQuantizer(CuVSResources resources) { + OneBitCodes oneBit = encodeOneBit(CuvsDistanceType.L2Expanded); + byte[][] codes = new byte[ROWS][2 * CODE_BYTES]; + int[] sums = new int[ROWS]; + float[] delta = new float[ROWS]; + float[] sumDelta = new float[ROWS]; + for (int i = 0; i < ROWS; i++) { + System.arraycopy(oneBit.codes()[i], 0, codes[i], 0, CODE_BYTES); + System.arraycopy(oneBit.codes()[i], 0, codes[i], CODE_BYTES, CODE_BYTES); + sums[i] = 3 * oneBit.sums()[i]; + delta[i] = (oneBit.upper()[i] - oneBit.lower()[i]) / 3; + sumDelta[i] = delta[i] * sums[i]; + } + return upload( + resources, BbqQuantizer.CodeLayout.TRANSPOSED_2B, codes, sums, delta, sumDelta, oneBit); + } + + /** The host-side result of {@link #encodeOneBit(CuvsDistanceType)}. */ + private record OneBitCodes( + CuvsDistanceType metric, + byte[][] codes, + float[] lower, + float[] upper, + float[] corrections, + int[] sums, + float[] rowNorms, + float[] centroid, + float centroidNormSq) {} + + private OneBitCodes encodeOneBit(CuvsDistanceType metric) { + boolean euclidean = + metric == CuvsDistanceType.L2Expanded || metric == CuvsDistanceType.L2SqrtExpanded; float[] centroid = new float[DIM]; for (float[] row : dataset) { for (int d = 0; d < DIM; d++) { @@ -569,8 +693,6 @@ private Quantizer newQuantizer(CuVSResources resources) { float[] corrections = new float[ROWS]; int[] sums = new int[ROWS]; float[] rowNorms = new float[ROWS]; - float[] delta = new float[ROWS]; - float[] sumDelta = new float[ROWS]; for (int i = 0; i < ROWS; i++) { float[] centred = new float[DIM]; @@ -578,10 +700,12 @@ private Quantizer newQuantizer(CuVSResources resources) { float min = Float.MAX_VALUE; float max = -Float.MAX_VALUE; float centredNormSq = 0.0f; + float centroidDot = 0.0f; double mean = 0.0; double var = 0.0; for (int d = 0; d < DIM; d++) { origNormSq += dataset[i][d] * dataset[i][d]; + centroidDot += dataset[i][d] * centroid[d]; centred[d] = dataset[i][d] - centroid[d]; min = Math.min(min, centred[d]); max = Math.max(max, centred[d]); @@ -595,7 +719,6 @@ private Quantizer newQuantizer(CuVSResources resources) { // kMinimumMseGrid[0] from the C++ reference, i.e. the 1-bit row. float a = (float) clamp(-0.798 * stddev + mean, min, max); float b = (float) clamp(0.798 * stddev + mean, min, max); - // One step for 1-bit codes, so each component lands on 0 or 1. float step = b - a; int sum = 0; for (int d = 0; d < DIM; d++) { @@ -606,17 +729,28 @@ private Quantizer newQuantizer(CuVSResources resources) { lower[i] = a; upper[i] = b; - // L2Expanded is euclidean, so the correction carries the centred norm. - corrections[i] = centredNormSq; + // As in the C++ reference: the centred norm for a euclidean metric, otherwise the original + // vector's dot product with the centroid. + corrections[i] = euclidean ? centredNormSq : centroidDot; sums[i] = sum; rowNorms[i] = origNormSq; - delta[i] = b - a; - sumDelta[i] = delta[i] * sum; } + return new OneBitCodes( + metric, codes, lower, upper, corrections, sums, rowNorms, centroid, centroidNormSq); + } + /** Uploads one encoding to device memory; the components that don't depend on it come shared. */ + private static Quantizer upload( + CuVSResources resources, + BbqQuantizer.CodeLayout layout, + byte[][] codes, + int[] sums, + float[] delta, + float[] sumDelta, + OneBitCodes shared) { List owned = new ArrayList<>(); var codesBuilder = - CuVSMatrix.deviceBuilder(resources, ROWS, CODE_BYTES, CuVSMatrix.DataType.BYTE); + CuVSMatrix.deviceBuilder(resources, ROWS, codes[0].length, CuVSMatrix.DataType.BYTE); for (byte[] row : codes) { codesBuilder.addVector(row); } @@ -624,17 +758,17 @@ private Quantizer newQuantizer(CuVSResources resources) { BbqQuantizer quantizer = new BbqQuantizer.Builder() .withCodes(track(owned, codesBuilder.build())) - .withLowerIntervals(track(owned, deviceVector(resources, lower))) - .withUpperIntervals(track(owned, deviceVector(resources, upper))) - .withAdditionalCorrections(track(owned, deviceVector(resources, corrections))) + .withLowerIntervals(track(owned, deviceVector(resources, shared.lower()))) + .withUpperIntervals(track(owned, deviceVector(resources, shared.upper()))) + .withAdditionalCorrections(track(owned, deviceVector(resources, shared.corrections()))) .withQuantizedComponentSums(track(owned, deviceVector(resources, sums))) - .withCentroid(track(owned, deviceRow(resources, centroid))) + .withCentroid(track(owned, deviceRow(resources, shared.centroid()))) .withDequantDelta(track(owned, deviceVector(resources, delta))) .withDequantSumDelta(track(owned, deviceVector(resources, sumDelta))) - .withRowNorm(track(owned, deviceVector(resources, rowNorms))) - .withLayout(BbqQuantizer.CodeLayout.PACKED_1B) - .withMetric(CuvsDistanceType.L2Expanded) - .withCentroidNormSq(centroidNormSq) + .withRowNorm(track(owned, deviceVector(resources, shared.rowNorms()))) + .withLayout(layout) + .withMetric(shared.metric()) + .withCentroidNormSq(shared.centroidNormSq()) .build(); return new Quantizer(quantizer, owned); } diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBuildAndSearchIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBuildAndSearchIT.java index a4f08c47d4..4be89a0615 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBuildAndSearchIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraBuildAndSearchIT.java @@ -180,7 +180,11 @@ public void testDeserializeReturnsCallerOwnedStandardDataset() throws Throwable try (var indexDataset = index.makePaddedDataset(deviceVectors); var outDataset = new CagraIndex.StandardDataset(); var inputStream = Files.newInputStream(indexPath); - var loadedIndex = CagraIndex.newBuilder(resources).from(inputStream, outDataset).build(); + var loadedIndex = + CagraIndex.newBuilder(resources) + .fromSerialized(inputStream) + .withOutputDataset(outDataset) + .build(); // The deserialized standard dataset is caller-owned but not searchable as-is. var loadedDataset = loadedIndex.makePaddedDataset(deviceVectors)) { assertTrue(outDataset.isPresent()); @@ -517,12 +521,7 @@ public void testReconstructIndexFromGraph() throws Throwable { try (var reconstructedIndex = CagraIndex.newBuilder(resources) - .from(graph) - .withDataset(deviceVectors) - .withIndexParams( - new CagraIndexParams.Builder() - .withMetric(CuvsDistanceType.L2Expanded) - .build()) + .fromGraph(CuvsDistanceType.L2Expanded, graph, deviceVectors) .build(); var indexDataset = index.makePaddedDataset(deviceVectors); var reconstructedDataset = reconstructedIndex.makePaddedDataset(deviceVectors)) { @@ -642,7 +641,7 @@ public void testPrefilteringReducesResults() throws Throwable { try (CuVSResources resources = CheckedCuVSResources.create(); CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(indexParams) .build(); var hostVectors = CuVSMatrix.ofArray(dataset); @@ -759,7 +758,7 @@ private CagraIndex indexOnce(CuVSMatrix dataset, CuVSResources resources) throws // Create the index with the dataset return CagraIndex.newBuilder(resources) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(indexParams) .build(); } @@ -776,7 +775,7 @@ private Path serializeOnce(CagraIndex index) throws Throwable { private CagraIndex deserializeOnce(Path indexFilePath, CuVSResources resources) throws Throwable { // Loading a CAGRA index from disk. try (var inputStream = Files.newInputStream(indexFilePath)) { - return CagraIndex.newBuilder(resources).from(inputStream).build(); + return CagraIndex.newBuilder(resources).fromSerialized(inputStream).build(); } } @@ -936,14 +935,14 @@ public void testMergingIndexes() throws Throwable { log.trace("Building first index..."); CagraIndex index1 = CagraIndex.newBuilder(resources) - .withDataset(vector1) + .fromDataset(vector1) .withIndexParams(indexParams) .build(); log.trace("Building second index..."); CagraIndex index2 = CagraIndex.newBuilder(resources) - .withDataset(vector2) + .fromDataset(vector2) .withIndexParams(indexParams) .build(); @@ -992,7 +991,7 @@ public void testMergingIndexes() throws Throwable { try (var inputStream = Files.newInputStream(indexFile); CagraIndex loadedMergedIndex = - CagraIndex.newBuilder(resources).from(inputStream).build()) { + CagraIndex.newBuilder(resources).fromSerialized(inputStream).build()) { SearchResults resultsFromLoaded = loadedMergedIndex.search(query); assertEquals(expectedResults, resultsFromLoaded.getResults()); @@ -1056,12 +1055,12 @@ public void testFilteredMerge() throws Throwable { CagraIndex index1 = CagraIndex.newBuilder(resources) - .withDataset(vector1) + .fromDataset(vector1) .withIndexParams(indexParams) .build(); CagraIndex index2 = CagraIndex.newBuilder(resources) - .withDataset(vector2) + .fromDataset(vector2) .withIndexParams(indexParams) .build(); @@ -1159,7 +1158,7 @@ public void testFilteredMergeRejectsOversizedFilter() throws Throwable { try (CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(vectors) + .fromDataset(vectors) .withIndexParams(indexParams) .build()) { // The index holds two rows, so bit 2 is one row past the end of the merge. @@ -1205,12 +1204,12 @@ public void testFailedMergeLeavesTheInputsUsable() throws Throwable { try (CagraIndex index1 = CagraIndex.newBuilder(resources) - .withDataset(vector1) + .fromDataset(vector1) .withIndexParams(indexParams) .build(); CagraIndex index2 = CagraIndex.newBuilder(resources) - .withDataset(vector2) + .fromDataset(vector2) .withIndexParams(indexParams) .build()) { @@ -1298,14 +1297,14 @@ public void testMergeStrategies() throws Throwable { log.trace("Building first index..."); CagraIndex index1 = CagraIndex.newBuilder(resources) - .withDataset(vector1) + .fromDataset(vector1) .withIndexParams(indexParams) .build(); log.trace("Building second index..."); CagraIndex index2 = CagraIndex.newBuilder(resources) - .withDataset(vector2) + .fromDataset(vector2) .withIndexParams(indexParams) .build(); @@ -1366,7 +1365,7 @@ public void testMergeStrategies() throws Throwable { try (var physicalInputStream = Files.newInputStream(physicalIndexFile); CagraIndex loadedPhysicalIndex = - CagraIndex.newBuilder(resources).from(physicalInputStream).build()) { + CagraIndex.newBuilder(resources).fromSerialized(physicalInputStream).build()) { SearchResults resultsFromLoadedPhysical = loadedPhysicalIndex.search(query); assertEquals( diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexBuilderIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexBuilderIT.java new file mode 100644 index 0000000000..4f4023d752 --- /dev/null +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexBuilderIT.java @@ -0,0 +1,268 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs; + +import static com.carrotsearch.randomizedtesting.RandomizedTest.assumeTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.nvidia.cuvs.CagraIndexParams.CagraGraphBuildAlgo; +import com.nvidia.cuvs.CagraIndexParams.CuvsDistanceType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Proxy; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * The checks of the builders returned by {@link CagraIndex.Builder}'s {@code from*} methods. Most + * of them fail before anything reaches native code; building, searching and loading are covered by + * the other CAGRA tests. + */ +@RunWith(RandomizedRunner.class) +public class CagraIndexBuilderIT extends CuVSTestCase { + + private static final int ROWS = 256; + private static final int DIM = 16; + + private float[][] vectors; + + @Before + public void setup() { + assumeTrue("not supported on " + System.getProperty("os.name"), isLinuxSupportedArch()); + initializeRandom(); + vectors = generateData(random, ROWS, DIM); + } + + @Test + public void aBuilderBuildsOnce() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var datasetBuilder = CagraIndex.newBuilder(resources).fromDataset(vectors); + try (CagraIndex index = datasetBuilder.build()) { + assertEquals(ROWS, index.size()); + } + assertThrows(IllegalStateException.class, datasetBuilder::build); + } + } + + @Test + public void aBuilderStartsOneIndex() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var builder = CagraIndex.newBuilder(resources); + builder.fromDataset(vectors); + var failure = + assertThrows( + IllegalStateException.class, + () -> builder.fromSerialized(new ByteArrayInputStream(new byte[0]))); + assertTrue(failure.getMessage(), failure.getMessage().contains("fromDataset")); + } + } + + @Test + public void fromBbqRejectsTwoQuantizersWithTheSameLayout() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var builder = CagraIndex.newBuilder(resources); + var failure = + assertThrows( + IllegalArgumentException.class, + () -> + builder.fromBbq( + quantizer(BbqQuantizer.CodeLayout.PACKED_1B), + quantizer(BbqQuantizer.CodeLayout.PACKED_1B))); + assertTrue(failure.getMessage(), failure.getMessage().contains("PACKED_1B")); + // The rejected call didn't use the builder up. + builder.fromDataset(vectors); + } + } + + @Test + public void fromBbqRejectsParametersForAnotherGraphBuildAlgorithm() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var bbqBuilder = + CagraIndex.newBuilder(resources).fromBbq(quantizer(BbqQuantizer.CodeLayout.PACKED_1B)); + var ivfPq = + new CagraIndexParams.Builder() + .withCagraGraphBuildAlgo(CagraGraphBuildAlgo.IVF_PQ) + .build(); + var failure = + assertThrows(IllegalArgumentException.class, () -> bbqBuilder.withIndexParams(ivfPq)); + assertTrue(failure.getMessage(), failure.getMessage().contains("IVF_PQ")); + } + } + + @Test + public void rejectsAnInputGivenTwice() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var params = new CagraIndexParams.Builder().build(); + var datasetBuilder = CagraIndex.newBuilder(resources).fromDataset(vectors); + datasetBuilder.withIndexParams(params); + assertThrows(IllegalStateException.class, () -> datasetBuilder.withIndexParams(params)); + + var bbqBuilder = + CagraIndex.newBuilder(resources).fromBbq(quantizer(BbqQuantizer.CodeLayout.PACKED_1B)); + var dense = fake(CuVSMatrix.class); + bbqBuilder.withDenseDataset(dense).withIndexParams(params); + assertThrows(IllegalStateException.class, () -> bbqBuilder.withDenseDataset(dense)); + assertThrows(IllegalStateException.class, () -> bbqBuilder.withIndexParams(params)); + + var serializedBuilder = + CagraIndex.newBuilder(resources).fromSerialized(new ByteArrayInputStream(new byte[0])); + serializedBuilder.withOutputDataset(new CagraIndex.StandardDataset()); + var failure = + assertThrows( + IllegalStateException.class, + () -> serializedBuilder.withOutputDataset(new CagraIndex.StandardDataset())); + assertTrue(failure.getMessage(), failure.getMessage().startsWith("withOutputDataset")); + } + } + + @Test + public void fromBbqTakesItsMetricFromTheQuantizers() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var quantizer = quantizer(BbqQuantizer.CodeLayout.PACKED_1B); // encoded for L2Expanded + var innerProduct = + new CagraIndexParams.Builder().withMetric(CuvsDistanceType.InnerProduct).build(); + var failure = + assertThrows( + IllegalArgumentException.class, + () -> + CagraIndex.newBuilder(resources) + .fromBbq(quantizer) + .withIndexParams(innerProduct)); + assertTrue(failure.getMessage(), failure.getMessage().contains("InnerProduct")); + + // AUTO_SELECT picks nn-descent for a BBQ dataset, so it is accepted. + CagraIndex.newBuilder(resources) + .fromBbq(quantizer) + .withIndexParams( + new CagraIndexParams.Builder() + .withCagraGraphBuildAlgo(CagraGraphBuildAlgo.AUTO_SELECT) + .build()); + } + } + + @Test + public void fromGraphNeedsADeviceDataset() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var hostMatrix = fake(CuVSMatrix.class); + var failure = + assertThrows( + IllegalArgumentException.class, + () -> + CagraIndex.newBuilder(resources) + .fromGraph(CuvsDistanceType.L2Expanded, hostMatrix, hostMatrix)); + assertTrue(failure.getMessage(), failure.getMessage().contains("device memory")); + } + } + + /** The stream belongs to the caller, so loading leaves it open, whether or not it succeeds. */ + @Test + public void fromSerializedLeavesTheStreamOpen() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + byte[] serialized; + try (CagraIndex index = CagraIndex.newBuilder(resources).fromDataset(vectors).build()) { + var bytes = new ByteArrayOutputStream(); + index.serialize(bytes); + serialized = bytes.toByteArray(); + } + + var stream = new CloseTrackingStream(serialized); + try (CagraIndex loaded = CagraIndex.newBuilder(resources).fromSerialized(stream).build()) { + assertEquals(ROWS, loaded.size()); + } + assertFalse(stream.closed); + + var failing = new CloseTrackingStream(serialized); + try (var notEmpty = new CagraIndex.StandardDataset()) { + notEmpty.setDelegate(() -> {}, 1); + var serializedBuilder = + CagraIndex.newBuilder(resources).fromSerialized(failing).withOutputDataset(notEmpty); + assertThrows(IllegalArgumentException.class, serializedBuilder::build); + } + assertFalse(failing.closed); + } + } + + @Test + public void fromDatasetRejectsAnArrayItCannotCopy() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var empty = CagraIndex.newBuilder(resources).fromDataset(new float[0][]); + assertThrows(IllegalArgumentException.class, empty::build); + var ragged = CagraIndex.newBuilder(resources).fromDataset(new float[][] {{1, 2}, {3, 4, 5}}); + var failure = assertThrows(IllegalArgumentException.class, ragged::build); + assertTrue(failure.getMessage(), failure.getMessage().contains("vectors[1] has 3")); + } + } + + @Test + public void requiredInputsMustNotBeNull() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var matrix = fake(CuVSMatrix.class); + var quantizer = quantizer(BbqQuantizer.CodeLayout.PACKED_1B); + var builder = CagraIndex.newBuilder(resources); + assertThrows(NullPointerException.class, () -> builder.fromDataset((CuVSMatrix) null)); + assertThrows(NullPointerException.class, () -> builder.fromDataset((float[][]) null)); + assertThrows(NullPointerException.class, () -> builder.fromBbq(null)); + assertThrows(NullPointerException.class, () -> builder.fromBbq(quantizer, null)); + assertThrows(NullPointerException.class, () -> builder.fromGraph(null, matrix, matrix)); + assertThrows( + NullPointerException.class, + () -> builder.fromGraph(CuvsDistanceType.L2Expanded, null, matrix)); + assertThrows(NullPointerException.class, () -> builder.fromSerialized(null)); + } + } + + /** A quantizer whose matrices are never read, since the checks here fail before that. */ + static BbqQuantizer quantizer(BbqQuantizer.CodeLayout layout) { + CuVSMatrix component = fake(CuVSMatrix.class); + return new BbqQuantizer.Builder() + .withCodes(component) + .withLowerIntervals(component) + .withUpperIntervals(component) + .withAdditionalCorrections(component) + .withQuantizedComponentSums(component) + .withCentroid(component) + .withDequantDelta(component) + .withDequantSumDelta(component) + .withRowNorm(component) + .withLayout(layout) + .withMetric(CuvsDistanceType.L2Expanded) + .withCentroidNormSq(0.0f) + .build(); + } + + /** A stream that records whether it was closed. */ + static final class CloseTrackingStream extends ByteArrayInputStream { + boolean closed; + + CloseTrackingStream(byte[] bytes) { + super(bytes); + } + + @Override + public void close() { + closed = true; + } + } + + /** An instance that only answers the methods of {@link Object}. */ + static T fake(Class type) { + return type.cast( + Proxy.newProxyInstance( + type.getClassLoader(), + new Class[] {type}, + (proxy, method, args) -> + switch (method.getName()) { + case "toString" -> "fake " + type.getSimpleName(); + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + default -> null; + })); + } +} diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexDeprecatedBuilderIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexDeprecatedBuilderIT.java new file mode 100644 index 0000000000..fb0e991e15 --- /dev/null +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraIndexDeprecatedBuilderIT.java @@ -0,0 +1,256 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs; + +import static com.carrotsearch.randomizedtesting.RandomizedTest.assumeTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.nvidia.cuvs.CagraIndexParams.CagraGraphBuildAlgo; +import com.nvidia.cuvs.CagraIndexParams.CuvsDistanceType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import org.junit.Before; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.RunWith; + +/** + * The deprecated setters of {@link CagraIndex.Builder}, end to end through the real provider, for + * as long as they are supported. Delete this class together with them. + */ +@RunWith(RandomizedRunner.class) +@SuppressWarnings("removal") +public class CagraIndexDeprecatedBuilderIT extends CuVSTestCase { + + private static final int ROWS = 512; + private static final int DIM = 16; + + private float[][] vectors; + + @Before + public void setup() { + assumeTrue("not supported on " + System.getProperty("os.name"), isLinuxSupportedArch()); + initializeRandom(); + vectors = generateData(random, ROWS, DIM); + } + + @Test + public void buildsFromADataset() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create(); + CagraIndex index = + CagraIndex.newBuilder(resources) + .withDataset(vectors) + .withIndexParams(params()) + .build()) { + assertEquals(ROWS, index.size()); + } + } + + @Test + public void loadsASerializedIndex() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + CuVSMatrix dataset = CuVSMatrix.ofArray(vectors); + // The file keeps the layout the index was built with, padded only if the rows already were. + boolean padded = CagraIndex.isPaddedDataset(dataset); + byte[] serialized; + try (CagraIndex index = + CagraIndex.newBuilder(resources).fromDataset(dataset).withIndexParams(params()).build()) { + var bytes = new ByteArrayOutputStream(); + index.serialize(bytes); + serialized = bytes.toByteArray(); + } + + // Unlike fromSerialized, the deprecated from(InputStream) closes the stream, as it always + // has. + var stream = new CagraIndexBuilderIT.CloseTrackingStream(serialized); + try (CagraIndex loaded = CagraIndex.newBuilder(resources).from(stream).build()) { + assertEquals(ROWS, loaded.size()); + } + assertTrue(stream.closed); + try (CagraIndex.DeserializeDataset outDataset = + padded ? new CagraIndex.PaddedDataset() : new CagraIndex.StandardDataset(); + CagraIndex loaded = + CagraIndex.newBuilder(resources) + .from(new ByteArrayInputStream(serialized), outDataset) + .build()) { + assertEquals(ROWS, loaded.size()); + assertTrue("the caller should own the loaded dataset", outDataset.isPresent()); + } + // As before, a later from(InputStream) drops the output dataset an earlier call gave. + try (var dropped = new CagraIndex.StandardDataset(); + CagraIndex loaded = + CagraIndex.newBuilder(resources) + .from(new ByteArrayInputStream(serialized), dropped) + .from(new ByteArrayInputStream(serialized)) + .build()) { + assertFalse(dropped.isPresent()); + } + } + } + + @Test + public void createsAnIndexFromAGraph() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create(); + CagraIndex index = + CagraIndex.newBuilder(resources) + .fromDataset(vectors) + .withIndexParams(params()) + .build(); + // A host copy, so the new index doesn't depend on the first one's device graph. + CuVSMatrix graph = index.getGraph().toHost()) { + // Owned by the new index once built, so it is not closed here. + CuVSMatrix deviceVectors; + try (CuVSMatrix hostVectors = CuVSMatrix.ofArray(vectors)) { + deviceVectors = hostVectors.toDevice(resources); + } + try (CagraIndex fromGraph = + CagraIndex.newBuilder(resources) + .from(graph) + .withDataset(deviceVectors) + .withIndexParams(params()) + .build()) { + assertEquals(ROWS, fromGraph.size()); + assertEquals(index.getGraphDegree(), fromGraph.getGraphDegree()); + } + } + } + + /** Without withIndexParams this used to fall back on native defaults that could not build. */ + @Test + public void buildsWithoutParameters() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create(); + CagraIndex index = CagraIndex.newBuilder(resources).withDataset(vectors).build()) { + assertEquals(ROWS, index.size()); + } + } + + @Test + public void keepsTheLastCall() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create(); + CagraIndex index = + CagraIndex.newBuilder(resources) + .withIndexParams(new CagraIndexParams.Builder().withGraphDegree(8).build()) + .withDataset(generateData(random, ROWS / 2, DIM)) + .withDataset(CuVSMatrix.ofArray(vectors)) + .withIndexParams(params()) + .build()) { + assertEquals(ROWS, index.size()); + assertEquals(16, index.getGraphDegree()); + } + } + + /** A failed build leaves the inputs in place, so the builder can be fixed and retried. */ + @Test + public void canRetryAFailedBuild() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var builder = + CagraIndex.newBuilder(resources) + .withDataset(vectors) + .withIndexParams(params()) + .from(new ByteArrayInputStream(new byte[0])); + // Loading the stream would ignore the dataset. + assertThrows(IllegalArgumentException.class, builder::build); + builder.from((InputStream) null); + try (CagraIndex index = builder.build()) { + assertEquals(ROWS, index.size()); + } + } + } + + /** The array is copied when it is given, so a bad one fails right there. */ + @Test + public void copiesAnArrayRightAway() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var builder = CagraIndex.newBuilder(resources); + assertThrows(IllegalArgumentException.class, () -> builder.withDataset(new float[0][])); + } + } + + @Test + public void rejectsEveryInputALoadWouldIgnore() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var matrix = CagraIndexBuilderIT.fake(CuVSMatrix.class); + var empty = new ByteArrayInputStream(new byte[0]); + assertIgnoredWhenLoading( + () -> CagraIndex.newBuilder(resources).from(empty).withDataset(matrix).build()); + assertIgnoredWhenLoading( + () -> CagraIndex.newBuilder(resources).withDataset(vectors).from(empty).build()); + assertIgnoredWhenLoading( + () -> CagraIndex.newBuilder(resources).from(empty).from(matrix).build()); + assertIgnoredWhenLoading( + () -> + CagraIndex.newBuilder(resources) + .from(empty) + .withBbqDataset(CagraIndexBuilderIT.quantizer(BbqQuantizer.CodeLayout.PACKED_1B)) + .build()); + assertIgnoredWhenLoading( + () -> CagraIndex.newBuilder(resources).from(empty).withIndexParams(params()).build()); + } + } + + /** from(InputStream) closes the stream, as documented, even when an input is rejected. */ + @Test + public void closesTheStreamWhenALoadIsRejected() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var stream = new CagraIndexBuilderIT.CloseTrackingStream(new byte[0]); + assertIgnoredWhenLoading( + () -> CagraIndex.newBuilder(resources).from(stream).withIndexParams(params()).build()); + assertTrue(stream.closed); + } + } + + @Test + public void rejectsQuantizersWhenCreatingAnIndexFromAGraph() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var matrix = CagraIndexBuilderIT.fake(CuVSMatrix.class); + var failure = + assertThrows( + IllegalArgumentException.class, + () -> + CagraIndex.newBuilder(resources) + .from(matrix) + .withDataset(matrix) + .withIndexParams(params()) + .withBbqDataset( + CagraIndexBuilderIT.quantizer(BbqQuantizer.CodeLayout.PACKED_1B)) + .build()); + assertTrue(failure.getMessage(), failure.getMessage().contains("fromGraph")); + } + } + + @Test + public void cannotBeMixedWithFrom() throws Throwable { + try (CuVSResources resources = CheckedCuVSResources.create()) { + var afterSetter = CagraIndex.newBuilder(resources).withIndexParams(params()); + var failure = + assertThrows(IllegalStateException.class, () -> afterSetter.fromDataset(vectors)); + assertTrue(failure.getMessage(), failure.getMessage().contains("withIndexParams")); + + var afterFrom = CagraIndex.newBuilder(resources); + afterFrom.fromDataset(vectors); + failure = assertThrows(IllegalStateException.class, () -> afterFrom.withDataset(vectors)); + assertTrue(failure.getMessage(), failure.getMessage().contains("fromDataset")); + } + } + + private static void assertIgnoredWhenLoading(ThrowingRunnable build) { + var failure = assertThrows(IllegalArgumentException.class, build); + assertTrue(failure.getMessage(), failure.getMessage().contains("fromSerialized")); + } + + private static CagraIndexParams params() { + return new CagraIndexParams.Builder() + .withCagraGraphBuildAlgo(CagraGraphBuildAlgo.NN_DESCENT) + .withGraphDegree(16) + .withIntermediateGraphDegree(32) + .withMetric(CuvsDistanceType.L2Expanded) + .build(); + } +} diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraMultiThreadStabilityIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraMultiThreadStabilityIT.java index 17790eb2fc..a91459f8cf 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraMultiThreadStabilityIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraMultiThreadStabilityIT.java @@ -87,7 +87,7 @@ private void testQueryingUsingMultipleThreads(ResourcesSupplier resourcesSupplie try (var deviceDataset = CuVSMatrix.ofArray(dataset).toDevice(resources); CagraIndex index = CagraIndex.newBuilder(resources) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(indexParams) .build(); // Dim=256 floats is already 16-byte aligned, so wrap a padded view directly. diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraRandomizedIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraRandomizedIT.java index 9110f6ddc2..7f44e41822 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraRandomizedIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/CagraRandomizedIT.java @@ -128,7 +128,7 @@ private void tmpResultsTopKWithRandomValues(TestDatasetMemoryKind datasetMemoryK } index = CagraIndex.newBuilder(resources) - .withDataset(datasetBuilder.build()) + .fromDataset(datasetBuilder.build()) .withIndexParams(indexParams) .build(); } else if (datasetMemoryKind == TestDatasetMemoryKind.DEVICE) { @@ -140,14 +140,14 @@ private void tmpResultsTopKWithRandomValues(TestDatasetMemoryKind datasetMemoryK } index = CagraIndex.newBuilder(resources) - .withDataset(datasetBuilder.build()) + .fromDataset(datasetBuilder.build()) .withIndexParams(indexParams) .build(); } else { assert datasetMemoryKind == TestDatasetMemoryKind.HEAP; index = CagraIndex.newBuilder(resources) - .withDataset(vectors) + .fromDataset(vectors) .withIndexParams(indexParams) .build(); } diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/FilterBitsetHandleIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/FilterBitsetHandleIT.java index d7464ca97e..afd777d87f 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/FilterBitsetHandleIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/FilterBitsetHandleIT.java @@ -359,7 +359,7 @@ private List buildPartitions( for (int p = 0; p < partStart.length; p++) { float[][] slice = Arrays.copyOfRange(dataset, partStart[p], partStart[p] + PART_ROWS); var index = - CagraIndex.newBuilder(resources).withDataset(slice).withIndexParams(indexParams).build(); + CagraIndex.newBuilder(resources).fromDataset(slice).withIndexParams(indexParams).build(); // DIM=16 float rows are already aligned, so retain the device matrix behind this view. try (var hostDataset = CuVSMatrix.ofArray(slice)) { var deviceDataset = hostDataset.toDevice(resources); diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java index 78f86bd964..371e3ca59d 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswBuildAndSearchIT.java @@ -61,7 +61,7 @@ private static void indexAndQueryOnce( // Create the index with the dataset CagraIndex index = - CagraIndex.newBuilder(resources).withDataset(dataset).withIndexParams(indexParams).build(); + CagraIndex.newBuilder(resources).fromDataset(dataset).withIndexParams(indexParams).build(); // hnswlib search runs on the host, so a host-built index serializes straight from its own // host-resident vectors without a detour through device-padded storage. diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswRandomizedIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswRandomizedIT.java index 4ace8fc92e..a663ada1f1 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswRandomizedIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/HnswRandomizedIT.java @@ -110,13 +110,13 @@ private void tmpResultsTopKWithRandomValues(boolean useNativeMemoryDataset) thro } index = CagraIndex.newBuilder(resources) - .withDataset(datasetBuilder.build()) + .fromDataset(datasetBuilder.build()) .withIndexParams(indexParams) .build(); } else { index = CagraIndex.newBuilder(resources) - .withDataset(vectors) + .fromDataset(vectors) .withIndexParams(indexParams) .build(); } diff --git a/java/cuvs-java/src/test/java/com/nvidia/cuvs/MultiPartitionCagraSearchIT.java b/java/cuvs-java/src/test/java/com/nvidia/cuvs/MultiPartitionCagraSearchIT.java index 2d1e33484a..6dc9c7f1ae 100644 --- a/java/cuvs-java/src/test/java/com/nvidia/cuvs/MultiPartitionCagraSearchIT.java +++ b/java/cuvs-java/src/test/java/com/nvidia/cuvs/MultiPartitionCagraSearchIT.java @@ -176,7 +176,7 @@ private List buildPartitions( for (int p = 0; p < partStart.length; p++) { float[][] slice = Arrays.copyOfRange(dataset, partStart[p], partStart[p] + PART_ROWS); var index = - CagraIndex.newBuilder(resources).withDataset(slice).withIndexParams(indexParams).build(); + CagraIndex.newBuilder(resources).fromDataset(slice).withIndexParams(indexParams).build(); // DIM=16 float rows are already aligned, so retain the device matrix behind this view. try (var hostDataset = CuVSMatrix.ofArray(slice)) { var deviceDataset = hostDataset.toDevice(resources); diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java index 18461bbcfb..5c7a4d8a0f 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java @@ -294,7 +294,7 @@ private static CuVSMatrix buildCagraGraphForSubset( try (Utils.OwnedIndex ownedIndex = Utils.ownDataset(subsetDataset)) { CagraIndex subsetIndex = CagraIndex.newBuilder(getCuVSResourcesInstance()) - .withDataset(subsetDataset) + .fromDataset(subsetDataset) .withIndexParams(params) .build(); ownedIndex.transferTo(subsetIndex); diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java index 1efa2d1174..d649175811 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java @@ -332,7 +332,7 @@ CagraIndex openCagraIndexForMerge(String field) throws IOException { cuvsIndexInput.slice( "cagra index", fieldEntry.cagraIndexOffset(), fieldEntry.cagraIndexLength()); var in = new IndexInputInputStream(slice)) { - return CagraIndex.newBuilder(getCuVSResourcesInstance()).from(in).build(); + return CagraIndex.newBuilder(getCuVSResourcesInstance()).fromSerialized(in).build(); } catch (Throwable t) { Utils.handleThrowable(t); throw new AssertionError("unreachable"); @@ -372,7 +372,7 @@ private GPUIndex loadCuVSIndex(FieldEntry fieldEntry) throws IOException { long off = fieldEntry.cagraIndexOffset(); try (var slice = cuvsIndexInput.slice("cagra index", off, len); var in = new IndexInputInputStream(slice)) { - cagraIndex = CagraIndex.newBuilder(getCuVSResourcesInstance()).from(in).build(); + cagraIndex = CagraIndex.newBuilder(getCuVSResourcesInstance()).fromSerialized(in).build(); } } len = fieldEntry.bruteForceIndexLength(); diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java index 5feb211afe..583157f427 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java @@ -254,7 +254,7 @@ private void writeCagraIndex(OutputStream os, CuVSMatrix dataset) throws Throwab CagraIndexParamsFactory.create(gpuSearchParams, dataset.size(), dataset.columns()); try (CagraIndex index = CagraIndex.newBuilder(getCuVSResourcesInstance()) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(params) .build(); var deviceVectors = dataset.toDevice(getCuVSResourcesInstance())) { diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java index f1949fbd2d..2424ebbb1e 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java @@ -183,7 +183,7 @@ private void writeNonTrivialField(FieldInfo fieldInfo, CuVSMatrix dataset) throw CagraIndexParamsFactory.create(acceleratedHNSWParams, dataset.size(), dataset.columns()); CagraIndex cagraIndex = CagraIndex.newBuilder(getCuVSResourcesInstance()) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(params) .build(); ownedIndex.transferTo(cagraIndex); diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java index c7399a10a9..269e29d2f8 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java @@ -181,7 +181,7 @@ private void writeNonTrivialField(FieldInfo fieldInfo, CuVSMatrix dataset) throw CagraIndexParamsFactory.create(acceleratedHNSWParams, dataset.size(), dataset.columns()); CagraIndex cagraIndex = CagraIndex.newBuilder(getCuVSResourcesInstance()) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(params) .build(); ownedIndex.transferTo(cagraIndex); diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java index bb131fdcf5..03cfb6305c 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java @@ -208,7 +208,7 @@ private void writeNonTrivialField(FieldInfo fieldInfo, CuVSMatrix dataset) throw CagraIndexParamsFactory.create(acceleratedHNSWParams, dataset.size(), dataset.columns()); CagraIndex cagraIndex = CagraIndex.newBuilder(getCuVSResourcesInstance()) - .withDataset(dataset) + .fromDataset(dataset) .withIndexParams(params) .build(); ownedIndex.transferTo(cagraIndex);