From ee335be1fec13445a96bc08f5011020200984bfc Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Tue, 29 Sep 2026 16:42:59 -1000 Subject: [PATCH 01/15] =?UTF-8?q?Add=20support=20for=20Lucene=2010.2?= =?UTF-8?q?=E2=80=9310.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build a separate jar for each Lucene minor release from 10.2 to 10.5, since Lucene changes its codec APIs between minor releases. All modules compile the same shared sources. Codecs built on an older Lucene release's default codec stay registered so existing indexes can still be read, but they can no longer write. Fixes #2410 --- .github/workflows/build.yaml | 3 +- .github/workflows/pr.yaml | 3 +- .github/workflows/test.yaml | 3 +- .pre-commit-config.yaml | 3 +- build.sh | 4 +- ci/checks/run_spotless.sh | 2 +- ci/release/update-version.sh | 2 +- ci/test_lucene_prebuilt.sh | 8 +- examples/java/cuvs-lucene/README.md | 4 + examples/java/cuvs-lucene/pom.xml | 10 +- .../examples/AcceleratedHnswExample.java | 4 +- .../examples/IndexAndSearchonGPUExample.java | 4 +- fern/docs.yml | 22 +- fern/pages/installation/java.md | 23 +- fern/pages/lucene_api/index.md | 13 +- ...nvidia-cuvs-lucene-acceleratedhnswutils.md | 18 +- ...idia-cuvs-lucene-cuvs2510gpusearchcodec.md | 71 +--- ...ia-cuvs-lucene-cuvs2510gpuvectorsformat.md | 14 +- ...ia-cuvs-lucene-cuvs2510gpuvectorsreader.md | 48 +-- ...ia-cuvs-lucene-cuvs2510gpuvectorswriter.md | 28 +- ...e-api-com-nvidia-cuvs-lucene-cuvscodecs.md | 232 ++++++++++ ...idia-cuvs-lucene-gpuknnfloatvectorquery.md | 8 +- ...vs-lucene-lucene101acceleratedhnswcodec.md | 59 +-- ...e103acceleratedhnswbinaryquantizedcodec.md | 49 +++ ...vs-lucene-lucene103acceleratedhnswcodec.md | 49 +++ ...e103acceleratedhnswscalarquantizedcodec.md | 49 +++ ...cuvs-lucene-lucene103cuvsgpusearchcodec.md | 49 +++ ...e104acceleratedhnswbinaryquantizedcodec.md | 49 +++ ...vs-lucene-lucene104acceleratedhnswcodec.md | 49 +++ ...e104acceleratedhnswscalarquantizedcodec.md | 49 +++ ...leratedhnswscalarquantizedvectorsformat.md | 48 +++ ...cuvs-lucene-lucene104cuvsgpusearchcodec.md | 49 +++ ...ne-lucene99acceleratedhnswvectorsformat.md | 12 +- ...ne-lucene99acceleratedhnswvectorswriter.md | 22 +- ...ceneacceleratedhnswbinaryquantizedcodec.md | 8 +- ...leratedhnswbinaryquantizedvectorsformat.md | 16 +- ...leratedhnswbinaryquantizedvectorswriter.md | 8 +- ...ceneacceleratedhnswscalarquantizedcodec.md | 8 +- ...leratedhnswscalarquantizedvectorsformat.md | 42 +- ...leratedhnswscalarquantizedvectorswriter.md | 22 +- ...i-com-nvidia-cuvs-lucene-luceneprovider.md | 15 - ...lucene-api-com-nvidia-cuvs-lucene-utils.md | 42 +- fern/pages/user_guide/lucene.md | 60 ++- fern/scripts/generate_api_reference.py | 13 +- java/cuvs-lucene/MAINTAINING.md | 241 +++++++++++ java/cuvs-lucene/README.md | 82 +++- java/cuvs-lucene/bench/pom.xml | 3 +- .../AcceleratedHnswIndexingBenchmarks.java | 14 +- .../AcceleratedHnswSearchBenchmarks.java | 14 +- .../benchmarks/CagraIndexingBenchmarks.java | 4 +- .../benchmarks/CagraSearchBenchmarks.java | 4 +- java/cuvs-lucene/build.sh | 19 +- java/cuvs-lucene/compat/README.md | 41 ++ .../lucene/CompatKnnFloatVectorQuery.java | 41 ++ .../lucene/CompatKnnFloatVectorQuery.java | 41 ++ .../cuvs/lucene/CompatKnnVectorsReader.java | 52 +++ .../cuvs/lucene/CompatKnnVectorsReader.java | 65 +++ .../cuvs/lucene/CompatKnnVectorsWriter.java | 26 ++ .../cuvs/lucene/CompatKnnVectorsWriter.java | 29 ++ .../nvidia/cuvs/lucene/TestLuceneCompat.java | 37 ++ .../nvidia/cuvs/lucene/TestLuceneCompat.java | 41 ++ java/cuvs-lucene/lucene-10.2/pom.xml | 41 ++ .../com/nvidia/cuvs/lucene/LuceneCompat.java | 89 ++++ .../services/org.apache.lucene.codecs.Codec | 0 .../org.apache.lucene.codecs.KnnVectorsFormat | 4 +- java/cuvs-lucene/lucene-10.3/pom.xml | 64 +++ .../com/nvidia/cuvs/lucene/LuceneCompat.java | 95 +++++ .../services/org.apache.lucene.codecs.Codec | 12 + .../org.apache.lucene.codecs.KnnVectorsFormat | 9 + ...03AcceleratedHNSWBinaryQuantizedCodec.java | 39 ++ .../lucene/Lucene103AcceleratedHNSWCodec.java | 39 ++ ...03AcceleratedHNSWScalarQuantizedCodec.java | 39 ++ .../lucene/Lucene103CuVSGPUSearchCodec.java | 40 ++ java/cuvs-lucene/lucene-10.4/pom.xml | 65 +++ .../com/nvidia/cuvs/lucene/LuceneCompat.java | 114 +++++ .../services/org.apache.lucene.codecs.Codec | 16 + .../org.apache.lucene.codecs.KnnVectorsFormat | 10 + ...04AcceleratedHNSWBinaryQuantizedCodec.java | 39 ++ .../lucene/Lucene104AcceleratedHNSWCodec.java | 39 ++ ...04AcceleratedHNSWScalarQuantizedCodec.java | 39 ++ ...ratedHNSWScalarQuantizedVectorsFormat.java | 38 ++ .../lucene/Lucene104CuVSGPUSearchCodec.java | 40 ++ java/cuvs-lucene/lucene-10.5/pom.xml | 65 +++ .../com/nvidia/cuvs/lucene/LuceneCompat.java | 114 +++++ .../services/org.apache.lucene.codecs.Codec | 16 + .../org.apache.lucene.codecs.KnnVectorsFormat | 10 + java/cuvs-lucene/pom.xml | 400 +++++++++++++----- java/cuvs-lucene/relocation/pom.xml | 129 ++++++ .../src/assembly/jar-with-dependencies.xml | 32 ++ .../cuvs/lucene/AcceleratedHNSWUtils.java | 31 +- ...ratedHNSWScalarQuantizedVectorsFormat.java | 97 +++++ .../cuvs/lucene/CuVS2510GPUSearchCodec.java | 76 +--- .../cuvs/lucene/CuVS2510GPUVectorsFormat.java | 17 +- .../cuvs/lucene/CuVS2510GPUVectorsReader.java | 25 +- .../cuvs/lucene/CuVS2510GPUVectorsWriter.java | 18 +- .../com/nvidia/cuvs/lucene/CuVSCodecs.java | 153 +++++++ .../nvidia/cuvs/lucene/CuVSFilterCodec.java | 270 ++++++++++++ .../cuvs/lucene/GPUKnnFloatVectorQuery.java | 19 +- .../lucene/Lucene101AcceleratedHNSWCodec.java | 79 +--- .../Lucene99AcceleratedHNSWVectorsFormat.java | 42 +- .../Lucene99AcceleratedHNSWVectorsWriter.java | 28 +- ...neAcceleratedHNSWBinaryQuantizedCodec.java | 57 +-- ...ratedHNSWBinaryQuantizedVectorsFormat.java | 53 +-- ...ratedHNSWBinaryQuantizedVectorsWriter.java | 16 +- ...neAcceleratedHNSWScalarQuantizedCodec.java | 61 +-- ...ratedHNSWScalarQuantizedVectorsFormat.java | 88 +--- ...ratedHNSWScalarQuantizedVectorsWriter.java | 28 +- .../nvidia/cuvs/lucene/LuceneProvider.java | 303 ------------- .../cuvs/lucene/LuceneVersionGuard.java | 66 +++ .../java/com/nvidia/cuvs/lucene/Utils.java | 20 + .../backcompat/GenerateReleasedIndices.java | 153 +++++++ .../backcompat/generate-released-indices.sh | 39 ++ .../BaseCuVSKnnVectorsFormatTestCase.java | 71 ++++ .../TestAcceleratedHNSWOddGraphDegree.java | 2 +- ...tedHNSWVectorsFormatThrowableHandling.java | 3 +- .../nvidia/cuvs/lucene/TestBackCompat.java | 127 +++++- .../cuvs/lucene/TestBackCompatIndices.java | 292 +++++++++++++ ...TestCagraToHnswSerializationAndSearch.java | 4 +- ...ializationAndSearchWithFallbackWriter.java | 2 +- .../cuvs/lucene/TestCuVSDeletedDocuments.java | 9 +- .../cuvs/lucene/TestCuVSVectorsFormat.java | 51 +-- .../TestGPUSelectiveFilterFallback.java | 4 +- ...tLucene99AcceleratedHNSWVectorsFormat.java | 51 +-- .../TestMultiSegmentGPUFilterConcurrency.java | 5 +- .../lucene/TestPerSegmentGPUFilterSearch.java | 4 +- .../lucene/TestQuantizedFallbackWriters.java | 137 ++++++ .../lucene/TestQuantizedVectorsFormats.java | 48 +-- .../lucene/TestSegmentMaxConnConsistency.java | 5 +- ...eleratedHNSWVectorsFormat-cuvs-25.12.0.zip | Bin 0 -> 12484 bytes ...dHNSWBinaryQuantizedCodec-cuvs-26.08.0.zip | Bin 0 -> 14327 bytes ...101AcceleratedHNSWBinaryQuantizedCodec.zip | Bin 0 -> 15096 bytes ...ne101AcceleratedHNSWCodec-cuvs-26.08.0.zip | Bin 0 -> 15868 bytes .../10.2-Lucene101AcceleratedHNSWCodec.zip | Bin 0 -> 15619 bytes ...dHNSWScalarQuantizedCodec-cuvs-26.08.0.zip | Bin 0 -> 17970 bytes ...101AcceleratedHNSWScalarQuantizedCodec.zip | Bin 0 -> 17686 bytes ...103AcceleratedHNSWBinaryQuantizedCodec.zip | Bin 0 -> 15016 bytes .../10.3-Lucene103AcceleratedHNSWCodec.zip | Bin 0 -> 15564 bytes ...103AcceleratedHNSWScalarQuantizedCodec.zip | Bin 0 -> 17629 bytes ...104AcceleratedHNSWBinaryQuantizedCodec.zip | Bin 0 -> 14986 bytes .../10.4-Lucene104AcceleratedHNSWCodec.zip | Bin 0 -> 15609 bytes ...104AcceleratedHNSWScalarQuantizedCodec.zip | Bin 0 -> 20233 bytes ...104AcceleratedHNSWBinaryQuantizedCodec.zip | Bin 0 -> 15013 bytes .../10.5-Lucene104AcceleratedHNSWCodec.zip | Bin 0 -> 15571 bytes ...104AcceleratedHNSWScalarQuantizedCodec.zip | Bin 0 -> 20217 bytes .../src/test/resources/backcompat/README.md | 28 ++ 145 files changed, 4875 insertions(+), 1392 deletions(-) create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswbinaryquantizedcodec.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswcodec.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswscalarquantizedcodec.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103cuvsgpusearchcodec.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswbinaryquantizedcodec.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswcodec.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedcodec.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedvectorsformat.md create mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104cuvsgpusearchcodec.md delete mode 100644 fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneprovider.md create mode 100644 java/cuvs-lucene/MAINTAINING.md create mode 100644 java/cuvs-lucene/compat/README.md create mode 100644 java/cuvs-lucene/compat/knn-float-vector-query-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java create mode 100644 java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java create mode 100644 java/cuvs-lucene/compat/knn-vectors-reader-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java create mode 100644 java/cuvs-lucene/compat/knn-vectors-reader-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java create mode 100644 java/cuvs-lucene/compat/knn-vectors-writer-102-void-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java create mode 100644 java/cuvs-lucene/compat/knn-vectors-writer-105-deferred-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java create mode 100644 java/cuvs-lucene/compat/leaf-reader-102-bits/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java create mode 100644 java/cuvs-lucene/compat/leaf-reader-103-acceptdocs/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java create mode 100644 java/cuvs-lucene/lucene-10.2/pom.xml create mode 100644 java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java rename java/cuvs-lucene/{ => lucene-10.2}/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec (100%) rename java/cuvs-lucene/{ => lucene-10.2}/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat (66%) create mode 100644 java/cuvs-lucene/lucene-10.3/pom.xml create mode 100644 java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java create mode 100644 java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec create mode 100644 java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat create mode 100644 java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWBinaryQuantizedCodec.java create mode 100644 java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWCodec.java create mode 100644 java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWScalarQuantizedCodec.java create mode 100644 java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103CuVSGPUSearchCodec.java create mode 100644 java/cuvs-lucene/lucene-10.4/pom.xml create mode 100644 java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java create mode 100644 java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec create mode 100644 java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat create mode 100644 java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWBinaryQuantizedCodec.java create mode 100644 java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWCodec.java create mode 100644 java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedCodec.java create mode 100644 java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java create mode 100644 java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104CuVSGPUSearchCodec.java create mode 100644 java/cuvs-lucene/lucene-10.5/pom.xml create mode 100644 java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java create mode 100644 java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec create mode 100644 java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat create mode 100644 java/cuvs-lucene/relocation/pom.xml create mode 100644 java/cuvs-lucene/src/assembly/jar-with-dependencies.xml create mode 100644 java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java create mode 100644 java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java create mode 100644 java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSFilterCodec.java delete mode 100644 java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java create mode 100644 java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneVersionGuard.java create mode 100644 java/cuvs-lucene/src/test/backcompat/GenerateReleasedIndices.java create mode 100755 java/cuvs-lucene/src/test/backcompat/generate-released-indices.sh create mode 100644 java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java create mode 100644 java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompatIndices.java create mode 100644 java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101-Lucene99AcceleratedHNSWVectorsFormat-cuvs-25.12.0.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWBinaryQuantizedCodec-cuvs-26.08.0.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWBinaryQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWCodec-cuvs-26.08.0.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWScalarQuantizedCodec-cuvs-26.08.0.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWScalarQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.3-Lucene103AcceleratedHNSWBinaryQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.3-Lucene103AcceleratedHNSWCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.3-Lucene103AcceleratedHNSWScalarQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.4-Lucene104AcceleratedHNSWBinaryQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.4-Lucene104AcceleratedHNSWCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.4-Lucene104AcceleratedHNSWScalarQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.5-Lucene104AcceleratedHNSWBinaryQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.5-Lucene104AcceleratedHNSWCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/10.5-Lucene104AcceleratedHNSWScalarQuantizedCodec.zip create mode 100644 java/cuvs-lucene/src/test/resources/backcompat/README.md diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 99e0f1f6bd..ef81d00241 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -234,7 +234,8 @@ jobs: container_image: "rapidsai/ci-conda:26.12-cuda${{ matrix.CUDA_VER }}-${{ matrix.LINUX_VER }}-py${{ matrix.PY_VER }}" script: "ci/build_lucene.sh cuvs-java-cuda${{ matrix.CUDA_VER }}" artifact-name: "cuvs-lucene-cuda${{ matrix.CUDA_VER }}" - file_to_upload: "java/cuvs-lucene/target/" + # One target/ per module (lucene-X.Y/, relocation/); the artifact keeps them under those directories. + file_to_upload: "java/cuvs-lucene/*/target/" sha: ${{ inputs.sha }} python-build: needs: [build-details, cpp-build] diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index b16137b161..7faa9b52d9 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -726,7 +726,8 @@ jobs: container_image: "rapidsai/ci-conda:26.12-cuda${{ matrix.CUDA_VER }}-${{ matrix.LINUX_VER }}-py${{ matrix.PY_VER }}" script: "ci/test_lucene.sh cuvs-java-cuda${{ matrix.CUDA_VER }}" artifact-name: "cuvs-lucene-cuda${{ matrix.CUDA_VER }}" - file_to_upload: "java/cuvs-lucene/target/" + # One target/ per module (lucene-X.Y/, relocation/); the artifact keeps them under those directories. + file_to_upload: "java/cuvs-lucene/*/target/" conda-lucene-tests-other-arch: # Verifies that cuvs-lucene, built against the amd64-built cuvs-java jar, also works # correctly on every non-amd64 architecture, by running its test suite against the diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 497e382b65..23c22fe49d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -166,7 +166,8 @@ jobs: container_image: "rapidsai/ci-conda:26.12-cuda${{ matrix.CUDA_VER }}-${{ matrix.LINUX_VER }}-py${{ matrix.PY_VER }}" script: "ci/test_lucene.sh cuvs-java-cuda${{ matrix.CUDA_VER }}" artifact-name: "cuvs-lucene-cuda${{ matrix.CUDA_VER }}" - file_to_upload: "java/cuvs-lucene/target/" + # One target/ per module (lucene-X.Y/, relocation/); the artifact keeps them under those directories. + file_to_upload: "java/cuvs-lucene/*/target/" conda-lucene-tests-other-arch: # Verifies that cuvs-lucene, built against the amd64-built cuvs-java jar, also works # correctly on every non-amd64 architecture, by running its test suite against the diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18a1fdd0c4..cc92f47499 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,6 +69,7 @@ repos: ^cpp/include/.*[.](h|hpp|cuh)$| ^python/cuvs/cuvs/.*[.](py|pyx|pxd)$| ^java/cuvs-(java|lucene)/src/main/.*[.]java$| + ^java/cuvs-lucene/lucene-[^/]+/src/since/.*[.]java$| ^rust/cuvs/src/.*[.]rs$| ^go/.*[.]go$| ^fern/scripts/generate_api_reference[.]py$| @@ -121,7 +122,7 @@ repos: name: spotless-fmt entry: ci/checks/run_spotless.sh pass_filenames: false - files: ^(java|examples/java)/(cuvs-java|cuvs-lucene)/([^/]+/)?src/.*\.java$ + files: ^(java|examples/java)/(cuvs-java|cuvs-lucene)/([^/]+/){0,2}src/.*\.java$ exclude: .*/panama/.* language: script verbose: true diff --git a/build.sh b/build.sh index 40ffefdc87..3b5f4a1472 100755 --- a/build.sh +++ b/build.sh @@ -81,7 +81,7 @@ RUST_BUILD_DIR=${REPODIR}/rust/target JAVA_INTERNAL_BUILD_DIR=${REPODIR}/java/internal/build JAVA_BUILD_DIR=${REPODIR}/java/cuvs-java/target JAVA_PANAMA_DIR=${REPODIR}/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/panama -LUCENE_BUILD_DIR=${REPODIR}/java/cuvs-lucene/target +LUCENE_BUILD_DIRS=("${REPODIR}"/java/cuvs-lucene/target "${REPODIR}"/java/cuvs-lucene/*/target) BUILD_DIRS=( "${LIBCUVS_BUILD_DIR}" "${PYTHON_BUILD_DIR}" @@ -89,7 +89,7 @@ BUILD_DIRS=( "${JAVA_INTERNAL_BUILD_DIR}" "${JAVA_BUILD_DIR}" "${JAVA_PANAMA_DIR}" - "${LUCENE_BUILD_DIR}" + "${LUCENE_BUILD_DIRS[@]}" ) # Set defaults for vars modified by flags to this script diff --git a/ci/checks/run_spotless.sh b/ci/checks/run_spotless.sh index da18413a8b..f588f60757 100755 --- a/ci/checks/run_spotless.sh +++ b/ci/checks/run_spotless.sh @@ -15,7 +15,7 @@ set -euo pipefail # Keep these in sync with the spotless-fmt hook's 'files'/'exclude' entries in # .pre-commit-config.yaml. -JAVA_SRC_PATTERN='^(java|examples/java)/(cuvs-java|cuvs-lucene)/([^/]+/)?src/.*\.java$' +JAVA_SRC_PATTERN='^(java|examples/java)/(cuvs-java|cuvs-lucene)/([^/]+/){0,2}src/.*\.java$' JAVA_SRC_EXCLUDE='.*/panama/.*' java_sources_modified() { diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index 5eef5500c5..dde2078f6e 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -172,7 +172,7 @@ done NEXT_FULL_JAVA_TAG="${NEXT_SHORT_TAG}.${PATCH_PEP440}" sed_runner "s/VERSION=\".*\"/VERSION=\"${NEXT_FULL_JAVA_TAG}\"/g" java/build.sh sed_runner "s/VERSION=\".*\"/VERSION=\"${NEXT_FULL_JAVA_TAG}\"/g" java/cuvs-lucene/build.sh -for FILE in java/*/pom.xml java/cuvs-lucene/bench/pom.xml examples/java/cuvs-java/pom.xml examples/java/cuvs-lucene/pom.xml; do +for FILE in java/*/pom.xml java/cuvs-lucene/lucene-*/pom.xml java/cuvs-lucene/relocation/pom.xml java/cuvs-lucene/bench/pom.xml examples/java/cuvs-java/pom.xml examples/java/cuvs-lucene/pom.xml; do sed_runner "/.*/s//${NEXT_FULL_JAVA_TAG}<\/version>/g" "${FILE}" done diff --git a/ci/test_lucene_prebuilt.sh b/ci/test_lucene_prebuilt.sh index 477ff75ad3..1a0800d29b 100755 --- a/ci/test_lucene_prebuilt.sh +++ b/ci/test_lucene_prebuilt.sh @@ -89,11 +89,11 @@ pushd java/cuvs-lucene mvn --batch-mode install:install-file -Dfile="${CUVS_JAVA_JARS[0]}" -DpomFile="${CUVS_JAVA_POM}" popd -rapids-logger "Restore the amd64-built cuvs-lucene target/ directory so Maven can run the tests without recompiling" +rapids-logger "Restore the amd64-built cuvs-lucene target/ directories so Maven can run the tests without recompiling" -rm -rf java/cuvs-lucene/target -mkdir -p java/cuvs-lucene/target -cp -a "${CUVS_LUCENE_DIR}/." java/cuvs-lucene/target/ +# The artifact holds one /target/ per module. +rm -rf java/cuvs-lucene/*/target +cp -a "${CUVS_LUCENE_DIR}/." java/cuvs-lucene/ EXITCODE=0 trap "EXITCODE=1" ERR diff --git a/examples/java/cuvs-lucene/README.md b/examples/java/cuvs-lucene/README.md index 960efb97d8..5f7fc96a26 100644 --- a/examples/java/cuvs-lucene/README.md +++ b/examples/java/cuvs-lucene/README.md @@ -15,6 +15,10 @@ First build `cuvs-lucene` and install it into your local Maven repository, as de ./build.sh libcuvs java lucene ``` +The examples use Lucene 10.5 and the matching `cuvs-lucene-10.5` artifact. To run them against another +supported Lucene release, change the `lucene.version` and `cuvs.lucene.artifactId` properties in `pom.xml`, for +example to `10.3.2` and `cuvs-lucene-10.3`. + Then return to this directory: ```sh diff --git a/examples/java/cuvs-lucene/pom.xml b/examples/java/cuvs-lucene/pom.xml index fa3207f1a2..44f953f2ff 100644 --- a/examples/java/cuvs-lucene/pom.xml +++ b/examples/java/cuvs-lucene/pom.xml @@ -19,6 +19,8 @@ 22 UTF-8 UTF-8 + 10.5.1 + cuvs-lucene-10.5 @@ -79,18 +81,18 @@ org.apache.lucene lucene-core - 10.2.0 + ${lucene.version} org.apache.lucene lucene-codecs - 10.2.0 + ${lucene.version} test org.apache.lucene lucene-backward-codecs - 10.2.0 + ${lucene.version} commons-io @@ -99,7 +101,7 @@ com.nvidia.cuvs.lucene - cuvs-lucene + ${cuvs.lucene.artifactId} 26.12.0 diff --git a/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/AcceleratedHnswExample.java b/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/AcceleratedHnswExample.java index d9fbffe9f0..3c963b87f3 100644 --- a/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/AcceleratedHnswExample.java +++ b/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/AcceleratedHnswExample.java @@ -8,7 +8,7 @@ import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; import com.nvidia.cuvs.lucene.AcceleratedHNSWParams; -import com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec; +import com.nvidia.cuvs.lucene.CuVSCodecs; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; @@ -46,7 +46,7 @@ public class AcceleratedHnswExample { public static void main(String[] args) throws Exception { AcceleratedHNSWParams params = new AcceleratedHNSWParams.Builder().build(); - Codec codec = new Lucene101AcceleratedHNSWCodec(params); + Codec codec = CuVSCodecs.acceleratedHNSW(params); IndexWriterConfig config = new IndexWriterConfig().setCodec(codec).setUseCompoundFile(false); random = new Random(222); diff --git a/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/IndexAndSearchonGPUExample.java b/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/IndexAndSearchonGPUExample.java index 1fd0a39f1f..5116722197 100644 --- a/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/IndexAndSearchonGPUExample.java +++ b/examples/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/examples/IndexAndSearchonGPUExample.java @@ -7,7 +7,7 @@ import static com.nvidia.cuvs.lucene.examples.Utils.generateDataset; import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; -import com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec; +import com.nvidia.cuvs.lucene.CuVSCodecs; import com.nvidia.cuvs.lucene.GPUKnnFloatVectorQuery; import com.nvidia.cuvs.lucene.GPUSearchParams; import com.nvidia.cuvs.spi.CuVSProvider; @@ -54,7 +54,7 @@ public static void main(String[] args) throws Exception { CuVSProvider.provider().enableRMMAsyncMemory(); GPUSearchParams params = new GPUSearchParams.Builder().build(); - Codec codec = new CuVS2510GPUSearchCodec(params); + Codec codec = CuVSCodecs.gpuSearch(params); IndexWriterConfig config = new IndexWriterConfig().setCodec(codec).setUseCompoundFile(false); random = new Random(222); diff --git a/fern/docs.yml b/fern/docs.yml index 77ee0dae2a..bae1d09a60 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -567,6 +567,8 @@ navigation: path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md" - page: "CagraIndexParamsFactory" path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cagraindexparamsfactory.md" + - page: "CuVSCodecs" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs.md" - page: "FilterBitsetCacheConfig" path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-filterbitsetcacheconfig.md" - page: "FilterCuVSServiceProvider" @@ -575,8 +577,6 @@ navigation: path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpuindex.md" - page: "GPUSearchParams" path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams.md" - - page: "LuceneProvider" - path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneprovider.md" - page: "ThreadLocalCuVSResourcesProvider" path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-threadlocalcuvsresourcesprovider.md" - page: "Utils" @@ -597,6 +597,24 @@ navigation: path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery.md" - page: "Lucene101AcceleratedHNSWCodec" path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswcodec.md" + - page: "Lucene103AcceleratedHNSWBinaryQuantizedCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswbinaryquantizedcodec.md" + - page: "Lucene103AcceleratedHNSWCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswcodec.md" + - page: "Lucene103AcceleratedHNSWScalarQuantizedCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswscalarquantizedcodec.md" + - page: "Lucene103CuVSGPUSearchCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103cuvsgpusearchcodec.md" + - page: "Lucene104AcceleratedHNSWBinaryQuantizedCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswbinaryquantizedcodec.md" + - page: "Lucene104AcceleratedHNSWCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswcodec.md" + - page: "Lucene104AcceleratedHNSWScalarQuantizedCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedcodec.md" + - page: "Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedvectorsformat.md" + - page: "Lucene104CuVSGPUSearchCodec" + path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104cuvsgpusearchcodec.md" - page: "Lucene99AcceleratedHNSWVectorsFormat" path: "./pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorsformat.md" - page: "Lucene99AcceleratedHNSWVectorsWriter" diff --git a/fern/pages/installation/java.md b/fern/pages/installation/java.md index cb91be0a9e..479a2c36a0 100644 --- a/fern/pages/installation/java.md +++ b/fern/pages/installation/java.md @@ -69,22 +69,33 @@ java/panama-bindings/generate-bindings.sh `cuvs-lucene` provides Apache Lucene codecs that offload vector index build, and optionally search, to the GPU. It is published as a separate artifact that builds on the Java API described above. For usage guidance, see the [Lucene Integration](/user-guide/lucene) guide. -`cuvs-lucene` targets Lucene 10.2, requires JDK 22 and Maven 3.9.6 or newer, and inherits the NVIDIA cuVS [CUDA GPU requirements](/installation#cuda-gpu-requirements). +`cuvs-lucene` supports Lucene 10.2 through 10.5 and requires Maven 3.9.6 or newer. It compiles and runs on JDK 21, the minimum of Lucene 10, but uses the GPU only on a JDK 22 or newer runtime: on JDK 21 the accelerated HNSW codecs build graphs on the CPU and the GPU search codec is unavailable. It inherits the NVIDIA cuVS [CUDA GPU requirements](/installation#cuda-gpu-requirements). -`Lucene101AcceleratedHNSWCodec` falls back to CPU index construction when no GPU or native library is available, so an application using it works on GPU and non-GPU hosts alike. The other three codecs require a working cuVS installation. See [Lucene Integration](/user-guide/lucene) for details. +The accelerated HNSW codec falls back to CPU index construction when no GPU or native library is available, so an application using it works on GPU and non-GPU hosts alike. The other three codecs require a working cuVS installation. See [Lucene Integration](/user-guide/lucene) for details. ### Add the Maven Dependency -To use `cuvs-lucene` in a Maven project, add the following dependency to your `pom.xml`: +Lucene changes its codec APIs between minor releases, so `cuvs-lucene` is published as one artifact per supported Lucene minor release. Pick the one that matches the Lucene version your application uses: + +| Lucene | Artifact | +| --- | --- | +| 10.2.x | `cuvs-lucene-10.2` | +| 10.3.x | `cuvs-lucene-10.3` | +| 10.4.x | `cuvs-lucene-10.4` | +| 10.5.x | `cuvs-lucene-10.5` | + +For example, for Lucene 10.5, add the following dependency to your `pom.xml`: ```xml com.nvidia.cuvs.lucene - cuvs-lucene - 26.10.0 + cuvs-lucene-10.5 + 26.12.0 ``` +An artifact refuses to load against any other Lucene minor release, with an error naming the artifact to use instead. Releases before 26.12 published a single `cuvs-lucene` artifact, built for Lucene 10.2. Its coordinates are relocated to `cuvs-lucene-10.2`, so Maven builds that only bump its version to 26.12 or later keep working and print a warning; switch to the artifact matching your Lucene version. Build tools other than Maven may not follow the relocation; update the artifact name there. + The native NVIDIA cuVS libraries are not bundled with the artifact. Install a matching version of `libcuvs` and `libcuvs_c` as described above, and make sure the directory containing them is on `LD_LIBRARY_PATH` before starting the JVM. ### Build cuVS Lucene From Source @@ -108,6 +119,6 @@ cd java/cuvs-lucene ./build.sh ``` -The resulting artifacts are written to `java/cuvs-lucene/target`. +This builds and installs every `cuvs-lucene-10.X` artifact into the local Maven repository. The resulting artifacts are written to `java/cuvs-lucene/lucene-10.X/target`. Add `--run-java-tests` to any of these commands to run the test suite, and `--build-java-examples` to build the example projects against the jars that were just built. Each target builds only its own examples: the `lucene` target builds `examples/java/cuvs-lucene`, and the `java` target builds `examples/java/cuvs-java`. diff --git a/fern/pages/lucene_api/index.md b/fern/pages/lucene_api/index.md index 3acfc3b798..0591cab41c 100644 --- a/fern/pages/lucene_api/index.md +++ b/fern/pages/lucene_api/index.md @@ -1,6 +1,6 @@ # Lucene API Documentation -These pages are generated from the Java source files in `java/cuvs-lucene/src/main`. +These pages are generated from the Java source files in `java/cuvs-lucene/src/main` and `java/cuvs-lucene/lucene-*/src/since`. For an introduction to the codecs, configuration, and tuning, see the [Lucene Integration](/user-guide/lucene) guide. @@ -9,11 +9,11 @@ For an introduction to the codecs, configuration, and tuning, see the [Lucene In - [AcceleratedHNSWParams](/api-reference/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswparams) - [AcceleratedHNSWUtils](/api-reference/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils) - [CagraIndexParamsFactory](/api-reference/lucene-api-com-nvidia-cuvs-lucene-cagraindexparamsfactory) +- [CuVSCodecs](/api-reference/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs) - [FilterBitsetCacheConfig](/api-reference/lucene-api-com-nvidia-cuvs-lucene-filterbitsetcacheconfig) - [FilterCuVSServiceProvider](/api-reference/lucene-api-com-nvidia-cuvs-lucene-filtercuvsserviceprovider) - [GPUIndex](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpuindex) - [GPUSearchParams](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams) -- [LuceneProvider](/api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneprovider) - [ThreadLocalCuVSResourcesProvider](/api-reference/lucene-api-com-nvidia-cuvs-lucene-threadlocalcuvsresourcesprovider) - [Utils](/api-reference/lucene-api-com-nvidia-cuvs-lucene-utils) @@ -27,6 +27,15 @@ For an introduction to the codecs, configuration, and tuning, see the [Lucene In - [GPUBuiltHnswGraph](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpubuilthnswgraph) - [GPUKnnFloatVectorQuery](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery) - [Lucene101AcceleratedHNSWCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswcodec) +- [Lucene103AcceleratedHNSWBinaryQuantizedCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswbinaryquantizedcodec) +- [Lucene103AcceleratedHNSWCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswcodec) +- [Lucene103AcceleratedHNSWScalarQuantizedCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswscalarquantizedcodec) +- [Lucene103CuVSGPUSearchCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103cuvsgpusearchcodec) +- [Lucene104AcceleratedHNSWBinaryQuantizedCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswbinaryquantizedcodec) +- [Lucene104AcceleratedHNSWCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswcodec) +- [Lucene104AcceleratedHNSWScalarQuantizedCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedcodec) +- [Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedvectorsformat) +- [Lucene104CuVSGPUSearchCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104cuvsgpusearchcodec) - [Lucene99AcceleratedHNSWVectorsFormat](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorsformat) - [Lucene99AcceleratedHNSWVectorsWriter](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter) - [LuceneAcceleratedHNSWBinaryQuantizedCodec](/api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedcodec) diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md index 219149bc6d..ab57636443 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md @@ -21,7 +21,7 @@ public static GPUBuiltHnswGraph createSingleVectorHnswGraph(int size, int dimens Creates a dummy HNSW graph for a single vector. The graph will have 1 level with 1 node and no neighbors. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:54`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:46`_ ### createMultiLayerHnswGraph @@ -35,7 +35,7 @@ M = ceil(cagraGraphDegree / 2), where cagraGraphDegree is the CAGRA adjacency li Each layer contains 1/M nodes from the previous layer Creates layers until the highest layer has ≤ M nodes -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:81`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:73`_ ### writeGraph @@ -62,7 +62,7 @@ a 2D array of offsets | --- | --- | | `IOException` | I/O Exceptions | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:237`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:244`_ ### writeMeta @@ -91,7 +91,7 @@ Writes the meta information for the index. | --- | --- | | `IOException` | I/O Exceptions | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:302`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:309`_ ### printInfoStream @@ -107,7 +107,7 @@ A utility method to print info/debugging messages using InfoStream. | --- | --- | | `msg` | the debugging message to print | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:384`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:391`_ ### writeEmpty @@ -129,7 +129,7 @@ Writes an empty meta information for the field. | --- | --- | | `IOException` | I/O Exceptions | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:396`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:403`_ ### quantizeFloatVectorsToBinary @@ -152,7 +152,7 @@ Bits are packed: 8 dimensions per byte. A list of byte binary representation for the input vectors -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:409`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:416`_ ### quantizeFloatVectorsToScalar @@ -172,6 +172,6 @@ Scalar quantization. A list of byte scalar representation for the input vectors -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:451`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:458`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:31`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:32`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpusearchcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpusearchcodec.md index c7120dc9d4..e1f8c932fc 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpusearchcodec.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpusearchcodec.md @@ -7,29 +7,26 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpusearchcodec _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class CuVS2510GPUSearchCodec extends FilterCodec +public class CuVS2510GPUSearchCodec extends CuVSFilterCodec ``` cuVS based codec for GPU based vector search that enables both - indexing and search on the GPU. cuVS serialization formats are in experimental phase and hence backward compatibility cannot be guaranteed. +This codec wraps `Lucene101Codec`, the default codec of Lucene 10.2. On later Lucene +releases it can only read existing indexes; use `CuVSCodecs#gpuSearch` to write. + ## Public Members ### CuVS2510GPUSearchCodec ```java -public CuVS2510GPUSearchCodec() throws Exception +public CuVS2510GPUSearchCodec() ``` Default constructor for `CuVS2510GPUSearchCodec`. -**Throws** - -| Type | Description | -| --- | --- | -| `Exception` | | - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:30`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:24`_ ### CuVS2510GPUSearchCodec @@ -47,12 +44,12 @@ having default parameter values. | `name` | the name of the codec | | `delegate` | the delegate codec | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:45`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:35`_ ### CuVS2510GPUSearchCodec ```java -public CuVS2510GPUSearchCodec(GPUSearchParams params) throws Exception +public CuVS2510GPUSearchCodec(GPUSearchParams params) ``` Initialize the codec with an instance of `GPUSearchParams` having either default @@ -64,18 +61,12 @@ or overridden parameter values. | --- | --- | | `params` | An instance of `GPUSearchParams` | -**Throws** - -| Type | Description | -| --- | --- | -| `Exception` | Exception raised when initializing the codec | - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:56`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:45`_ ### CuVS2510GPUSearchCodec ```java -public CuVS2510GPUSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) throws Exception +public CuVS2510GPUSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) ``` Initialize the codec with GPU search and filter-bitset-cache parameters. @@ -87,13 +78,7 @@ Initialize the codec with GPU search and filter-bitset-cache parameters. | `params` | GPU index and search parameters | | `filterCacheConfig` | filter-bitset-cache configuration | -**Throws** - -| Type | Description | -| --- | --- | -| `Exception` | Exception raised when initializing the codec | - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:67`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:55`_ ### CuVS2510GPUSearchCodec @@ -112,36 +97,6 @@ Initialize a named codec with explicit delegate, GPU search, and filter-cache pa | `params` | GPU index and search parameters | | `filterCacheConfig` | filter-bitset-cache configuration | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:80`_ - -### knnVectorsFormat - -```java -@Override public KnnVectorsFormat knnVectorsFormat() -``` - -Get the configured `KnnVectorsFormat`. - -**Returns** - -the instance of the `KnnVectorsFormat` - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:111`_ - -### setKnnFormat - -```java -public void setKnnFormat(KnnVectorsFormat format) -``` - -Set the `KnnVectorsFormat`. - -**Parameters** - -| Name | Description | -| --- | --- | -| `format` | the `KnnVectorsFormat` to set | - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:121`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:71`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:20`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsformat.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsformat.md index b747d12460..1a49ab1a01 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsformat.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsformat.md @@ -29,7 +29,7 @@ Initializes the `CuVS2510GPUVectorsFormat` with default parameter values. | --- | --- | | `LibraryException` | if the native library fails to load | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:55`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:46`_ ### CuVS2510GPUVectorsFormat @@ -51,7 +51,7 @@ Initializes the `CuVS2510GPUVectorsFormat` with an instance of `GPUSearchParams` | --- | --- | | `LibraryException` | if the native library fails to load | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:65`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:56`_ ### CuVS2510GPUVectorsFormat @@ -74,7 +74,7 @@ Initializes the format with GPU search and filter-bitset-cache parameters. | --- | --- | | `LibraryException` | if the native library fails to load | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:76`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:67`_ ### fieldsWriter @@ -84,7 +84,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Returns a KnnVectorsReader instance to write the vectors to the index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:86`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:77`_ ### fieldsReader @@ -94,7 +94,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Returns a KnnVectorsReader instance to read the vectors from the index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:96`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:88`_ ### getMaxDimensions @@ -104,6 +104,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Returns the maximum number of vector dimensions supported by this codec for the given field name. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:106`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:99`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:25`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java:26`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsreader.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsreader.md index 1899eae6a9..2114a71a67 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsreader.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsreader.md @@ -7,7 +7,7 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorsreader _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class CuVS2510GPUVectorsReader extends KnnVectorsReader +public class CuVS2510GPUVectorsReader extends CompatKnnVectorsReader ``` KnnVectorsReader instance associated with cuVS format for reading vectors from an index. @@ -35,7 +35,7 @@ Initializes the `CuVS2510GPUVectorsReader`, checks and loads the index. | --- | --- | | `IOException` | I/O exception | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:87`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:77`_ ### CuVS2510GPUVectorsReader @@ -59,7 +59,7 @@ Initializes the reader with the cache owned by its vectors format. | --- | --- | | `IOException` | I/O exception | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:100`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:90`_ ### readSimilarityFunction @@ -85,7 +85,7 @@ an instance of VectorSimilarityFunction | --- | --- | | `IOException` | | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:228`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:219`_ ### readVectorEncoding @@ -111,7 +111,7 @@ the vector encoding | --- | --- | | `IOException` | | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:243`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:234`_ ### getFieldEntry @@ -132,7 +132,7 @@ cuVS index for it. the meta information for the field, or `null` -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:306`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:297`_ ### openCagraIndexForMerge @@ -165,7 +165,7 @@ a freshly loaded CAGRA index, or `null` if this segment has none for the field | --- | --- | | `IOException` | I/O exception | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:325`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:316`_ ### close @@ -175,7 +175,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Closes the resources. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:394`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:385`_ ### checkIntegrity @@ -185,7 +185,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Checks consistency of this reader. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:412`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:403`_ ### getFloatVectorValues @@ -195,7 +195,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Returns the FloatVectorValues for the given field. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:421`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:412`_ ### getByteVectorValues @@ -207,29 +207,29 @@ Returns the ByteVectorValues for the given field. This is not supported. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:431`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:422`_ -### search +### doSearch ```java -@Override public void search(String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException +@Override protected void doSearch(String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException ``` Returns the k nearest neighbor documents using cuVS's CAGRA or brute force algorithm for this field, to the given vector. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:455`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:446`_ -### search +### doSearch ```java -@Override public void search(String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException +@Override protected void doSearch(String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException ``` Return the k nearest neighbor documents as determined by comparison of their vector values for this field, to the given vector. This is not supported. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:609`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:600`_ ### readEntry @@ -257,7 +257,7 @@ an instance of FieldEntry | --- | --- | | `IOException` | I/O Exceptions | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:637`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:628`_ ### getCagraIndexForField @@ -278,7 +278,7 @@ Returns the `CagraIndex` for the given field, or `null` if unavailable the CAGRA index, or `null` -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:689`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:680`_ ### getFilterBitsetCache @@ -288,7 +288,7 @@ FilterBitsetCache getFilterBitsetCache() Returns the filter cache owned by the vectors format that created this reader. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:699`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:690`_ ### getFieldInfos @@ -302,7 +302,7 @@ Gets the instance of FieldInfos. the instance of FieldInfos -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:708`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:699`_ ### getCuvsIndexes @@ -316,7 +316,7 @@ Gets the map of `GPUIndex` objects. the map of GPU index objects -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:717`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:708`_ ### getFieldEntries @@ -330,6 +330,6 @@ Gets the map of FieldEntry objects that hold the meta information for the field. the map of FieldEntry objects -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:726`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:717`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:59`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsReader.java:58`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorswriter.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorswriter.md index df1abbbdc7..f80303e7f8 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorswriter.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorswriter.md @@ -7,7 +7,7 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpuvectorswriter _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class CuVS2510GPUVectorsWriter extends KnnVectorsWriter +public class CuVS2510GPUVectorsWriter extends CompatKnnVectorsWriter ``` extends upon KnnVectorsWriter and has implementation for critical methods like flush, merge etc. @@ -22,7 +22,7 @@ CAGRA(true, false), /** Builds a Brute Force index. */ BRUTE_FORCE(false, true), Builds a CAGRA index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:89`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:81`_ ### BRUTE_FORCE @@ -32,7 +32,7 @@ BRUTE_FORCE(false, true), /** Builds both - CAGRA and Brute Force indexes. */ CA Builds a Brute Force index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:92`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:84`_ ### CAGRA_AND_BRUTE_FORCE @@ -42,7 +42,7 @@ CAGRA_AND_BRUTE_FORCE(true, true) Builds both - CAGRA and Brute Force indexes. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:95`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:87`_ ### CuVS2510GPUVectorsWriter @@ -66,7 +66,7 @@ Initializes `CuVS2510GPUVectorsWriter`. | --- | --- | | `IOException` | I/O exceptions | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:121`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:113`_ ### addField @@ -76,7 +76,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Add new field for indexing. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:160`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:152`_ ### flush @@ -86,17 +86,17 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Creates the CAGRA and/or brute force indexes and writes them to the disk. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:303`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:295`_ -### mergeOneField +### doMergeOneField ```java -@Override public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException +@Override protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException ``` Write field for merging. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:685`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:677`_ ### ramBytesUsed @@ -106,7 +106,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Returns the memory usage of this object in bytes. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:696`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:688`_ ### finish @@ -116,7 +116,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Called once at the end before close. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:708`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:700`_ ### close @@ -126,6 +126,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVecto Close the applicable resources. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:728`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:720`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:59`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsWriter.java:60`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs.md new file mode 100644 index 0000000000..d9dfef837c --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs.md @@ -0,0 +1,232 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs +--- + +# CuVSCodecs + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public final class CuVSCodecs +``` + +Creates the cuvs-lucene codecs and vectors formats that write indexes on the Lucene release this +artifact is built for. + +Each cuvs-lucene codec wraps the default codec of one Lucene release, so the codec to write +with changes along with Lucene: `Lucene101AcceleratedHNSWCodec` on Lucene 10.2, \{@code +Lucene103AcceleratedHNSWCodec\} on 10.3, `Lucene104AcceleratedHNSWCodec` on 10.4 and later. +The older codecs stay available for reading existing indexes. Creating codecs through this class +instead of naming them keeps application code unchanged when it moves to another Lucene release +and the matching cuvs-lucene artifact. + +Every method throws `IllegalStateException` if the Lucene release in use is not the one +this cuvs-lucene artifact is built for. + +## Public Members + +### checkLuceneVersion + +```java +public static void checkLuceneVersion() +``` + +Checks that the Lucene release in use is the one this cuvs-lucene artifact is built for. + +A cuvs-lucene artifact on the wrong Lucene release does not break Lucene: Lucene keeps +working, and so do indexes that do not use the cuvs-lucene codecs, while using one of these +codecs fails. Applications that would rather not start at all in that case can call this method +during startup. + +**Throws** + +| Type | Description | +| --- | --- | +| `IllegalStateException` | if the artifact is not built for the Lucene release in use; the message names the artifact to use instead | + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:40`_ + +### acceleratedHNSW + +```java +public static Codec acceleratedHNSW() +``` + +Returns the codec that builds HNSW graphs on the GPU and searches them on the CPU, with default +parameters. + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:50`_ + +### acceleratedHNSW + +```java +public static Codec acceleratedHNSW(AcceleratedHNSWParams params) +``` + +Returns the codec that builds HNSW graphs on the GPU and searches them on the CPU. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | the index build parameters | + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:60`_ + +### acceleratedHNSWScalarQuantized + +```java +public static Codec acceleratedHNSWScalarQuantized() +``` + +Returns the codec that builds HNSW graphs on the GPU over scalar-quantized vectors, with +default parameters. + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:71`_ + +### acceleratedHNSWScalarQuantized + +```java +public static Codec acceleratedHNSWScalarQuantized(AcceleratedHNSWParams params) +``` + +Returns the codec that builds HNSW graphs on the GPU over scalar-quantized vectors. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | the index build parameters | + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:81`_ + +### acceleratedHNSWScalarQuantizedFormat + +```java +public static KnnVectorsFormat acceleratedHNSWScalarQuantizedFormat( AcceleratedHNSWParams params) +``` + +Returns the vectors format used by \{@link +#acceleratedHNSWScalarQuantized(AcceleratedHNSWParams)\}, for use with a per-field codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | the index build parameters | + +**Returns** + +the vectors format + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:93`_ + +### acceleratedHNSWBinaryQuantized + +```java +public static Codec acceleratedHNSWBinaryQuantized() +``` + +Returns the codec that builds HNSW graphs on the GPU over binary-quantized vectors, with +default parameters. + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:105`_ + +### acceleratedHNSWBinaryQuantized + +```java +public static Codec acceleratedHNSWBinaryQuantized(AcceleratedHNSWParams params) +``` + +Returns the codec that builds HNSW graphs on the GPU over binary-quantized vectors. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | the index build parameters | + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:115`_ + +### gpuSearch + +```java +public static Codec gpuSearch() +``` + +Returns the codec that both builds and searches CAGRA indexes on the GPU, with default +parameters. + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:126`_ + +### gpuSearch + +```java +public static Codec gpuSearch(GPUSearchParams params) +``` + +Returns the codec that both builds and searches CAGRA indexes on the GPU, with the default +filter-bitset-cache configuration. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | the GPU index and search parameters | + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:137`_ + +### gpuSearch + +```java +public static Codec gpuSearch(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) +``` + +Returns the codec that both builds and searches CAGRA indexes on the GPU. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | the GPU index and search parameters | +| `filterCacheConfig` | the filter-bitset-cache configuration | + +**Returns** + +the codec + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:148`_ + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java:26`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery.md index cb11784c0d..31601795a6 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery.md @@ -7,7 +7,7 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class GPUKnnFloatVectorQuery extends KnnFloatVectorQuery +public class GPUKnnFloatVectorQuery extends CompatKnnFloatVectorQuery ``` Extends `KnnFloatVectorQuery` for GPU-only search. @@ -62,7 +62,7 @@ and max_iterations auto-selected (0). | `iTopK` | CAGRA itopk_size parameter | | `searchWidth` | CAGRA search_width parameter | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:100`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:99`_ ### GPUKnnFloatVectorQuery @@ -86,6 +86,6 @@ Initializes `GPUKnnFloatVectorQuery`. | `maxIterations` | CAGRA max_iterations (0 = auto) | | `searchAlgo` | CAGRA search algorithm | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:118`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:117`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:82`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java:81`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswcodec.md index 7f7dde5bbf..39c77f6df1 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswcodec.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswcodec.md @@ -7,30 +7,27 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswco _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class Lucene101AcceleratedHNSWCodec extends FilterCodec +public class Lucene101AcceleratedHNSWCodec extends CuVSFilterCodec ``` A codec that enables GPU-based accelerated HNSW capability and can be used to accelerated indexing using GPUs and search using CPUs. Fallbacks to CPU based indexing when used on a machine without a GPU and/or cuVS. +This codec wraps `Lucene101Codec`, the default codec of Lucene 10.2. On later Lucene +releases it can only read existing indexes; use `CuVSCodecs#acceleratedHNSW` to write. + ## Public Members ### Lucene101AcceleratedHNSWCodec ```java -public Lucene101AcceleratedHNSWCodec() throws Exception +public Lucene101AcceleratedHNSWCodec() ``` Default constructor for `Lucene101AcceleratedHNSWCodec`. -**Throws** - -| Type | Description | -| --- | --- | -| `Exception` | | - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:31`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:25`_ ### Lucene101AcceleratedHNSWCodec @@ -47,12 +44,12 @@ Constructor for `Lucene101AcceleratedHNSWCodec`. | `name` | the codec's name | | `delegate` | the delegate codec to filter | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:41`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:35`_ ### Lucene101AcceleratedHNSWCodec ```java -public Lucene101AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) throws Exception +public Lucene101AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) ``` Constructor for `Lucene101AcceleratedHNSWCodec`. @@ -63,42 +60,6 @@ Constructor for `Lucene101AcceleratedHNSWCodec`. | --- | --- | | `acceleratedHNSWParams` | instance of `AcceleratedHNSWParams` | -**Throws** - -| Type | Description | -| --- | --- | -| `Exception` | exception | - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:52`_ - -### knnVectorsFormat - -```java -@Override public KnnVectorsFormat knnVectorsFormat() -``` - -Get the configured `KnnVectorsFormat`. - -**Returns** - -the instance of the `KnnVectorsFormat` - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:87`_ - -### setKnnFormat - -```java -public void setKnnFormat(KnnVectorsFormat format) -``` - -Set the `KnnVectorsFormat`. - -**Parameters** - -| Name | Description | -| --- | --- | -| `format` | the `KnnVectorsFormat` to set | - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:97`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:48`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:21`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java:19`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswbinaryquantizedcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswbinaryquantizedcodec.md new file mode 100644 index 0000000000..4503bd6608 --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswbinaryquantizedcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswbinaryquantizedcodec +--- + +# Lucene103AcceleratedHNSWBinaryQuantizedCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene103AcceleratedHNSWBinaryQuantizedCodec extends CuVSFilterCodec +``` + +A codec that builds HNSW graphs on the GPU over binary-quantized vectors, and writes them in +Lucene's HNSW format so that they are searched on the CPU. See \{@link +LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat\}. + +This codec wraps `Lucene103Codec`, the default codec of Lucene 10.3. Once a later Lucene +release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#acceleratedHNSWBinaryQuantized\} to write. + +## Public Members + +### Lucene103AcceleratedHNSWBinaryQuantizedCodec + +```java +public Lucene103AcceleratedHNSWBinaryQuantizedCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWBinaryQuantizedCodec.java:22`_ + +### Lucene103AcceleratedHNSWBinaryQuantizedCodec + +```java +public Lucene103AcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `acceleratedHNSWParams` | the index build parameters | + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWBinaryQuantizedCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWBinaryQuantizedCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswcodec.md new file mode 100644 index 0000000000..e3826696b9 --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswcodec +--- + +# Lucene103AcceleratedHNSWCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene103AcceleratedHNSWCodec extends CuVSFilterCodec +``` + +A codec that builds HNSW graphs on the GPU, and writes them in Lucene's HNSW format so that they +are searched on the CPU. Falls back to building the graph on the CPU when no GPU or cuVS is +available. + +This codec wraps `Lucene103Codec`, the default codec of Lucene 10.3. Once a later Lucene +release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#acceleratedHNSW\} to write. + +## Public Members + +### Lucene103AcceleratedHNSWCodec + +```java +public Lucene103AcceleratedHNSWCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWCodec.java:22`_ + +### Lucene103AcceleratedHNSWCodec + +```java +public Lucene103AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `acceleratedHNSWParams` | the index build parameters | + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswscalarquantizedcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswscalarquantizedcodec.md new file mode 100644 index 0000000000..1703e2cd84 --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswscalarquantizedcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103acceleratedhnswscalarquantizedcodec +--- + +# Lucene103AcceleratedHNSWScalarQuantizedCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene103AcceleratedHNSWScalarQuantizedCodec extends CuVSFilterCodec +``` + +A codec that builds HNSW graphs on the GPU over scalar-quantized vectors, and writes them in +Lucene's HNSW format so that they are searched on the CPU. See \{@link +LuceneAcceleratedHNSWScalarQuantizedVectorsFormat\}. + +This codec wraps `Lucene103Codec`, the default codec of Lucene 10.3. Once a later Lucene +release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#acceleratedHNSWScalarQuantized\} to write. + +## Public Members + +### Lucene103AcceleratedHNSWScalarQuantizedCodec + +```java +public Lucene103AcceleratedHNSWScalarQuantizedCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWScalarQuantizedCodec.java:22`_ + +### Lucene103AcceleratedHNSWScalarQuantizedCodec + +```java +public Lucene103AcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `acceleratedHNSWParams` | the index build parameters | + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWScalarQuantizedCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWScalarQuantizedCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103cuvsgpusearchcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103cuvsgpusearchcodec.md new file mode 100644 index 0000000000..55a3807022 --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene103cuvsgpusearchcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene103cuvsgpusearchcodec +--- + +# Lucene103CuVSGPUSearchCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene103CuVSGPUSearchCodec extends CuVSFilterCodec +``` + +A codec that both builds and searches CAGRA indexes on the GPU. cuVS serialization formats are in +experimental phase and hence backward compatibility cannot be guaranteed. + +This codec wraps `Lucene103Codec`, the default codec of Lucene 10.3. Once a later Lucene +release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#gpuSearch\} to write. + +## Public Members + +### Lucene103CuVSGPUSearchCodec + +```java +public Lucene103CuVSGPUSearchCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103CuVSGPUSearchCodec.java:21`_ + +### Lucene103CuVSGPUSearchCodec + +```java +public Lucene103CuVSGPUSearchCodec( GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | GPU index and search parameters | +| `filterCacheConfig` | filter-bitset-cache configuration | + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103CuVSGPUSearchCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103CuVSGPUSearchCodec.java:17`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswbinaryquantizedcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswbinaryquantizedcodec.md new file mode 100644 index 0000000000..26335b4c48 --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswbinaryquantizedcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswbinaryquantizedcodec +--- + +# Lucene104AcceleratedHNSWBinaryQuantizedCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene104AcceleratedHNSWBinaryQuantizedCodec extends CuVSFilterCodec +``` + +A codec that builds HNSW graphs on the GPU over binary-quantized vectors, and writes them in +Lucene's HNSW format so that they are searched on the CPU. See \{@link +LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat\}. + +This codec wraps `Lucene104Codec`, the default codec of Lucene 10.4 and 10.5. Once a +later Lucene release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#acceleratedHNSWBinaryQuantized\} to write. + +## Public Members + +### Lucene104AcceleratedHNSWBinaryQuantizedCodec + +```java +public Lucene104AcceleratedHNSWBinaryQuantizedCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWBinaryQuantizedCodec.java:22`_ + +### Lucene104AcceleratedHNSWBinaryQuantizedCodec + +```java +public Lucene104AcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `acceleratedHNSWParams` | the index build parameters | + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWBinaryQuantizedCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWBinaryQuantizedCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswcodec.md new file mode 100644 index 0000000000..60d27bdd2e --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswcodec +--- + +# Lucene104AcceleratedHNSWCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene104AcceleratedHNSWCodec extends CuVSFilterCodec +``` + +A codec that builds HNSW graphs on the GPU, and writes them in Lucene's HNSW format so that they +are searched on the CPU. Falls back to building the graph on the CPU when no GPU or cuVS is +available. + +This codec wraps `Lucene104Codec`, the default codec of Lucene 10.4 and 10.5. Once a +later Lucene release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#acceleratedHNSW\} to write. + +## Public Members + +### Lucene104AcceleratedHNSWCodec + +```java +public Lucene104AcceleratedHNSWCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWCodec.java:22`_ + +### Lucene104AcceleratedHNSWCodec + +```java +public Lucene104AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `acceleratedHNSWParams` | the index build parameters | + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedcodec.md new file mode 100644 index 0000000000..fb5d35d6ec --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedcodec +--- + +# Lucene104AcceleratedHNSWScalarQuantizedCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene104AcceleratedHNSWScalarQuantizedCodec extends CuVSFilterCodec +``` + +A codec that builds HNSW graphs on the GPU over scalar-quantized vectors, and writes them in +Lucene's HNSW format so that they are searched on the CPU. See \{@link +Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat\}. + +This codec wraps `Lucene104Codec`, the default codec of Lucene 10.4 and 10.5. Once a +later Lucene release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#acceleratedHNSWScalarQuantized\} to write. + +## Public Members + +### Lucene104AcceleratedHNSWScalarQuantizedCodec + +```java +public Lucene104AcceleratedHNSWScalarQuantizedCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedCodec.java:22`_ + +### Lucene104AcceleratedHNSWScalarQuantizedCodec + +```java +public Lucene104AcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `acceleratedHNSWParams` | the index build parameters | + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedvectorsformat.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedvectorsformat.md new file mode 100644 index 0000000000..163477cd0a --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedvectorsformat.md @@ -0,0 +1,48 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104acceleratedhnswscalarquantizedvectorsformat +--- + +# Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat extends BaseAcceleratedHNSWScalarQuantizedVectorsFormat +``` + +cuVS based scalar-quantized KnnVectorsFormat for indexing on the GPU and searching on the CPU. + +Stores the vectors with Lucene's `Lucene104ScalarQuantizedVectorsFormat`, quantized to 7 +bits per dimension, which replaced `Lucene99ScalarQuantizedVectorsFormat` in Lucene 10.4. +The graph is built on the GPU over the vectors quantized the same way as \{@link +LuceneAcceleratedHNSWScalarQuantizedVectorsFormat\} does. + +## Public Members + +### Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat + +```java +public Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat() +``` + +Creates the format with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java:20`_ + +### Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat + +```java +public Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat( AcceleratedHNSWParams acceleratedHNSWParams) +``` + +Creates the format. + +**Parameters** + +| Name | Description | +| --- | --- | +| `acceleratedHNSWParams` | the index build parameters | + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java:29`_ + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java:17`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104cuvsgpusearchcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104cuvsgpusearchcodec.md new file mode 100644 index 0000000000..b1688d0e0c --- /dev/null +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene104cuvsgpusearchcodec.md @@ -0,0 +1,49 @@ +--- +slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene104cuvsgpusearchcodec +--- + +# Lucene104CuVSGPUSearchCodec + +_Java package: `com.nvidia.cuvs.lucene`_ + +```java +public class Lucene104CuVSGPUSearchCodec extends CuVSFilterCodec +``` + +A codec that both builds and searches CAGRA indexes on the GPU. cuVS serialization formats are in +experimental phase and hence backward compatibility cannot be guaranteed. + +This codec wraps `Lucene104Codec`, the default codec of Lucene 10.4 and 10.5. Once a +later Lucene release replaces that default, this codec can only read existing indexes; use \{@link +CuVSCodecs#gpuSearch\} to write. + +## Public Members + +### Lucene104CuVSGPUSearchCodec + +```java +public Lucene104CuVSGPUSearchCodec() +``` + +Creates the codec with default parameters. + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104CuVSGPUSearchCodec.java:21`_ + +### Lucene104CuVSGPUSearchCodec + +```java +public Lucene104CuVSGPUSearchCodec( GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) +``` + +Creates the codec. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | GPU index and search parameters | +| `filterCacheConfig` | filter-bitset-cache configuration | + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104CuVSGPUSearchCodec.java:31`_ + +_Source: `java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104CuVSGPUSearchCodec.java:17`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorsformat.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorsformat.md index e84e99efc6..6ca9542c7e 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorsformat.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorsformat.md @@ -29,7 +29,7 @@ of `AcceleratedHNSWParams` with default parameter values. | --- | --- | | `LibraryException` | if the native library fails to load | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:56`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:47`_ ### Lucene99AcceleratedHNSWVectorsFormat @@ -46,7 +46,7 @@ of `AcceleratedHNSWParams`. | --- | --- | | `acceleratedHNSWParams` | An instance of `AcceleratedHNSWParams` | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:66`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:57`_ ### fieldsWriter @@ -56,7 +56,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Returns a KnnVectorsWriter to write the vectors to the index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:74`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:65`_ ### fieldsReader @@ -66,7 +66,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Returns a KnnVectorsReader to read the vectors from the index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:101`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:83`_ ### getMaxDimensions @@ -76,6 +76,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Returns the maximum number of vector dimensions supported by this codec for the given field name. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:114`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:92`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:27`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java:28`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md index 2f30caeb07..9dcea78384 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md @@ -7,7 +7,7 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvec _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class Lucene99AcceleratedHNSWVectorsWriter extends KnnVectorsWriter +public class Lucene99AcceleratedHNSWVectorsWriter extends CompatKnnVectorsWriter ``` This class extends upon the KnnVectorsWriter to @@ -37,7 +37,7 @@ Initializes `Lucene99AcceleratedHNSWVectorsWriter` | --- | --- | | `IOException` | IOException | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:86`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:80`_ ### addField @@ -47,7 +47,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Add new field for indexing. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:128`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:118`_ ### flush @@ -57,17 +57,17 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Build the indexes and writes it to the disk. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:203`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:193`_ -### mergeOneField +### doMergeOneField ```java -@Override public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException +@Override protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException ``` Write field for merging. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:291`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:281`_ ### finish @@ -77,7 +77,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Called once at the end before close. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:300`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:290`_ ### close @@ -87,7 +87,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Closes the resources. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:320`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:310`_ ### ramBytesUsed @@ -97,6 +97,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Returns the memory usage of this object in bytes. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:330`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:320`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:52`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:53`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedcodec.md index 981e432792..9f4f26f85e 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedcodec.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedcodec.md @@ -7,9 +7,13 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinar _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class LuceneAcceleratedHNSWBinaryQuantizedCodec extends FilterCodec +public class LuceneAcceleratedHNSWBinaryQuantizedCodec extends CuVSFilterCodec ``` CuVS based codec for GPU based vector search -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedCodec.java:19`_ +This codec, named `Lucene101AcceleratedHNSWBinaryQuantizedCodec`, wraps \{@code +Lucene101Codec\}, the default codec of Lucene 10.2. On later Lucene releases it can only read +existing indexes; use `CuVSCodecs#acceleratedHNSWBinaryQuantized` to write. + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorsformat.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorsformat.md index b574aff9b1..32431c2596 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorsformat.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorsformat.md @@ -12,6 +12,10 @@ public class LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat extends KnnVector cuVS based Binary Quantized KnnVectorsFormat for indexing on GPU and searching on the CPU. +The vectors are binary quantized only to build the graph on the GPU. The index stores them in +full precision, with Lucene's `Lucene99FlatVectorsFormat`, and search scores them in full +precision too. + ## Public Members ### LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat @@ -28,7 +32,7 @@ Initializes `LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat` with default val | --- | --- | | `LibraryException` | if the native library fails to load | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:52`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:46`_ ### LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat @@ -44,7 +48,7 @@ Initializes `LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat` with the given t | --- | --- | | `acceleratedHNSWParams` | An instance of `AcceleratedHNSWParams` | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:61`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:55`_ ### fieldsWriter @@ -54,7 +58,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Returns a KnnVectorsWriter to write the binary quantized vectors to the index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:70`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:64`_ ### fieldsReader @@ -64,7 +68,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Returns a KnnVectorsReader to read the binary quantized vectors from the index. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:99`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:86`_ ### getMaxDimensions @@ -74,6 +78,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Returns the maximum number of vector dimensions supported by this codec for the given field name. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:112`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:95`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:26`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java:32`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md index 91a4e67303..58f7d4cf89 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md @@ -7,7 +7,7 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinar _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter extends KnnVectorsWriter +public class LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter extends CompatKnnVectorsWriter ``` This class extends upon the KnnVectorsWriter to enable the creation of GPU-based accelerated @@ -37,7 +37,7 @@ Initializes `LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter` | --- | --- | | `IOException` | IOException | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:79`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:83`_ ### addField @@ -59,10 +59,10 @@ Build the indexes and writes it to the disk. _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:215`_ -### mergeOneField +### doMergeOneField ```java -@Override public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException +@Override protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException ``` Write field for merging. diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedcodec.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedcodec.md index e31e66c7fc..1cb168886c 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedcodec.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedcodec.md @@ -7,9 +7,13 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscala _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class LuceneAcceleratedHNSWScalarQuantizedCodec extends FilterCodec +public class LuceneAcceleratedHNSWScalarQuantizedCodec extends CuVSFilterCodec ``` CuVS based codec for GPU based vector search -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedCodec.java:19`_ +This codec, named `Lucene101AcceleratedHNSWScalarQuantizedCodec`, wraps \{@code +Lucene101Codec\}, the default codec of Lucene 10.2. On later Lucene releases it can only read +existing indexes; use `CuVSCodecs#acceleratedHNSWScalarQuantized` to write. + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedCodec.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md index 1d249bc2df..b1b32c479e 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md @@ -7,11 +7,15 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscala _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class LuceneAcceleratedHNSWScalarQuantizedVectorsFormat extends KnnVectorsFormat +public class LuceneAcceleratedHNSWScalarQuantizedVectorsFormat extends BaseAcceleratedHNSWScalarQuantizedVectorsFormat ``` cuVS based Scalar Quantized KnnVectorsFormat for indexing on GPU and searching on the CPU. +Stores the vectors with Lucene's `Lucene99ScalarQuantizedVectorsFormat`, which Lucene +10.4 moved to its backward codecs. On Lucene 10.4 and later this format can only read existing +indexes; use `CuVSCodecs#acceleratedHNSWScalarQuantizedFormat` to write. + ## Public Members ### LuceneAcceleratedHNSWScalarQuantizedVectorsFormat @@ -28,7 +32,7 @@ Initializes `LuceneAcceleratedHNSWScalarQuantizedVectorsFormat` with default val | --- | --- | | `LibraryException` | if the native library fails to load | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:47`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:25`_ ### LuceneAcceleratedHNSWScalarQuantizedVectorsFormat @@ -44,36 +48,6 @@ Initializes `LuceneAcceleratedHNSWScalarQuantizedVectorsFormat` with the given t | --- | --- | | `acceleratedHNSWParams` | An instance of `AcceleratedHNSWParams` | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:56`_ - -### fieldsWriter - -```java -@Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException -``` - -Returns a KnnVectorsWriter to write the scalar quantized vectors to the index. - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:65`_ - -### fieldsReader - -```java -@Override public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException -``` - -Returns a KnnVectorsReader to read the scalar quantized vectors from the index. - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:91`_ - -### getMaxDimensions - -```java -@Override public int getMaxDimensions(String fieldName) -``` - -Returns the maximum number of vector dimensions supported by this Codec for the given field name. - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:104`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:34`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:24`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:18`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md index 9c51cb26e9..f8523edb9e 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md @@ -7,7 +7,7 @@ slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscala _Java package: `com.nvidia.cuvs.lucene`_ ```java -public class LuceneAcceleratedHNSWScalarQuantizedVectorsWriter extends KnnVectorsWriter +public class LuceneAcceleratedHNSWScalarQuantizedVectorsWriter extends CompatKnnVectorsWriter ``` This class extends upon the KnnVectorsWriter to enable the creation of GPU-based accelerated @@ -37,7 +37,7 @@ Initializes `LuceneAcceleratedHNSWScalarQuantizedVectorsWriter` | --- | --- | | `IOException` | IOException | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:89`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:83`_ ### addField @@ -47,7 +47,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Add new field for indexing. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:136`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:126`_ ### flush @@ -57,17 +57,17 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Build the indexes and writes it to the disk. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:241`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:231`_ -### mergeOneField +### doMergeOneField ```java -@Override public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException +@Override protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException ``` Write field for merging. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:326`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:316`_ ### finish @@ -77,7 +77,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Called once at the end before close. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:357`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:347`_ ### close @@ -87,7 +87,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Closes the resources. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:377`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:367`_ ### ramBytesUsed @@ -97,6 +97,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Returns the memory usage of this object in bytes. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:386`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:376`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:56`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:57`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneprovider.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneprovider.md deleted file mode 100644 index e46ee10813..0000000000 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneprovider.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -slug: api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneprovider ---- - -# LuceneProvider - -_Java package: `com.nvidia.cuvs.lucene`_ - -```java -public class LuceneProvider -``` - -Dynamically loads Lucene format, reader, and writer classes with a fallback mechanism. - -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java:31`_ 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 0d2a9cb1a6..58e78db5c1 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 @@ -45,7 +45,29 @@ never returns; always throws | --- | --- | | `IOException` | | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:43`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:45`_ + +### cpuHnswFormat + +```java +static KnnVectorsFormat cpuHnswFormat(AcceleratedHNSWParams params) +``` + +Returns Lucene's own HNSW vectors format, configured like the given accelerated one. Used to +build the graph on the CPU when no GPU is available. It writes the same files as the GPU +writers, so the accelerated formats read its segments like their own. + +**Parameters** + +| Name | Description | +| --- | --- | +| `params` | the accelerated format's parameters | + +**Returns** + +the CPU vectors format + +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:62`_ ### createFloatMatrix @@ -70,7 +92,7 @@ without creating intermediate heap arrays. an instance of CuVSMatrix -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:63`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:83`_ ### createByteMatrix @@ -95,7 +117,7 @@ without creating intermediate heap arrays. an instance of CuVSMatrix with BYTE data type -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:92`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:112`_ ### createByteMatrixFromArray @@ -117,7 +139,7 @@ A method to build a CuVSMatrix from a 2D byte array (for binary quantized vector an instance of CuVSMatrix with BYTE data type -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:119`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:139`_ ### nanosToMillis @@ -137,7 +159,7 @@ A utility method to convert nanoseconds to milliseconds. milliseconds -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:141`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:161`_ ### cuVSResourcesOrNull @@ -151,7 +173,7 @@ Creates an instance of CuVSResources. an instance of CuVSResources -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:150`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:170`_ ### handleThrowableWithIgnore @@ -174,7 +196,7 @@ A utility method that conditionally ignores certain throwable objects | --- | --- | | `IOException` | | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:178`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:198`_ ### createListFromMergedVectors @@ -200,7 +222,7 @@ a list of float arrays | --- | --- | | `IOException` | I/O Exception | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:192`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:212`_ ### info @@ -218,6 +240,6 @@ Utility to print info/debug messages via InfoStream. | `component` | the name of the index writer | | `msg` | the log message to push via the InfoStream | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:210`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:230`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:26`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java:28`_ diff --git a/fern/pages/user_guide/lucene.md b/fern/pages/user_guide/lucene.md index 34aa9402d4..7a5a031426 100644 --- a/fern/pages/user_guide/lucene.md +++ b/fern/pages/user_guide/lucene.md @@ -6,7 +6,7 @@ slug: user-guide/lucene NVIDIA cuVS Lucene (`cuvs-lucene`) is a pluggable [KnnVectorsFormat](https://lucene.apache.org/core/10_2_0/core/org/apache/lucene/codecs/KnnVectorsFormat.html) that offloads vector index build, and optionally search, from the CPU to NVIDIA GPUs. Because it plugs in through a standard Lucene codec, an existing Lucene application adopts it by setting a codec on its `IndexWriterConfig`; the rest of the indexing and query code is unchanged. -`cuvs-lucene` targets Lucene 10.2 and is built on the [NVIDIA cuVS Java APIs](/api-reference/java-api-documentation). For installation and build instructions, see [cuVS Lucene](/installation/java#cuvs-lucene) in the Java installation guide. For class-level details, see the [Lucene API Documentation](/api-reference/lucene-api-documentation). +`cuvs-lucene` supports Lucene 10.2 through 10.5, with one artifact per Lucene minor release, and is built on the [NVIDIA cuVS Java APIs](/api-reference/java-api-documentation). For installation and build instructions, see [cuVS Lucene](/installation/java#cuvs-lucene) in the Java installation guide. For class-level details, see the [Lucene API Documentation](/api-reference/lucene-api-documentation). ## When To Use It @@ -18,26 +18,40 @@ If you are running Apache Solr, you do not need to integrate `cuvs-lucene` direc ## Choosing a Codec -Four codecs are available. All four build the vector index on the GPU; they differ in where search runs and in what is written to disk. +Four codecs are available, created through [`CuVSCodecs`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-cuvscodecs). All four build the vector index on the GPU; they differ in where search runs and in what is written to disk. | Codec | Search | On-disk format | Use when | | --- | --- | --- | --- | -| [`Lucene101AcceleratedHNSWCodec`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-lucene101acceleratedhnswcodec) | CPU | Standard Lucene HNSW | You want faster index builds without changing the search path or index format. | -| [`LuceneAcceleratedHNSWScalarQuantizedCodec`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedcodec) | CPU | Standard Lucene HNSW, scalar-quantized | As above, with a smaller index footprint and some loss of precision. | -| [`LuceneAcceleratedHNSWBinaryQuantizedCodec`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedcodec) | CPU | Standard Lucene HNSW, binary-quantized | As above, with the smallest footprint and the largest loss of precision. | -| [`CuVS2510GPUSearchCodec`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-cuvs2510gpusearchcodec) | GPU | cuVS format | Search throughput matters and GPUs are available on the query path. | +| `CuVSCodecs.acceleratedHNSW` | CPU | Standard Lucene HNSW | You want faster index builds without changing the search path or index format. | +| `CuVSCodecs.acceleratedHNSWScalarQuantized` | CPU | Standard Lucene HNSW, scalar-quantized | As above, with a smaller index footprint and some loss of precision. | +| `CuVSCodecs.acceleratedHNSWBinaryQuantized` | CPU | Standard Lucene HNSW, full-precision vectors | As above, with the graph built over binary-quantized vectors. The vectors themselves are stored and scored in full precision. | +| `CuVSCodecs.gpuSearch` | GPU | cuVS format | Search throughput matters and GPUs are available on the query path. | The three accelerated HNSW codecs write the stock Lucene HNSW vector format and read it back through the standard Lucene reader. GPU acceleration therefore applies to index build only: the query API and the read path are unchanged, and the read path touches neither a GPU nor the native cuVS library. The graph itself is built by CAGRA rather than Lucene's HNSW builder, so benchmark recall if you are migrating an existing index. -Deployment does change, though. Lucene resolves codecs by name through its service loader, so search nodes need the same `cuvs-lucene` and `cuvs-java` jars and a JDK 22 or newer runtime as indexing nodes, or the segments cannot be opened. +Deployment does change, though. Lucene resolves codecs by name through its service loader, so search nodes need the same `cuvs-lucene` and `cuvs-java` jars as indexing nodes, or the segments cannot be opened. Searching the accelerated HNSW codecs' indexes needs no GPU, so search nodes can run any JDK 21 or newer. -`CuVS2510GPUSearchCodec` writes a cuVS-specific format instead. That format is experimental and backward compatibility across releases is not guaranteed, so plan on being able to rebuild indexes when upgrading. +The GPU search codec writes a cuVS-specific format instead. That format is experimental and backward compatibility across releases is not guaranteed, so plan on being able to rebuild indexes when upgrading. -`Lucene101AcceleratedHNSWCodec` falls back to CPU index construction when cuVS resources cannot be created, which happens when no GPU is present or the native library cannot be loaded. It logs a warning and hands the segment to the stock Lucene HNSW writer, so the same application runs on GPU and non-GPU hosts and produces the same index format either way. +The three accelerated HNSW codecs fall back to CPU index construction when cuVS resources cannot be created, which happens when no GPU is present or the native library cannot be loaded. They log a warning and hand the segment to the stock Lucene HNSW writer, so the same application runs on GPU and non-GPU hosts and produces the same index format either way. -The two quantized variants attempt the same fallback but do not currently complete it, so treat a working cuVS installation as required when using them. +The GPU search codec has no such fallback. Its on-disk format has no CPU reader, so both indexing and search throw `UnsupportedOperationException` when cuVS is unavailable. -`CuVS2510GPUSearchCodec` has no such fallback. Its on-disk format has no CPU reader, so both indexing and search throw `UnsupportedOperationException` when cuVS is unavailable. +## Lucene Versions and Upgrades + +Lucene changes its codec APIs between minor releases, so `cuvs-lucene` is published as one artifact per Lucene minor release, `cuvs-lucene-10.2` through `cuvs-lucene-10.5`; see [cuVS Lucene](/installation/java#cuvs-lucene). Use the one that matches your application's Lucene version. An artifact refuses to load against another Lucene minor release. + +Each codec also wraps the default codec of one Lucene release, and Lucene records the codec's name in every segment it writes. The codec that writes therefore changes with Lucene: + +| Lucene | Accelerated HNSW codec | GPU search codec | +| --- | --- | --- | +| 10.2 | `Lucene101AcceleratedHNSWCodec` | `CuVS2510GPUSearchCodec` | +| 10.3 | `Lucene103AcceleratedHNSWCodec` | `Lucene103CuVSGPUSearchCodec` | +| 10.4, 10.5 | `Lucene104AcceleratedHNSWCodec` | `Lucene104CuVSGPUSearchCodec` | + +The quantized codecs follow the same pattern, for example `Lucene104AcceleratedHNSWScalarQuantizedCodec`. `CuVSCodecs` always returns the codecs that write on the Lucene release in use, so code that creates its codecs through it does not change when the application moves to another Lucene release. + +To upgrade Lucene, switch to the matching `cuvs-lucene` artifact. Every artifact also includes the codecs of the earlier Lucene releases it supports, so existing segments stay readable. Those codecs are read-only, like Lucene's own backward codecs: new segments, including those produced by merges, are written with the current codec, and writing with an older one throws `UnsupportedOperationException`. Configuration that names a codec, rather than creating it through `CuVSCodecs`, has to be updated to the new name. ## Quickstart @@ -47,7 +61,7 @@ Setting one of the codecs on an `IndexWriterConfig` is the only change an existi import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; import com.nvidia.cuvs.lucene.AcceleratedHNSWParams; -import com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec; +import com.nvidia.cuvs.lucene.CuVSCodecs; import java.nio.file.Files; import java.nio.file.Path; import java.util.Random; @@ -81,7 +95,7 @@ public class AcceleratedHnswQuickstart { Path indexDirPath = Files.createTempDirectory("cuvs-lucene-quickstart"); AcceleratedHNSWParams params = new AcceleratedHNSWParams.Builder().build(); - Codec codec = new Lucene101AcceleratedHNSWCodec(params); + Codec codec = CuVSCodecs.acceleratedHNSW(params); IndexWriterConfig config = new IndexWriterConfig().setCodec(codec); // Indexing @@ -122,18 +136,18 @@ public class AcceleratedHnswQuickstart { } ``` -Substituting `LuceneAcceleratedHNSWScalarQuantizedCodec` or `LuceneAcceleratedHNSWBinaryQuantizedCodec` is the only change needed to use a quantized variant; both take the same [`AcceleratedHNSWParams`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswparams). +Substituting `CuVSCodecs.acceleratedHNSWScalarQuantized` or `CuVSCodecs.acceleratedHNSWBinaryQuantized` is the only change needed to use a quantized variant; both take the same [`AcceleratedHNSWParams`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswparams). This program is a condensed version of `AcceleratedHnswExample`. That example and a GPU search one live in the [`examples/java/cuvs-lucene`](https://github.com/NVIDIA/cuvs/tree/main/examples/java/cuvs-lucene) directory of the NVIDIA cuVS repository, which the build compiles through `./build.sh lucene --build-java-examples`. ## Searching on the GPU -`CuVS2510GPUSearchCodec` runs both index build and search on the GPU. Configure it with [`GPUSearchParams`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams). A stock `KnnFloatVectorQuery` already searches on the GPU against this codec, so existing query code keeps working; [`GPUKnnFloatVectorQuery`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery) adds control over the CAGRA search parameters and lets cuVS serve all segments in a single request, which is usually faster. This program is the GPU counterpart of the quickstart, with a filter applied to the search: +The GPU search codec, from `CuVSCodecs.gpuSearch`, runs both index build and search on the GPU. Configure it with [`GPUSearchParams`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpusearchparams). A stock `KnnFloatVectorQuery` already searches on the GPU against this codec, so existing query code keeps working; [`GPUKnnFloatVectorQuery`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-gpuknnfloatvectorquery) adds control over the CAGRA search parameters and lets cuVS serve all segments in a single request, which is usually faster. This program is the GPU counterpart of the quickstart, with a filter applied to the search: ```java import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; -import com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec; +import com.nvidia.cuvs.lucene.CuVSCodecs; import com.nvidia.cuvs.lucene.GPUKnnFloatVectorQuery; import com.nvidia.cuvs.lucene.GPUSearchParams; import com.nvidia.cuvs.spi.CuVSProvider; @@ -177,7 +191,7 @@ public class GpuSearchQuickstart { Path indexDirPath = Files.createTempDirectory("cuvs-lucene-gpu-search"); GPUSearchParams params = new GPUSearchParams.Builder().build(); - Codec codec = new CuVS2510GPUSearchCodec(params); + Codec codec = CuVSCodecs.gpuSearch(params); IndexWriterConfig config = new IndexWriterConfig().setCodec(codec); // Indexing @@ -239,7 +253,7 @@ Switching either class to the `CUSTOM` strategy exposes the underlying CAGRA par A few settings matter once `cuvs-lucene` runs in a long-lived, multi-threaded application such as a search server. -**Device memory allocator.** Applications using `CuVS2510GPUSearchCodec` can opt into RMM's stream-ordered asynchronous device allocator during startup: +**Device memory allocator.** Applications using the GPU search codec can opt into RMM's stream-ordered asynchronous device allocator during startup: ```java CuVSProvider.provider().enableRMMAsyncMemory(); @@ -266,7 +280,7 @@ Install the handle on each search thread before that thread runs its first searc **Filter bitsets.** Filtered GPU searches upload an acceptance mask per segment. Two independent settings control this: -- [`FilterBitsetCacheConfig`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-filterbitsetcacheconfig) caches the packed host-side bitsets for one vectors-format instance, with a 128 MiB budget by default. Pass a custom configuration to the `CuVS2510GPUSearchCodec` constructor to change or disable it. The budget is a retention cap, not a preallocation. +- [`FilterBitsetCacheConfig`](/api-reference/lucene-api-com-nvidia-cuvs-lucene-filterbitsetcacheconfig) caches the packed host-side bitsets for one vectors-format instance, with a 128 MiB budget by default. Pass a custom configuration to `CuVSCodecs.gpuSearch` to change or disable it. The budget is a retention cap, not a preallocation. - The `com.nvidia.cuvs.filterBitsetPoolSize` system property sizes the process-wide RMM device pool that backs filter bitset uploads, defaulting to a 4 MiB initial reservation that can grow. Set it to `0` to disable pooling. ## Troubleshooting @@ -275,10 +289,14 @@ Install the handle on each search thread before that thread runs its first searc **An index written with these codecs will not open.** Lucene looks codecs up by name through its service loader, so a reader without `cuvs-lucene` on its classpath fails with an SPI lookup error naming the missing codec. Deploy the same `cuvs-lucene` and `cuvs-java` jars to search nodes as to indexing nodes. -**`UnsupportedOperationException: cuVS is not supported`.** `CuVS2510GPUSearchCodec` raises this when cuVS resources cannot be created, on both the indexing and the search path. The causes are the same as above; unlike the accelerated HNSW codecs, this codec cannot continue without a GPU. +**`UnsupportedOperationException: cuVS is not supported`.** The GPU search codec raises this when cuVS resources cannot be created, on both the indexing and the search path. The causes are the same as above; unlike the accelerated HNSW codecs, this codec cannot continue without a GPU. + +**`IllegalStateException: cuvs-lucene-10.X is built for Lucene 10.X.x, but Lucene 10.Y.z is in use`.** The `cuvs-lucene` artifact on the classpath was built for another Lucene minor release than the one the application uses. Lucene itself keeps working, and so do indexes that do not use the cuvs-lucene codecs; creating a cuvs-lucene codec, or reading or writing an index that uses one, fails with this message. The same problem is logged at `SEVERE` the first time Lucene looks up its codecs, usually at startup. Applications that would rather refuse to start can call `CuVSCodecs.checkLuceneVersion()` during startup, which throws this exception. Replace the artifact with the one the message names, if it exists; see [Lucene Versions and Upgrades](#lucene-versions-and-upgrades). + +**`UnsupportedOperationException: The ... codec can only read indexes`.** The application writes with the codec of an earlier Lucene release, which the current release can only read. Create the codec through `CuVSCodecs`, or switch to the codec for the current release. **Native libraries do not match.** A missing or mismatched native cuVS library is caught during resource creation and logged at `WARNING` rather than thrown, so it surfaces as one of the two symptoms above rather than as a load error. Make sure the installed native libraries match the `cuvs-lucene` version; see [cuVS Lucene](/installation/java#cuvs-lucene). -**Errors about native access or an unsupported class file version.** `cuvs-lucene` is compiled for Java 22 and uses the Panama FFI APIs, so it needs a JDK 22 or newer runtime on Linux (`amd64` or `aarch64`). Run the JVM with `--enable-native-access=ALL-UNNAMED` to suppress native-access warnings, or `--enable-native-access=com.nvidia.cuvs` if you put `cuvs-java` on the module path. +**Errors about native access, or indexing never uses the GPU.** `cuvs-lucene` is compiled for Java 21, but `cuvs-java` reaches the GPU through the Panama FFI APIs, which need a JDK 22 or newer runtime on Linux (`amd64` or `aarch64`). On JDK 21, cuVS reports that it is not supported: the accelerated HNSW codecs build graphs on the CPU, and the GPU search codec throws `UnsupportedOperationException`. Run the JVM with `--enable-native-access=ALL-UNNAMED` to suppress native-access warnings, or `--enable-native-access=com.nvidia.cuvs` if you put `cuvs-java` on the module path. **GPU search is slower than expected.** A stock `KnnFloatVectorQuery` searches each segment separately; switching to `GPUKnnFloatVectorQuery` lets cuVS serve all segments in one request. Indexes with many small segments also benefit from force-merging to fewer, more evenly sized segments. diff --git a/fern/scripts/generate_api_reference.py b/fern/scripts/generate_api_reference.py index 3cf916d868..0d46841cc8 100755 --- a/fern/scripts/generate_api_reference.py +++ b/fern/scripts/generate_api_reference.py @@ -34,6 +34,11 @@ ] LUCENE_SOURCE_DIRS = [ REPO_DIR / "java" / "cuvs-lucene" / "src" / "main" / "java", + # Codecs introduced by a later Lucene release, which live in that release's module and are + # compiled into every later one as well. + *sorted( + (REPO_DIR / "java" / "cuvs-lucene").glob("lucene-*/src/since/java") + ), ] API_NAV_SECTIONS = [ ("C API Documentation", "c_api", "c-api-documentation", "C API", "c-api"), @@ -142,6 +147,12 @@ "KnnVectorsFormat", "KnnVectorsReader", "KnnVectorsWriter", + # cuvs-lucene's own bases for these, which keep one signature across Lucene releases. + "BaseAcceleratedHNSWScalarQuantizedVectorsFormat", + "CompatKnnFloatVectorQuery", + "CompatKnnVectorsReader", + "CompatKnnVectorsWriter", + "CuVSFilterCodec", } ) JAVA_SUPERTYPE_RE = re.compile(r"\b(?:extends|implements)\s+(?P\w+)") @@ -3014,7 +3025,7 @@ def generate_lucene_api_pages() -> None: title="Lucene API Documentation", intro=( "These pages are generated from the Java source files in " - "`java/cuvs-lucene/src/main`.\n\n" + "`java/cuvs-lucene/src/main` and `java/cuvs-lucene/lucene-*/src/since`.\n\n" "For an introduction to the codecs, configuration, and tuning, see the " "[Lucene Integration](/user-guide/lucene) guide." ), diff --git a/java/cuvs-lucene/MAINTAINING.md b/java/cuvs-lucene/MAINTAINING.md new file mode 100644 index 0000000000..427014c95c --- /dev/null +++ b/java/cuvs-lucene/MAINTAINING.md @@ -0,0 +1,241 @@ +# Maintaining cuvs-lucene across Lucene releases + +`cuvs-lucene` is published as one artifact per supported Lucene minor release (`cuvs-lucene-10.2` … +`cuvs-lucene-10.5`), all built from the same sources. This page lists exactly what to change when +Lucene publishes a new patch release (for example 10.5.2) or a new minor release (for example 10.6.0). +For how the layout works, see [Supporting several Lucene releases](README.md#supporting-several-lucene-releases). + +All commands run from `java/cuvs-lucene/` unless stated otherwise, with `libcuvs` built and `cuvs-java` +installed in the local Maven repository (`./build.sh libcuvs java` from the repository root). + +## A new patch release (example: 10.5.2) + +Every artifact accepts any patch release of its minor (the version check in `LuceneCompat` compares only +`major.minor`), so users can already run `cuvs-lucene-10.5` on Lucene 10.5.2. Moving the build to the +new patch release makes the artifact depend on, and be tested against, it. + +1. In `lucene-10.5/pom.xml`, set `10.5.2`. +2. If the patch release is for the newest minor, also update the places that pin it: the default + `lucene.version` in the parent `pom.xml`, and `lucene.version` in `examples/java/cuvs-lucene/pom.xml`. +3. Run the tests of that module: `mvn -pl lucene-10.5 verify` (add `-am` if the parent is not installed). +4. If something no longer compiles or a test fails, Lucene broke its API or behaviour in a patch + release, which is rare. Fix it in that module's `LuceneCompat` (`lucene-10.5/src/main/java`), or + in a new `compat/` variant used only by that module (see step 2 of the minor release below), never + in the shared `src/`. A module targets one patch release at a time: if the fix cannot work with + both 10.5.1 and 10.5.2, raise the minimum in the documentation. + +Nothing else needs to change: no back-compat indexes, no codecs, no CI, no docs besides the examples. + +## A new minor release (example: 10.6.0) + +Work through the steps in order. Steps 1–3 always apply; the others depend on what the release changed. + +### 1. Create the module + +1. Copy the newest module: `cp -r lucene-10.5 lucene-10.6` (then `rm -rf lucene-10.6/target`). +2. In `lucene-10.6/pom.xml`: + - change `artifactId` and `name` to `cuvs-lucene-10.6`, and the version in `description`; + - set `10.6.0`; + - keep the `add-since-sources` list: it must name the `src/since/java` directory of **every** earlier + module that has one (today `../lucene-10.3/src/since/java` and `../lucene-10.4/src/since/java`); + - keep the `cuvs.lucene.compat.*` properties: the new release starts with the same variant of each + Lucene API as the previous one (see [compat/README.md](compat/README.md)). +3. In the parent `pom.xml`, add `lucene-10.6`, and set its default `lucene.version` + to `10.6.0` (only the parent uses it; it tracks the newest supported release). +4. In `lucene-10.6/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java`, set `LUCENE_MINOR = 6` + and the Lucene version in the javadoc. Keep `LUCENE_MAJOR`/`LUCENE_MINOR` compile-time constants: + the version check reads them without loading `LuceneCompat`. + +Nothing else in the build needs to know about the new module: `ci/release/update-version.sh`, +`build.sh`, the CI workflows (`java/cuvs-lucene/*/target/`) and `ci/test_lucene_prebuilt.sh` +all match `lucene-*`. + +### 2. Make it compile + +```bash +mvn -pl lucene-10.6 -am test-compile +``` + +Fix every error without changing what the other modules compile: + +- calls whose signature or class location changed (a class moved to `org.apache.lucene.backward_codecs`, + a constructor lost or gained parameters, an enum moved to another class): in + `lucene-10.6/src/main/java/.../LuceneCompat.java`; +- a Lucene class cuvs-lucene adapts in `compat/` changed (for example `KnnVectorsWriter`): copy the + variant the module uses to `compat/-106-` (for example + `compat/knn-vectors-writer-106-`), change it there, and point the module's property + (`cuvs.lucene.compat.knnVectorsWriter`) at it. Do not edit a variant other modules still use; +- a Lucene class with no variants yet changed: move its adapter from the shared `src/` into two new + variants, `-102-` for the existing modules and `-106-`, add a + `cuvs.lucene.compat.` property to every module and a matching `` line to the + `add-shared-sources` execution of the parent `pom.xml` (see [compat/README.md](compat/README.md)); +- test-only calls, such as `LeafReader.searchNearestVectors`: the same, with `TestLuceneCompat` in the + `leaf-reader-*` variants (their `src/test/java`). + +The shared `src/` must keep compiling against every supported release. If a shared class needs an API +that differs between releases, add a method to **every** module's `LuceneCompat` and call that instead. +Never use reflection, and never read version constants such as `VERSION_CURRENT` from Lucene: they +change meaning between releases. + +### 3. Look for changes the compiler cannot see + +A method Lucene adds with a default implementation compiles fine but may be wrong for cuvs-lucene (10.3's +`KnnVectorsReader.getOffHeapByteSize` is an example). Compare the overridable API of every Lucene class +cuvs-lucene extends between the previous and the new release: + +```bash +M=~/.m2/repository/org/apache/lucene/lucene-core +for cls in org.apache.lucene.codecs.FilterCodec org.apache.lucene.codecs.KnnVectorsFormat \ + org.apache.lucene.codecs.KnnVectorsReader org.apache.lucene.codecs.KnnVectorsWriter \ + org.apache.lucene.codecs.KnnFieldVectorsWriter org.apache.lucene.search.KnnFloatVectorQuery \ + org.apache.lucene.search.TopKnnCollector org.apache.lucene.search.Query \ + org.apache.lucene.search.Weight org.apache.lucene.search.Scorer \ + org.apache.lucene.search.ScorerSupplier org.apache.lucene.search.DocIdSetIterator \ + 'org.apache.lucene.util.hnsw.HnswGraph' 'org.apache.lucene.util.hnsw.HnswGraph$NodesIterator'; do + diff <(javap -protected -cp $M/10.5.1/lucene-core-10.5.1.jar "$cls" | grep -Ev ' (final|static) ' | sort) \ + <(javap -protected -cp $M/10.6.0/lucene-core-10.6.0.jar "$cls" | grep -Ev ' (final|static) ' | sort) \ + | grep '^[<>]' && echo "^^^ $cls" +done +``` + +For each new or changed method, decide whether Lucene's default is right for the cuvs-lucene subclass. +If not, override it in the `Compat*` class of the variant the new release uses, creating a new variant +if older releases share that one (see how `compat/knn-vectors-reader-103-acceptdocs` overrides `getOffHeapByteSize`). To refresh the class list, run `javap` on the classes in +`lucene-10.6/target/classes/com/nvidia/cuvs/lucene` and collect their `org.apache.lucene` supertypes. + +Also read Lucene's `CHANGES.txt` for the release, looking for: + +- changes to `Lucene99HnswVectorsFormat`'s on-disk format. The GPU writers write its `.vex`/`.vem` files + themselves (`AcceleratedHNSWUtils`) with `VERSION_START`; the new release's reader must still accept + that version. If Lucene replaced `Lucene99HnswVectorsFormat` as its HNSW format, plan a new + cuvs-lucene format; that is feature work, not part of this checklist; +- vector formats moved to `backward_codecs` (they can then only be read, see step 4); +- changes to `BaseKnnVectorsFormatTestCase` (see step 5). + +### 4. If Lucene's default codec changed + +Check which codec the release writes by default: + +```bash +unzip -p ~/.m2/repository/org/apache/lucene/lucene-core/10.6.0/lucene-core-10.6.0.jar \ + META-INF/services/org.apache.lucene.codecs.Codec +``` + +If it is still `Lucene104Codec`, skip this step: the `Lucene104*` codecs keep writing on 10.6. +If it is new, for example `Lucene106Codec`: + +1. Create `lucene-10.6/src/since/java/com/nvidia/cuvs/lucene/` with the four codecs of the new + generation, copied from `lucene-10.4/src/since/java` and renamed: `Lucene106AcceleratedHNSWCodec`, + `Lucene106AcceleratedHNSWScalarQuantizedCodec`, `Lucene106AcceleratedHNSWBinaryQuantizedCodec`, + `Lucene106CuVSGPUSearchCodec`. Each passes generation `106` and `() -> LuceneCompat.lucene106Codec()` + (a lambda, not a method reference) to the `CuVSFilterCodec` constructor. The codec name + is written into every segment, so never reuse or rename one once released. +2. Add `src/since/java` to the `add-since-sources` list in `lucene-10.6/pom.xml` (and, later, in every + module after it). +3. In `lucene-10.6/.../LuceneCompat.java`: + - add `lucene106Codec()` returning `new org.apache.lucene.codecs.lucene106.Lucene106Codec()`; + - point `lucene104Codec()` at `org.apache.lucene.backward_codecs.lucene104.Lucene104Codec`, where + Lucene moves the old default codec; + - set `CURRENT_CODEC_GENERATION = 106`. The new codecs pass `106` as their generation to the + `CuVSFilterCodec` constructor, and the `Lucene104*` codecs, which pass `104`, become read-only; + - make `acceleratedHNSWCodec`, `acceleratedHNSWScalarQuantizedCodec`, + `acceleratedHNSWBinaryQuantizedCodec` and `gpuSearchCodec` return the `Lucene106*` codecs. +4. Register the four new codecs in `lucene-10.6/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec`, + after the existing ones. List only cuvs-lucene classes in the services files: a Lucene class there + breaks every codec or format lookup in the JVM once a later release moves it. +5. Update the list of codecs per Lucene release in the class javadoc of `CuVSCodecs`. + +If a vector format cuvs-lucene writes moved to `backward_codecs` (as `Lucene99ScalarQuantizedVectorsFormat` +did in 10.4), the codecs using it can no longer write on 10.6: add a new format next to the codecs of +step 1 (see `lucene-10.4/src/since/java/.../Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java`), +register it in `META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat`, and have the new codecs +and `LuceneCompat.acceleratedHNSWScalarQuantizedFormat` use it. + +### 5. Make the tests pass + +```bash +mvn -pl lucene-10.6 -am verify +``` + +- New abstract methods or new tests in Lucene's `BaseKnnVectorsFormatTestCase` are handled in the shared + test classes without `@Override`, so that they still compile against older releases. See + `supportsFloatVectorFallback()` and `testWriterByteVectorRamEstimate()` in + `src/test/java/.../TestLucene99AcceleratedHNSWVectorsFormat.java` (and the same in + `TestCuVSVectorsFormat` and `TestQuantizedVectorsFormats`). +- A test call whose signature changed goes through `TestLuceneCompat`, in a `compat/` variant (step 2). +- `TestBackCompat` fails if a codec from `CuVSCodecs` is not registered, or if an older codec can still + write. + +### 6. Add the back-compat indexes + +```bash +mvn -pl lucene-10.6 test -Dtest=TestBackCompatIndices -Dtests.method=testCreateIndices \ + -Dtests.cuvs.createBackCompatIndices=$PWD/src/test/resources/backcompat +``` + +This writes `10.6-.zip` for each codec from `CuVSCodecs` except the GPU search codec. Commit them. +Every module from 10.6 on reads them, and 10.6 reads every earlier one, so step 7 checks that indexes +written with each earlier release still open. + +### 7. Run everything + +```bash +./build.sh --run-java-tests --build-java-examples +``` + +This builds and tests every module, installs them, and builds the examples. + +### 8. Update the documentation and examples + +| File | Change | +| --- | --- | +| `README.md` (this directory) | the supported range and the artifact list; the codec paragraph if step 4 applied | +| `../../fern/pages/installation/java.md` | "supports Lucene 10.2 through 10.X", the artifact table, the dependency example | +| `../../fern/pages/user_guide/lucene.md` | the supported range, the artifact list, the codec table if step 4 applied | +| `../../examples/java/cuvs-lucene/pom.xml` | `lucene.version` and `cuvs.lucene.artifactId`, to use the newest release | +| `../../examples/java/cuvs-lucene/README.md` | the Lucene version the examples use | +| `bench/pom.xml` | the `cuvs-lucene-10.X` dependency, to use the newest release | + +The API reference pages in `fern/pages/lucene_api` are generated: the `fern-api-reference` pre-commit +hook picks up new classes in `lucene-*/src/since/java` by itself. Run `pre-commit run --all-files` last. + +## Publishing + +Nothing in this repository deploys to Maven Central; the publishing step takes the files from each +module's `target/` directory (the CI artifact holds them all). For every release, publish: + +- each `lucene-10.X/target/`: the jar, the `-sources`, `-javadoc` and `-jar-with-dependencies` jars, and + `pom.xml` (the flattened, standalone pom); +- `relocation/target/pom.xml`, as `com.nvidia.cuvs.lucene:cuvs-lucene` (packaging `pom`, no jar). Until + 26.10 cuvs-lucene was a single artifact under that name; this pom relocates it to `cuvs-lucene-10.2`, + so that Maven users who only bump the version keep building and see a warning telling them to switch. + The parent `cuvs-lucene-parent` pom does not need to be published. + +## After a cuvs-lucene release + +Add the indexes written by the published jars to the back-compat tests, so that later changes cannot +silently make indexes written by that release unreadable. On a host with a GPU and the matching native +libraries: + +```bash +src/test/backcompat/generate-released-indices.sh +``` + +This currently handles releases published as the single `cuvs-lucene` artifact (up to 26.08); releases +from 26.12 on are published per Lucene release, so the script needs extending to pick one of those +artifacts. Record the new zips in `src/test/resources/backcompat/README.md`. + +## Dropping a minor release + +1. Remove its `` from the parent `pom.xml` and delete its `lucene-10.X` directory. If it has a + `src/since/java`, move that directory to the oldest remaining module that lists it, and update the + `add-since-sources` lists of the later modules. +2. Delete every `compat/` variant no remaining module uses (`grep -h cuvs.lucene.compat lucene-*/pom.xml`). + If all remaining modules use the same variant of an API, move its classes into the shared `src/`, + delete the variant directory and the property, and remove its `` line from the parent + `pom.xml`. Do the same for any `LuceneCompat` method that is now identical in every module. +3. Keep its codecs and its `src/test/resources/backcompat/10.X-*.zip` files as long as later releases + must read indexes written with it. +4. Update the documentation listed in step 8 above. +5. When dropping 10.2, the relocation in `relocation/pom.xml` would point to an artifact that is no longer + published: remove the `relocation` module (and stop publishing it) at that point. diff --git a/java/cuvs-lucene/README.md b/java/cuvs-lucene/README.md index d49415ed07..704e6f0a1d 100644 --- a/java/cuvs-lucene/README.md +++ b/java/cuvs-lucene/README.md @@ -8,18 +8,25 @@ This is a project for using [cuVS](https://github.com/NVIDIA/cuvs), NVIDIA's GPU 2. [Installing cuvs-lucene](#installing-cuvs-lucene) 3. [Getting Started](#getting-started) 4. [Contributing](#contributing) -5. [References](#references) +5. [Supporting several Lucene releases](#supporting-several-lucene-releases) +6. [References](#references) ## What is cuvs-lucene? -`cuvs-lucene` provides a pluggable [KnnVectorsFormat](https://lucene.apache.org/core/10_2_0/core/org/apache/lucene/codecs/KnnVectorsFormat.html) that uses cuVS to offload vector index build — and optionally search — to NVIDIA GPUs. Because it plugs in through a standard Lucene codec, existing Lucene applications can take advantage of GPU acceleration with minimal code changes. `Lucene101AcceleratedHNSWCodec` also falls back to the stock CPU codec when no GPU is present; the other three require a working cuVS installation. +`cuvs-lucene` provides a pluggable [KnnVectorsFormat](https://lucene.apache.org/core/10_5_0/core/org/apache/lucene/codecs/KnnVectorsFormat.html) that uses cuVS to offload vector index build — and optionally search — to NVIDIA GPUs. Because it plugs in through a standard Lucene codec, existing Lucene applications can take advantage of GPU acceleration with minimal code changes. It supports Lucene 10.2 through 10.5. -Four codecs are currently provided: +Four codecs are currently provided, created through `CuVSCodecs`: -- `Lucene101AcceleratedHNSWCodec` — GPU-accelerated HNSW build with CPU HNSW search. The on-disk format is standard Lucene HNSW, so indexes built on the GPU are read back through the standard Lucene 10.2 reader and searching them needs no GPU. Lucene still resolves the codec by name, so search nodes need the `cuvs-lucene` and `cuvs-java` jars on their classpath. - - `LuceneAcceleratedHNSWScalarQuantizedCodec` — scalar-quantized vectors for a smaller index footprint. - - `LuceneAcceleratedHNSWBinaryQuantizedCodec` — binary-quantized vectors for an even smaller index footprint. -- `CuVS2510GPUSearchCodec` — GPU-accelerated HNSW build and GPU search +- `CuVSCodecs.acceleratedHNSW()` — GPU-accelerated HNSW build with CPU HNSW search. The on-disk format is standard Lucene HNSW, so indexes built on the GPU are read back through the standard Lucene reader and searching them needs no GPU. Lucene still resolves the codec by name, so search nodes need the `cuvs-lucene` and `cuvs-java` jars on their classpath. Falls back to building the graph on the CPU when no GPU is present. + - `CuVSCodecs.acceleratedHNSWScalarQuantized()` — scalar-quantized vectors for a smaller index footprint. + - `CuVSCodecs.acceleratedHNSWBinaryQuantized()` — graph built over binary-quantized vectors. +- `CuVSCodecs.gpuSearch()` — GPU-accelerated build and GPU search. Requires a GPU. + +Each codec wraps the default codec of one Lucene release, so the codec that writes changes with Lucene: for +example `Lucene101AcceleratedHNSWCodec` on Lucene 10.2, `Lucene103AcceleratedHNSWCodec` on 10.3, and +`Lucene104AcceleratedHNSWCodec` on 10.4 and 10.5. Codecs of earlier releases stay available so that existing +indexes can still be read after an upgrade, but they refuse to write. `CuVSCodecs` always returns the codecs +that write on the Lucene release in use. For guidance on choosing between them, configuring builds, and tuning GPU resources, see the [Lucene Integration](https://docs.nvidia.com/cuvs/user-guide/lucene) guide. @@ -30,22 +37,28 @@ For guidance on choosing between them, configuring builds, and tuning GPU resour - An NVIDIA GPU, to use the GPU-accelerated paths (the accelerated HNSW codecs fall back to CPU index construction without one; `CuVS2510GPUSearchCodec` requires a GPU) - [CUDA Toolkit 12.2+](https://developer.nvidia.com/cuda-toolkit-archive) and an Ampere architecture GPU or newer, matching the [cuVS requirements](https://docs.nvidia.com/cuvs/installation) -- [JDK 22](https://jdk.java.net/archive/) +- JDK 21 or newer. GPU acceleration needs a [JDK 22](https://jdk.java.net/archive/) or newer runtime; on JDK 21 + the accelerated HNSW codecs build graphs on the CPU, and the GPU search codec is unavailable - [Maven 3.9.6+](https://maven.apache.org/download.cgi) - A matching version of the [cuVS libraries](https://docs.nvidia.com/cuvs/installation/java). For Maven usage, install the cuVS libraries and add them to your system library load path. ### Maven -To pull `cuvs-lucene` into a Maven project, add the following dependency to your `pom.xml`: +Lucene changes its codec APIs between minor releases, so there is one `cuvs-lucene` artifact per supported +Lucene minor release: `cuvs-lucene-10.2`, `cuvs-lucene-10.3`, `cuvs-lucene-10.4` and `cuvs-lucene-10.5`. Pick +the one matching the Lucene version your application uses. For Lucene 10.5, add the following dependency to +your `pom.xml`: ```xml com.nvidia.cuvs.lucene - cuvs-lucene + cuvs-lucene-10.5 26.12.0 ``` +An artifact fails to load against another Lucene minor release, with an error that names the artifact to use. + ### Building from source `cuvs-lucene` lives in the [cuVS repository](https://github.com/NVIDIA/cuvs) and builds against the cuVS @@ -55,7 +68,8 @@ Java bindings. If the libcuvs libraries and the Java bindings have not been buil Alternatively, if libcuvs is already built and the `cuvs-java` artifact is already installed in your local Maven repository, do `./build.sh lucene` in the top level directory or just do `./build.sh` in this directory. -The resulting artifacts are written to `target/`. +This builds, and installs into your local Maven repository, every `cuvs-lucene-10.X` artifact. They are +written to `lucene-10.X/target/`. To run the tests, add `--run-java-tests` to any of the commands above. Be sure to set (manually, if needed) your `LD_LIBRARY_PATH` to include the directory with the appropriate (matching) version of `libcuvs.so`, as @@ -68,8 +82,8 @@ codec into a standard Lucene `IndexWriter`, searching on the GPU, tuning index b resources in a long-lived application. Class-level documentation is in the [Lucene API reference](https://docs.nvidia.com/cuvs/api-reference/lucene-api-documentation). -Runnable examples of CAGRA-accelerated HNSW indexing, and of indexing and searching entirely on the GPU with -`CuVS2510GPUSearchCodec`, are in the [`examples/`](../../examples/java/cuvs-lucene) directory. +Runnable examples of CAGRA-accelerated HNSW indexing, and of indexing and searching entirely on the GPU, are +in the [`examples/`](../../examples/java/cuvs-lucene) directory. ## Contributing @@ -78,6 +92,48 @@ If you are interested in contributing to cuvs-lucene, please read the cuVS [Cont > [!NOTE] > The code style format is enforced using the [Spotless maven plugin](https://github.com/diffplug/spotless/tree/main/plugin-maven), which runs as a `pre-commit` hook. Run `pre-commit run --all-files`, or `mvn spotless:apply` in this directory, to format the sources. +## Supporting several Lucene releases + +All `cuvs-lucene-10.X` artifacts are built from the same sources: + +``` +java/cuvs-lucene/ + pom.xml parent pom, lists one module per Lucene minor release + src/main/java shared code: everything that compiles against every supported Lucene release + src/test/java shared tests + src/test/resources/backcompat indexes written by each release, see below + compat//src the adapter for one Lucene class as it looks from one release on, shared by + the releases that have it, such as knn-vectors-reader-102-bits and + knn-vectors-reader-103-acceptdocs; see compat/README.md + lucene-10.X/ + pom.xml the Lucene 10.X dependencies, and which compat/ variant of each API it uses + src/main/java LuceneCompat: the calls that differ on Lucene 10.X + src/main/resources the META-INF/services files: the codecs and formats Lucene 10.X can load + src/since/java code first needed on Lucene 10.X, such as the codecs wrapping its default + codec; compiled by this module and every later one +``` + +Each module compiles the shared sources against its own Lucene release, so the compiler checks every release. +The shared code never refers to a Lucene API that differs between releases. Instead it goes through: + +- `LuceneCompat`, for calls whose signature or class location differs, such as the codecs of earlier releases, + which move to Lucene's backward codecs. +- `CompatKnnVectorsReader`, `CompatKnnVectorsWriter` and `CompatKnnFloatVectorQuery`, which override the Lucene + methods whose signatures changed and forward to a `do*` method with the same signature on every release. + They live in `compat/`, one directory per variant, and each module picks its variants in its `pom.xml`. + +Do not look up Lucene classes or constants by reflection. It hides API changes from the compiler, and constants +such as `Lucene99HnswVectorsFormat.VERSION_CURRENT` change meaning between releases. + +`TestBackCompatIndices` reads indexes written by each module, stored as zips in `src/test/resources/backcompat`, +and checks that every later module can still read and search them. `TestBackCompat` checks that only the codecs +from `CuVSCodecs` write. + +### Adding a Lucene release + +[MAINTAINING.md](MAINTAINING.md) lists exactly what to change when Lucene publishes a new patch or minor +release, and how to drop support for one. + ## References - [Bring Massive-Scale Vector Search to the GPU with Apache Lucene](https://www.nvidia.com/en-us/on-demand/session/gtc25-S71286/) — NVIDIA GTC 2025 session video diff --git a/java/cuvs-lucene/bench/pom.xml b/java/cuvs-lucene/bench/pom.xml index 7a8f54ee60..39dd001cc4 100644 --- a/java/cuvs-lucene/bench/pom.xml +++ b/java/cuvs-lucene/bench/pom.xml @@ -82,9 +82,10 @@ ${jmh.version} provided + com.nvidia.cuvs.lucene - cuvs-lucene + cuvs-lucene-10.5 26.12.0 diff --git a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswIndexingBenchmarks.java b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswIndexingBenchmarks.java index 08a41f6a5b..4bbdfe9577 100644 --- a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswIndexingBenchmarks.java +++ b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswIndexingBenchmarks.java @@ -8,7 +8,8 @@ import static com.nvidia.cuvs.lucene.benchmarks.Utils.generateDataset; import static com.nvidia.cuvs.lucene.benchmarks.Utils.index; -import com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec; +import com.nvidia.cuvs.lucene.AcceleratedHNSWParams; +import com.nvidia.cuvs.lucene.CuVSCodecs; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -59,7 +60,16 @@ public class AcceleratedHnswIndexingBenchmarks { public void setup() throws Exception { random = new Random(222); indexDirPath = Paths.get(UUID.randomUUID().toString()); - codec = new Lucene101AcceleratedHNSWCodec(32, 128, 64, 3, 16, 100); + codec = + CuVSCodecs.acceleratedHNSW( + new AcceleratedHNSWParams.Builder() + .withWriterThreads(32) + .withIntermediateGraphDegree(128) + .withGraphDegree(64) + .withHNSWLayer(3) + .withMaxConn(16) + .withBeamWidth(100) + .build()); numDocs = 1000; dimension = 128; dataset = generateDataset(random, numDocs, dimension); diff --git a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswSearchBenchmarks.java b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswSearchBenchmarks.java index 32c8d6f970..2f6c03ef3a 100644 --- a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswSearchBenchmarks.java +++ b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/AcceleratedHnswSearchBenchmarks.java @@ -9,7 +9,8 @@ import static com.nvidia.cuvs.lucene.benchmarks.Utils.index; import static com.nvidia.cuvs.lucene.benchmarks.Utils.search; -import com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec; +import com.nvidia.cuvs.lucene.AcceleratedHNSWParams; +import com.nvidia.cuvs.lucene.CuVSCodecs; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -63,7 +64,16 @@ public class AcceleratedHnswSearchBenchmarks { public void setup() throws Exception { random = new Random(222); indexDirPath = Paths.get(UUID.randomUUID().toString()); - codec = new Lucene101AcceleratedHNSWCodec(32, 128, 64, 3, 16, 100); + codec = + CuVSCodecs.acceleratedHNSW( + new AcceleratedHNSWParams.Builder() + .withWriterThreads(32) + .withIntermediateGraphDegree(128) + .withGraphDegree(64) + .withHNSWLayer(3) + .withMaxConn(16) + .withBeamWidth(100) + .build()); numDocs = 1000; dimension = 128; topK = 5; diff --git a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraIndexingBenchmarks.java b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraIndexingBenchmarks.java index 01cf540c85..af207d280c 100644 --- a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraIndexingBenchmarks.java +++ b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraIndexingBenchmarks.java @@ -8,7 +8,7 @@ import static com.nvidia.cuvs.lucene.benchmarks.Utils.generateDataset; import static com.nvidia.cuvs.lucene.benchmarks.Utils.index; -import com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec; +import com.nvidia.cuvs.lucene.CuVSCodecs; import com.nvidia.cuvs.spi.CuVSProvider; import java.io.IOException; import java.nio.file.Path; @@ -61,7 +61,7 @@ public void setup() throws Exception { CuVSProvider.provider().enableRMMAsyncMemory(); random = new Random(222); indexDirPath = Paths.get(UUID.randomUUID().toString()); - codec = new CuVS2510GPUSearchCodec(); + codec = CuVSCodecs.gpuSearch(); numDocs = 1000; dimension = 128; dataset = generateDataset(random, numDocs, dimension); diff --git a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraSearchBenchmarks.java b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraSearchBenchmarks.java index 111d8c1417..2f7e205495 100644 --- a/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraSearchBenchmarks.java +++ b/java/cuvs-lucene/bench/src/main/java/com/nvidia/cuvs/lucene/benchmarks/CagraSearchBenchmarks.java @@ -9,7 +9,7 @@ import static com.nvidia.cuvs.lucene.benchmarks.Utils.index; import static com.nvidia.cuvs.lucene.benchmarks.Utils.search; -import com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec; +import com.nvidia.cuvs.lucene.CuVSCodecs; import com.nvidia.cuvs.lucene.GPUKnnFloatVectorQuery; import com.nvidia.cuvs.spi.CuVSProvider; import java.io.IOException; @@ -66,7 +66,7 @@ public void setup() throws Exception { CuVSProvider.provider().enableRMMAsyncMemory(); random = new Random(222); indexDirPath = Paths.get(UUID.randomUUID().toString()); - codec = new CuVS2510GPUSearchCodec(); + codec = CuVSCodecs.gpuSearch(); numDocs = 1000; dimension = 128; topK = 5; diff --git a/java/cuvs-lucene/build.sh b/java/cuvs-lucene/build.sh index dcf6668e99..3418b2f600 100755 --- a/java/cuvs-lucene/build.sh +++ b/java/cuvs-lucene/build.sh @@ -9,7 +9,6 @@ ARGS="$*" NUMARGS=$# VERSION="26.12.0" # Note: The version is updated automatically when ci/release/update-version.sh is invoked -GROUP_ID="com.nvidia.cuvs.lucene" function hasArg { (( NUMARGS != 0 )) && (echo " ${ARGS} " | grep -q " $1 ") @@ -34,22 +33,18 @@ if [ -d "${CUVS_LIB_DIR}" ]; then export LD_LIBRARY_PATH="${CUVS_LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" fi -MAVEN_VERIFY_ARGS=() +MAVEN_INSTALL_ARGS=() if ! hasArg --run-java-tests; then - MAVEN_VERIFY_ARGS=("-DskipTests") + MAVEN_INSTALL_ARGS=("-DskipTests") fi cd "${LUCENE_DIR}" -mvn clean verify "${MAVEN_VERIFY_ARGS[@]}" - -# Coverage data only exists when the tests actually ran. -if hasArg --run-java-tests; then - mvn jacoco:report -fi - -mvn install:install-file -Dfile=./target/cuvs-lucene-$VERSION.jar -DgroupId=$GROUP_ID -DartifactId=cuvs-lucene -Dversion=$VERSION -Dpackaging=jar -cp pom.xml ./target/ +# Builds every lucene-X.Y module (one artifact per supported Lucene release), runs their tests when +# asked to, and installs them into the local Maven repository. Each module's target/ also gets a +# standalone pom.xml, so that it holds all the files needed to publish that module. When the tests +# run, the build also writes their coverage report to each module's target/site/jacoco. +mvn clean install "${MAVEN_INSTALL_ARGS[@]}" # Build the cuvs-lucene examples against the jar just installed above, to catch drift between the # examples and the cuvs-lucene API. diff --git a/java/cuvs-lucene/compat/README.md b/java/cuvs-lucene/compat/README.md new file mode 100644 index 0000000000..752c895b25 --- /dev/null +++ b/java/cuvs-lucene/compat/README.md @@ -0,0 +1,41 @@ +# Lucene API variants + +Some Lucene classes that cuvs-lucene extends or calls changed between Lucene releases. The code adapting +to such a class differs between releases, but is the same in all the releases that share one version of +the class, so it lives here once instead of being copied into every `lucene-10.X` module. + +Each directory is one **variant**: the adapter for one Lucene class, as that class looks from one release +on. Directories are named `--`: + +- `` is the Lucene class being adapted. There is one `cuvs.lucene.compat.` property per + class, and each module's `pom.xml` sets it to the variant its Lucene release needs. The parent `pom.xml` + adds the chosen directories to the module's sources. +- `` is the first supported Lucene release with that version of the class (`102` is the oldest + supported release, so `102` variants describe how the class looked before any change we adapt to). + Names never change afterwards, even when older releases are dropped. +- `` says what that release changed, so that variants can be told apart without opening them. + +| Property | Variant | Used by | What changed | +| --- | --- | --- | --- | +| `cuvs.lucene.compat.knnVectorsReader` | `knn-vectors-reader-102-bits` | 10.2 | `search` takes the accepted documents as `Bits` | +| | `knn-vectors-reader-103-acceptdocs` | 10.3–10.5 | `search` takes `AcceptDocs`; readers also report `getOffHeapByteSize` | +| `cuvs.lucene.compat.knnVectorsWriter` | `knn-vectors-writer-102-void-merge` | 10.2–10.4 | `mergeOneField` returns `void` | +| | `knn-vectors-writer-105-deferred-merge` | 10.5 | `mergeOneField` returns deferred work as an `IORunnable` | +| `cuvs.lucene.compat.knnFloatVectorQuery` | `knn-float-vector-query-102-bits` | 10.2 | `approximateSearch` takes `Bits` | +| | `knn-float-vector-query-103-acceptdocs` | 10.3–10.5 | `approximateSearch` takes `AcceptDocs` | +| `cuvs.lucene.compat.leafReader` (tests only) | `leaf-reader-102-bits` | 10.2 | `searchNearestVectors` takes `Bits` | +| | `leaf-reader-103-acceptdocs` | 10.3–10.5 | `searchNearestVectors` takes `AcceptDocs` | + +Code that differs in (almost) every release, such as which Lucene classes moved to `backward_codecs`, +stays in each module's own `LuceneCompat`. + +- **A new release changes a class that already has variants:** copy the variant the previous release uses + to `--`, change it there, and point the new module's property at it. Never + edit a variant that another module still uses. +- **A new release changes a class with no variants yet:** move the adapter out of the shared `src/` into + two variants, `-102-` and `--`, add a + `cuvs.lucene.compat.` property to every module, and a matching `` line to the + `add-shared-sources` (or `add-shared-test-sources`) execution of the parent `pom.xml`. +- **A release is dropped:** delete every variant no remaining module uses. When all remaining modules use + the same variant of a class, move its code into the shared `src/`, delete the variant and the property, + and remove its `` line from the parent `pom.xml`. diff --git a/java/cuvs-lucene/compat/knn-float-vector-query-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java b/java/cuvs-lucene/compat/knn-float-vector-query-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java new file mode 100644 index 0000000000..8ad3f54329 --- /dev/null +++ b/java/cuvs-lucene/compat/knn-float-vector-query-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.search.KnnFloatVectorQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.knn.KnnCollectorManager; +import org.apache.lucene.util.Bits; + +/** + * A {@link KnnFloatVectorQuery} whose per-segment search has the same signature on every Lucene + * release. Lucene 10.2 passes the accepted documents as {@link Bits}. + */ +abstract class CompatKnnFloatVectorQuery extends KnnFloatVectorQuery { + + CompatKnnFloatVectorQuery(String field, float[] target, int k, Query filter) { + super(field, target, k, filter); + LuceneVersionGuard.ensureCompatible(); + } + + /** Returns the collector for searching one segment, given its visit budget. */ + abstract KnnCollector newPerLeafCollector(int visitedLimit); + + @Override + protected TopDocs approximateSearch( + LeafReaderContext context, + Bits acceptDocs, + int visitedLimit, + KnnCollectorManager knnCollectorManager) + throws IOException { + KnnCollector results = newPerLeafCollector(visitedLimit); + context.reader().searchNearestVectors(field, getTargetCopy(), results, acceptDocs); + return results.topDocs(); + } +} diff --git a/java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java b/java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java new file mode 100644 index 0000000000..c917eec727 --- /dev/null +++ b/java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.AcceptDocs; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.search.KnnFloatVectorQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.knn.KnnCollectorManager; + +/** + * A {@link KnnFloatVectorQuery} whose per-segment search has the same signature on every Lucene + * release. Since Lucene 10.3, the accepted documents come as {@link AcceptDocs}. + */ +abstract class CompatKnnFloatVectorQuery extends KnnFloatVectorQuery { + + CompatKnnFloatVectorQuery(String field, float[] target, int k, Query filter) { + super(field, target, k, filter); + LuceneVersionGuard.ensureCompatible(); + } + + /** Returns the collector for searching one segment, given its visit budget. */ + abstract KnnCollector newPerLeafCollector(int visitedLimit); + + @Override + protected TopDocs approximateSearch( + LeafReaderContext context, + AcceptDocs acceptDocs, + int visitedLimit, + KnnCollectorManager knnCollectorManager) + throws IOException { + KnnCollector results = newPerLeafCollector(visitedLimit); + context.reader().searchNearestVectors(field, getTargetCopy(), results, acceptDocs); + return results.topDocs(); + } +} diff --git a/java/cuvs-lucene/compat/knn-vectors-reader-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java b/java/cuvs-lucene/compat/knn-vectors-reader-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java new file mode 100644 index 0000000000..f47042e6a0 --- /dev/null +++ b/java/cuvs-lucene/compat/knn-vectors-reader-102-bits/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.hnsw.FlatVectorsReader; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.util.Bits; + +/** + * A {@link KnnVectorsReader} whose search methods have the same signature on every Lucene release. + * Lucene 10.2 passes the accepted documents as {@link Bits}. + */ +abstract class CompatKnnVectorsReader extends KnnVectorsReader { + + /** + * @param rawVectorsReader reads the raw vectors of the fields; unused on Lucene 10.2, which does + * not ask for the off-heap size + */ + CompatKnnVectorsReader(FlatVectorsReader rawVectorsReader) {} + + @Override + public void search(String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) + throws IOException { + doSearch(field, target, knnCollector, acceptDocs); + } + + @Override + public void search(String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) + throws IOException { + doSearch(field, target, knnCollector, acceptDocs); + } + + /** + * Implements {@link #search(String, float[], KnnCollector, Bits)}. + * + * @param acceptDocs the documents that may be returned, or {@code null} if all may be + */ + protected abstract void doSearch( + String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException; + + /** + * Implements {@link #search(String, byte[], KnnCollector, Bits)}. + * + * @param acceptDocs the documents that may be returned, or {@code null} if all may be + */ + protected abstract void doSearch( + String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException; +} diff --git a/java/cuvs-lucene/compat/knn-vectors-reader-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java b/java/cuvs-lucene/compat/knn-vectors-reader-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java new file mode 100644 index 0000000000..e186ab5537 --- /dev/null +++ b/java/cuvs-lucene/compat/knn-vectors-reader-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsReader.java @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import java.util.Map; +import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.hnsw.FlatVectorsReader; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.search.AcceptDocs; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.util.Bits; + +/** + * A {@link KnnVectorsReader} whose search methods have the same signature on every Lucene release. + * Since Lucene 10.3, the accepted documents come as {@link AcceptDocs}, and readers report their + * off-heap size. + */ +abstract class CompatKnnVectorsReader extends KnnVectorsReader { + + private final FlatVectorsReader rawVectorsReader; + + /** + * @param rawVectorsReader reads the raw vectors of the fields, which are all of the reader's + * off-heap data: the cuVS indexes are loaded into GPU memory + */ + CompatKnnVectorsReader(FlatVectorsReader rawVectorsReader) { + this.rawVectorsReader = rawVectorsReader; + } + + @Override + public Map getOffHeapByteSize(FieldInfo fieldInfo) { + return rawVectorsReader.getOffHeapByteSize(fieldInfo); + } + + @Override + public void search(String field, float[] target, KnnCollector knnCollector, AcceptDocs acceptDocs) + throws IOException { + doSearch(field, target, knnCollector, acceptDocs == null ? null : acceptDocs.bits()); + } + + @Override + public void search(String field, byte[] target, KnnCollector knnCollector, AcceptDocs acceptDocs) + throws IOException { + doSearch(field, target, knnCollector, acceptDocs == null ? null : acceptDocs.bits()); + } + + /** + * Implements {@link #search(String, float[], KnnCollector, AcceptDocs)}. + * + * @param acceptDocs the documents that may be returned, or {@code null} if all may be + */ + protected abstract void doSearch( + String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException; + + /** + * Implements {@link #search(String, byte[], KnnCollector, AcceptDocs)}. + * + * @param acceptDocs the documents that may be returned, or {@code null} if all may be + */ + protected abstract void doSearch( + String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException; +} diff --git a/java/cuvs-lucene/compat/knn-vectors-writer-102-void-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java b/java/cuvs-lucene/compat/knn-vectors-writer-102-void-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java new file mode 100644 index 0000000000..e2492278aa --- /dev/null +++ b/java/cuvs-lucene/compat/knn-vectors-writer-102-void-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.MergeState; + +/** + * A {@link KnnVectorsWriter} whose merge method has the same signature on every Lucene release. + * Through Lucene 10.4, {@code mergeOneField} returns nothing. + */ +abstract class CompatKnnVectorsWriter extends KnnVectorsWriter { + + @Override + public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + doMergeOneField(fieldInfo, mergeState); + } + + /** Merges the vectors of one field, and finishes all the work for it before returning. */ + protected abstract void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) + throws IOException; +} diff --git a/java/cuvs-lucene/compat/knn-vectors-writer-105-deferred-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java b/java/cuvs-lucene/compat/knn-vectors-writer-105-deferred-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java new file mode 100644 index 0000000000..12e8421e12 --- /dev/null +++ b/java/cuvs-lucene/compat/knn-vectors-writer-105-deferred-merge/src/main/java/com/nvidia/cuvs/lucene/CompatKnnVectorsWriter.java @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.MergeState; +import org.apache.lucene.util.IORunnable; + +/** + * A {@link KnnVectorsWriter} whose merge method has the same signature on every Lucene release. + * Since Lucene 10.5, {@code mergeOneField} may return work for Lucene to run after all fields are + * merged; the cuvs-lucene writers finish each field right away and return none. + */ +abstract class CompatKnnVectorsWriter extends KnnVectorsWriter { + + @Override + public IORunnable mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + doMergeOneField(fieldInfo, mergeState); + return null; + } + + /** Merges the vectors of one field, and finishes all the work for it before returning. */ + protected abstract void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) + throws IOException; +} diff --git a/java/cuvs-lucene/compat/leaf-reader-102-bits/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java b/java/cuvs-lucene/compat/leaf-reader-102-bits/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java new file mode 100644 index 0000000000..ba897c6718 --- /dev/null +++ b/java/cuvs-lucene/compat/leaf-reader-102-bits/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.util.Bits; + +/** The Lucene test calls that differ between the Lucene releases cuvs-lucene supports. */ +final class TestLuceneCompat { + + private TestLuceneCompat() {} + + /** + * Calls {@link LeafReader#searchNearestVectors} with the accepted documents given as {@link + * Bits}, which is what Lucene 10.2 takes. + */ + static void searchNearestVectors( + LeafReader reader, String field, float[] target, KnnCollector collector, Bits acceptDocs) + throws IOException { + reader.searchNearestVectors(field, target, collector, acceptDocs); + } + + /** + * Calls {@link LeafReader#searchNearestVectors} with the accepted documents given as {@link + * Bits}, {@code null} meaning all, which is what Lucene 10.2 takes. + */ + static TopDocs searchNearestVectors( + LeafReader reader, String field, float[] target, int k, Bits acceptDocs, int visitedLimit) + throws IOException { + return reader.searchNearestVectors(field, target, k, acceptDocs, visitedLimit); + } +} diff --git a/java/cuvs-lucene/compat/leaf-reader-103-acceptdocs/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java b/java/cuvs-lucene/compat/leaf-reader-103-acceptdocs/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java new file mode 100644 index 0000000000..dcd6544f4d --- /dev/null +++ b/java/cuvs-lucene/compat/leaf-reader-103-acceptdocs/src/test/java/com/nvidia/cuvs/lucene/TestLuceneCompat.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.search.AcceptDocs; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.util.Bits; + +/** The Lucene test calls that differ between the Lucene releases cuvs-lucene supports. */ +final class TestLuceneCompat { + + private TestLuceneCompat() {} + + /** + * Calls {@link LeafReader#searchNearestVectors} with the accepted documents given as {@link + * Bits}. Since Lucene 10.3 it takes them as {@link AcceptDocs}. + */ + static void searchNearestVectors( + LeafReader reader, String field, float[] target, KnnCollector collector, Bits acceptDocs) + throws IOException { + reader.searchNearestVectors( + field, target, collector, AcceptDocs.fromLiveDocs(acceptDocs, reader.maxDoc())); + } + + /** + * Calls {@link LeafReader#searchNearestVectors} with the accepted documents given as {@link + * Bits}, {@code null} meaning all. Since Lucene 10.3 it takes them as {@link AcceptDocs}, which + * must not be {@code null}. + */ + static TopDocs searchNearestVectors( + LeafReader reader, String field, float[] target, int k, Bits acceptDocs, int visitedLimit) + throws IOException { + return reader.searchNearestVectors( + field, target, k, AcceptDocs.fromLiveDocs(acceptDocs, reader.maxDoc()), visitedLimit); + } +} diff --git a/java/cuvs-lucene/lucene-10.2/pom.xml b/java/cuvs-lucene/lucene-10.2/pom.xml new file mode 100644 index 0000000000..e74db4da12 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.2/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + com.nvidia.cuvs.lucene + cuvs-lucene-parent + + 26.12.0 + + cuvs-lucene-10.2 + cuvs-lucene-10.2 + jar + + + Pluggable codecs for Apache Lucene 10.2.x that enable GPU-based accelerated vector search. + + + + 10.2.2 + + knn-vectors-reader-102-bits + knn-vectors-writer-102-void-merge + knn-float-vector-query-102-bits + leaf-reader-102-bits + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + diff --git a/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java new file mode 100644 index 0000000000..13f2f0db58 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene101.Lucene101Codec; +import org.apache.lucene.codecs.lucene99.Lucene99HnswScalarQuantizedVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99ScalarQuantizedVectorsFormat; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.ReadAdvice; + +/** + * The Lucene calls that differ between the Lucene releases cuvs-lucene supports, for Lucene 10.2. + * + *

Every lucene-X.Y module has its own copy of this class, compiled against that release. The + * shared sources go through it instead of calling those parts of Lucene directly. + */ +final class LuceneCompat { + + // The Lucene release this module is built for. Both must stay compile-time constants: javac + // copies them into the classes that read them, so LuceneVersionGuard can check the Lucene version + // without loading this class, which does not link against other Lucene releases. + static final int LUCENE_MAJOR = 10; + static final int LUCENE_MINOR = 2; + + // The codec generation that writes on this Lucene release: the version number of the Lucene + // codec that codecs of this generation wrap (Lucene101Codec). Codecs of other generations are + // read-only. Also a compile-time constant, for the same reason. + static final int CURRENT_CODEC_GENERATION = 101; + + private LuceneCompat() {} + + /** Returns {@code context} with a hint that the file will be read sequentially. */ + static IOContext sequentialReadContext(IOContext context) { + return context.withReadAdvice(ReadAdvice.SEQUENTIAL); + } + + /** Returns Lucene 10.2's {@code Lucene101Codec}. */ + static Codec lucene101Codec() { + return new Lucene101Codec(); + } + + /** Returns the flat format that stores the vectors of the Lucene 10.2 and 10.3 SQ codecs. */ + static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { + return new Lucene99ScalarQuantizedVectorsFormat(); + } + + /** + * Returns Lucene's HNSW format over {@link #lucene99ScalarQuantizedFlatFormat()}, which builds + * the graph on the CPU. + */ + static KnnVectorsFormat lucene99HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { + int numMergeWorkers = params.getNumMergeWorkers(); + return new Lucene99HnswScalarQuantizedVectorsFormat( + params.getMaxConn(), + params.getBeamWidth(), + numMergeWorkers, + 7, + false, + null, + numMergeWorkers > 1 ? params.getMergeExec() : null); + } + + // The codecs that write on this Lucene release, returned by CuVSCodecs. + + static Codec acceleratedHNSWCodec(AcceleratedHNSWParams params) { + return new Lucene101AcceleratedHNSWCodec(params); + } + + static Codec acceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams params) { + return new LuceneAcceleratedHNSWScalarQuantizedCodec(params); + } + + static KnnVectorsFormat acceleratedHNSWScalarQuantizedFormat(AcceleratedHNSWParams params) { + return new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(params); + } + + static Codec acceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams params) { + return new LuceneAcceleratedHNSWBinaryQuantizedCodec(params); + } + + static Codec gpuSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + return new CuVS2510GPUSearchCodec(params, filterCacheConfig); + } +} diff --git a/java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec b/java/cuvs-lucene/lucene-10.2/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec similarity index 100% rename from java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec rename to java/cuvs-lucene/lucene-10.2/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec diff --git a/java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat b/java/cuvs-lucene/lucene-10.2/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat similarity index 66% rename from java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat rename to java/cuvs-lucene/lucene-10.2/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat index 6625ac72a4..c0a7e0ffc8 100644 --- a/java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat +++ b/java/cuvs-lucene/lucene-10.2/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat @@ -1,8 +1,8 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 -org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat -org.apache.lucene.codecs.lucene99.Lucene99HnswScalarQuantizedVectorsFormat +# Only cuvs-lucene's own formats. Lucene registers its formats itself, and listing a Lucene class +# that a later release moved makes every KnnVectorsFormat lookup in the JVM fail. com.nvidia.cuvs.lucene.CuVS2510GPUVectorsFormat com.nvidia.cuvs.lucene.Lucene99AcceleratedHNSWVectorsFormat com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat diff --git a/java/cuvs-lucene/lucene-10.3/pom.xml b/java/cuvs-lucene/lucene-10.3/pom.xml new file mode 100644 index 0000000000..71824e4c5f --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + com.nvidia.cuvs.lucene + cuvs-lucene-parent + + 26.12.0 + + cuvs-lucene-10.3 + cuvs-lucene-10.3 + jar + + + Pluggable codecs for Apache Lucene 10.3.x that enable GPU-based accelerated vector search. + + + + 10.3.2 + + knn-vectors-reader-103-acceptdocs + knn-vectors-writer-102-void-merge + knn-float-vector-query-103-acceptdocs + leaf-reader-103-acceptdocs + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + add-since-sources + generate-sources + + add-source + + + + src/since/java + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + diff --git a/java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java new file mode 100644 index 0000000000..ee692aaccf --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import org.apache.lucene.backward_codecs.lucene101.Lucene101Codec; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene103.Lucene103Codec; +import org.apache.lucene.codecs.lucene99.Lucene99HnswScalarQuantizedVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99ScalarQuantizedVectorsFormat; +import org.apache.lucene.store.DataAccessHint; +import org.apache.lucene.store.IOContext; + +/** + * The Lucene calls that differ between the Lucene releases cuvs-lucene supports, for Lucene 10.3. + * + *

Every lucene-X.Y module has its own copy of this class, compiled against that release. The + * shared sources go through it instead of calling those parts of Lucene directly. + */ +final class LuceneCompat { + + // The Lucene release this module is built for. Both must stay compile-time constants: javac + // copies them into the classes that read them, so LuceneVersionGuard can check the Lucene version + // without loading this class, which does not link against other Lucene releases. + static final int LUCENE_MAJOR = 10; + static final int LUCENE_MINOR = 3; + + // The codec generation that writes on this Lucene release: the version number of the Lucene + // codec that codecs of this generation wrap (Lucene103Codec). Codecs of other generations are + // read-only. Also a compile-time constant, for the same reason. + static final int CURRENT_CODEC_GENERATION = 103; + + private LuceneCompat() {} + + /** Returns {@code context} with a hint that the file will be read sequentially. */ + static IOContext sequentialReadContext(IOContext context) { + return context.withHints(DataAccessHint.SEQUENTIAL); + } + + /** Returns Lucene 10.2's {@code Lucene101Codec}, which Lucene 10.3 can only read. */ + static Codec lucene101Codec() { + return new Lucene101Codec(); + } + + /** Returns Lucene 10.3's {@code Lucene103Codec}. */ + static Codec lucene103Codec() { + return new Lucene103Codec(); + } + + /** Returns the flat format that stores the vectors of the Lucene 10.2 and 10.3 SQ codecs. */ + static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { + return new Lucene99ScalarQuantizedVectorsFormat(); + } + + /** + * Returns Lucene's HNSW format over {@link #lucene99ScalarQuantizedFlatFormat()}, which builds + * the graph on the CPU. + */ + static KnnVectorsFormat lucene99HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { + int numMergeWorkers = params.getNumMergeWorkers(); + return new Lucene99HnswScalarQuantizedVectorsFormat( + params.getMaxConn(), + params.getBeamWidth(), + numMergeWorkers, + 7, + false, + null, + numMergeWorkers > 1 ? params.getMergeExec() : null); + } + + // The codecs that write on this Lucene release, returned by CuVSCodecs. + + static Codec acceleratedHNSWCodec(AcceleratedHNSWParams params) { + return new Lucene103AcceleratedHNSWCodec(params); + } + + static Codec acceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams params) { + return new Lucene103AcceleratedHNSWScalarQuantizedCodec(params); + } + + static KnnVectorsFormat acceleratedHNSWScalarQuantizedFormat(AcceleratedHNSWParams params) { + return new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(params); + } + + static Codec acceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams params) { + return new Lucene103AcceleratedHNSWBinaryQuantizedCodec(params); + } + + static Codec gpuSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + return new Lucene103CuVSGPUSearchCodec(params, filterCacheConfig); + } +} diff --git a/java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec b/java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec new file mode 100644 index 0000000000..e503916f23 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Every codec generation this Lucene release can read. Only the newest one can write. +com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.Lucene103CuVSGPUSearchCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWScalarQuantizedCodec diff --git a/java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat b/java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat new file mode 100644 index 0000000000..506efdf381 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Only cuvs-lucene's own formats. Lucene registers its formats itself, and listing a Lucene class +# that a later release moved makes every KnnVectorsFormat lookup in the JVM fail. +com.nvidia.cuvs.lucene.CuVS2510GPUVectorsFormat +com.nvidia.cuvs.lucene.Lucene99AcceleratedHNSWVectorsFormat +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedVectorsFormat diff --git a/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWBinaryQuantizedCodec.java b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWBinaryQuantizedCodec.java new file mode 100644 index 0000000000..ff01041d8a --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWBinaryQuantizedCodec.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that builds HNSW graphs on the GPU over binary-quantized vectors, and writes them in + * Lucene's HNSW format so that they are searched on the CPU. See {@link + * LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat}. + * + *

This codec wraps {@code Lucene103Codec}, the default codec of Lucene 10.3. Once a later Lucene + * release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#acceleratedHNSWBinaryQuantized} to write. + * + * @since 26.12 + */ +public class Lucene103AcceleratedHNSWBinaryQuantizedCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene103AcceleratedHNSWBinaryQuantizedCodec"; + + /** Creates the codec with default parameters. */ + public Lucene103AcceleratedHNSWBinaryQuantizedCodec() { + this(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Creates the codec. + * + * @param acceleratedHNSWParams the index build parameters + */ + public Lucene103AcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 103, + () -> LuceneCompat.lucene103Codec(), + () -> new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat(acceleratedHNSWParams)); + } +} diff --git a/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWCodec.java b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWCodec.java new file mode 100644 index 0000000000..55cfff3f04 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWCodec.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that builds HNSW graphs on the GPU, and writes them in Lucene's HNSW format so that they + * are searched on the CPU. Falls back to building the graph on the CPU when no GPU or cuVS is + * available. + * + *

This codec wraps {@code Lucene103Codec}, the default codec of Lucene 10.3. Once a later Lucene + * release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#acceleratedHNSW} to write. + * + * @since 26.12 + */ +public class Lucene103AcceleratedHNSWCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene103AcceleratedHNSWCodec"; + + /** Creates the codec with default parameters. */ + public Lucene103AcceleratedHNSWCodec() { + this(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Creates the codec. + * + * @param acceleratedHNSWParams the index build parameters + */ + public Lucene103AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 103, + () -> LuceneCompat.lucene103Codec(), + () -> new Lucene99AcceleratedHNSWVectorsFormat(acceleratedHNSWParams)); + } +} diff --git a/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWScalarQuantizedCodec.java b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWScalarQuantizedCodec.java new file mode 100644 index 0000000000..7b0aa83040 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103AcceleratedHNSWScalarQuantizedCodec.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that builds HNSW graphs on the GPU over scalar-quantized vectors, and writes them in + * Lucene's HNSW format so that they are searched on the CPU. See {@link + * LuceneAcceleratedHNSWScalarQuantizedVectorsFormat}. + * + *

This codec wraps {@code Lucene103Codec}, the default codec of Lucene 10.3. Once a later Lucene + * release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#acceleratedHNSWScalarQuantized} to write. + * + * @since 26.12 + */ +public class Lucene103AcceleratedHNSWScalarQuantizedCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene103AcceleratedHNSWScalarQuantizedCodec"; + + /** Creates the codec with default parameters. */ + public Lucene103AcceleratedHNSWScalarQuantizedCodec() { + this(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Creates the codec. + * + * @param acceleratedHNSWParams the index build parameters + */ + public Lucene103AcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 103, + () -> LuceneCompat.lucene103Codec(), + () -> new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(acceleratedHNSWParams)); + } +} diff --git a/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103CuVSGPUSearchCodec.java b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103CuVSGPUSearchCodec.java new file mode 100644 index 0000000000..c3e5bd08e6 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.3/src/since/java/com/nvidia/cuvs/lucene/Lucene103CuVSGPUSearchCodec.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that both builds and searches CAGRA indexes on the GPU. cuVS serialization formats are in + * experimental phase and hence backward compatibility cannot be guaranteed. + * + *

This codec wraps {@code Lucene103Codec}, the default codec of Lucene 10.3. Once a later Lucene + * release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#gpuSearch} to write. + * + * @since 26.12 + */ +public class Lucene103CuVSGPUSearchCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene103CuVSGPUSearchCodec"; + + /** Creates the codec with default parameters. */ + public Lucene103CuVSGPUSearchCodec() { + this(new GPUSearchParams.Builder().build(), FilterBitsetCacheConfig.DEFAULT); + } + + /** + * Creates the codec. + * + * @param params GPU index and search parameters + * @param filterCacheConfig filter-bitset-cache configuration + */ + public Lucene103CuVSGPUSearchCodec( + GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + super( + NAME, + 103, + () -> LuceneCompat.lucene103Codec(), + () -> new CuVS2510GPUVectorsFormat(params, filterCacheConfig)); + } +} diff --git a/java/cuvs-lucene/lucene-10.4/pom.xml b/java/cuvs-lucene/lucene-10.4/pom.xml new file mode 100644 index 0000000000..46fc45b575 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + com.nvidia.cuvs.lucene + cuvs-lucene-parent + + 26.12.0 + + cuvs-lucene-10.4 + cuvs-lucene-10.4 + jar + + + Pluggable codecs for Apache Lucene 10.4.x that enable GPU-based accelerated vector search. + + + + 10.4.0 + + knn-vectors-reader-103-acceptdocs + knn-vectors-writer-102-void-merge + knn-float-vector-query-103-acceptdocs + leaf-reader-103-acceptdocs + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + add-since-sources + generate-sources + + add-source + + + + ../lucene-10.3/src/since/java + src/since/java + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + diff --git a/java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java new file mode 100644 index 0000000000..b07263d09d --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import org.apache.lucene.backward_codecs.lucene101.Lucene101Codec; +import org.apache.lucene.backward_codecs.lucene103.Lucene103Codec; +import org.apache.lucene.backward_codecs.lucene99.Lucene99ScalarQuantizedVectorsFormat; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene104.Lucene104Codec; +import org.apache.lucene.codecs.lucene104.Lucene104HnswScalarQuantizedVectorsFormat; +import org.apache.lucene.codecs.lucene104.Lucene104ScalarQuantizedVectorsFormat; +import org.apache.lucene.codecs.lucene104.Lucene104ScalarQuantizedVectorsFormat.ScalarEncoding; +import org.apache.lucene.store.DataAccessHint; +import org.apache.lucene.store.IOContext; + +/** + * The Lucene calls that differ between the Lucene releases cuvs-lucene supports, for Lucene 10.4. + * + *

Every lucene-X.Y module has its own copy of this class, compiled against that release. The + * shared sources go through it instead of calling those parts of Lucene directly. + */ +final class LuceneCompat { + + // The Lucene release this module is built for. Both must stay compile-time constants: javac + // copies them into the classes that read them, so LuceneVersionGuard can check the Lucene version + // without loading this class, which does not link against other Lucene releases. + static final int LUCENE_MAJOR = 10; + static final int LUCENE_MINOR = 4; + + // The codec generation that writes on this Lucene release: the version number of the Lucene + // codec that codecs of this generation wrap (Lucene104Codec). Codecs of other generations are + // read-only. Also a compile-time constant, for the same reason. + static final int CURRENT_CODEC_GENERATION = 104; + + private LuceneCompat() {} + + /** Returns {@code context} with a hint that the file will be read sequentially. */ + static IOContext sequentialReadContext(IOContext context) { + return context.withHints(DataAccessHint.SEQUENTIAL); + } + + /** Returns Lucene 10.2's {@code Lucene101Codec}, which Lucene 10.4 can only read. */ + static Codec lucene101Codec() { + return new Lucene101Codec(); + } + + /** Returns Lucene 10.3's {@code Lucene103Codec}, which Lucene 10.4 can only read. */ + static Codec lucene103Codec() { + return new Lucene103Codec(); + } + + /** Returns Lucene 10.4's {@code Lucene104Codec}. */ + static Codec lucene104Codec() { + return new Lucene104Codec(); + } + + /** + * Returns the flat format that stores the vectors of the Lucene 10.2 and 10.3 SQ codecs, which + * Lucene 10.4 can only read. + */ + static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { + return new Lucene99ScalarQuantizedVectorsFormat(); + } + + /** Lucene 10.4 cannot write {@link #lucene99ScalarQuantizedFlatFormat()}, so this throws. */ + static KnnVectorsFormat lucene99HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { + throw new UnsupportedOperationException("Old codecs may only be used for reading"); + } + + /** Returns the flat format that stores the vectors of the SQ codecs since Lucene 10.4. */ + static FlatVectorsFormat lucene104ScalarQuantizedFlatFormat() { + return new Lucene104ScalarQuantizedVectorsFormat(ScalarEncoding.SEVEN_BIT); + } + + /** + * Returns Lucene's HNSW format over {@link #lucene104ScalarQuantizedFlatFormat()}, which builds + * the graph on the CPU. + */ + static KnnVectorsFormat lucene104HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { + int numMergeWorkers = params.getNumMergeWorkers(); + return new Lucene104HnswScalarQuantizedVectorsFormat( + ScalarEncoding.SEVEN_BIT, + params.getMaxConn(), + params.getBeamWidth(), + numMergeWorkers, + numMergeWorkers > 1 ? params.getMergeExec() : null); + } + + // The codecs that write on this Lucene release, returned by CuVSCodecs. + + static Codec acceleratedHNSWCodec(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWCodec(params); + } + + static Codec acceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWScalarQuantizedCodec(params); + } + + static KnnVectorsFormat acceleratedHNSWScalarQuantizedFormat(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat(params); + } + + static Codec acceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWBinaryQuantizedCodec(params); + } + + static Codec gpuSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + return new Lucene104CuVSGPUSearchCodec(params, filterCacheConfig); + } +} diff --git a/java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec b/java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec new file mode 100644 index 0000000000..57a189938f --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Every codec generation this Lucene release can read. Only the newest one can write. +com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.Lucene103CuVSGPUSearchCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWScalarQuantizedCodec +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.Lucene104CuVSGPUSearchCodec +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWScalarQuantizedCodec diff --git a/java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat b/java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat new file mode 100644 index 0000000000..02c0129db0 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Only cuvs-lucene's own formats. Lucene registers its formats itself, and listing a Lucene class +# that a later release moved makes every KnnVectorsFormat lookup in the JVM fail. +com.nvidia.cuvs.lucene.CuVS2510GPUVectorsFormat +com.nvidia.cuvs.lucene.Lucene99AcceleratedHNSWVectorsFormat +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedVectorsFormat +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat diff --git a/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWBinaryQuantizedCodec.java b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWBinaryQuantizedCodec.java new file mode 100644 index 0000000000..f8b86d07f8 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWBinaryQuantizedCodec.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that builds HNSW graphs on the GPU over binary-quantized vectors, and writes them in + * Lucene's HNSW format so that they are searched on the CPU. See {@link + * LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat}. + * + *

This codec wraps {@code Lucene104Codec}, the default codec of Lucene 10.4 and 10.5. Once a + * later Lucene release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#acceleratedHNSWBinaryQuantized} to write. + * + * @since 26.12 + */ +public class Lucene104AcceleratedHNSWBinaryQuantizedCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene104AcceleratedHNSWBinaryQuantizedCodec"; + + /** Creates the codec with default parameters. */ + public Lucene104AcceleratedHNSWBinaryQuantizedCodec() { + this(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Creates the codec. + * + * @param acceleratedHNSWParams the index build parameters + */ + public Lucene104AcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 104, + () -> LuceneCompat.lucene104Codec(), + () -> new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat(acceleratedHNSWParams)); + } +} diff --git a/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWCodec.java b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWCodec.java new file mode 100644 index 0000000000..1eb3b16d05 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWCodec.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that builds HNSW graphs on the GPU, and writes them in Lucene's HNSW format so that they + * are searched on the CPU. Falls back to building the graph on the CPU when no GPU or cuVS is + * available. + * + *

This codec wraps {@code Lucene104Codec}, the default codec of Lucene 10.4 and 10.5. Once a + * later Lucene release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#acceleratedHNSW} to write. + * + * @since 26.12 + */ +public class Lucene104AcceleratedHNSWCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene104AcceleratedHNSWCodec"; + + /** Creates the codec with default parameters. */ + public Lucene104AcceleratedHNSWCodec() { + this(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Creates the codec. + * + * @param acceleratedHNSWParams the index build parameters + */ + public Lucene104AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 104, + () -> LuceneCompat.lucene104Codec(), + () -> new Lucene99AcceleratedHNSWVectorsFormat(acceleratedHNSWParams)); + } +} diff --git a/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedCodec.java b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedCodec.java new file mode 100644 index 0000000000..216604baf4 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedCodec.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that builds HNSW graphs on the GPU over scalar-quantized vectors, and writes them in + * Lucene's HNSW format so that they are searched on the CPU. See {@link + * Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat}. + * + *

This codec wraps {@code Lucene104Codec}, the default codec of Lucene 10.4 and 10.5. Once a + * later Lucene release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#acceleratedHNSWScalarQuantized} to write. + * + * @since 26.12 + */ +public class Lucene104AcceleratedHNSWScalarQuantizedCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene104AcceleratedHNSWScalarQuantizedCodec"; + + /** Creates the codec with default parameters. */ + public Lucene104AcceleratedHNSWScalarQuantizedCodec() { + this(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Creates the codec. + * + * @param acceleratedHNSWParams the index build parameters + */ + public Lucene104AcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 104, + () -> LuceneCompat.lucene104Codec(), + () -> new Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat(acceleratedHNSWParams)); + } +} diff --git a/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java new file mode 100644 index 0000000000..48141dccdc --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat.java @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * cuVS based scalar-quantized KnnVectorsFormat for indexing on the GPU and searching on the CPU. + * + *

Stores the vectors with Lucene's {@code Lucene104ScalarQuantizedVectorsFormat}, quantized to 7 + * bits per dimension, which replaced {@code Lucene99ScalarQuantizedVectorsFormat} in Lucene 10.4. + * The graph is built on the GPU over the vectors quantized the same way as {@link + * LuceneAcceleratedHNSWScalarQuantizedVectorsFormat} does. + * + * @since 26.12 + */ +public class Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat + extends BaseAcceleratedHNSWScalarQuantizedVectorsFormat { + + /** Creates the format with default parameters. */ + public Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat() { + this(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Creates the format. + * + * @param acceleratedHNSWParams the index build parameters + */ + public Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat( + AcceleratedHNSWParams acceleratedHNSWParams) { + super( + "Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat", + acceleratedHNSWParams, + () -> LuceneCompat.lucene104ScalarQuantizedFlatFormat(), + params -> LuceneCompat.lucene104HnswScalarQuantizedFormat(params)); + } +} diff --git a/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104CuVSGPUSearchCodec.java b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104CuVSGPUSearchCodec.java new file mode 100644 index 0000000000..25b1c98c15 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.4/src/since/java/com/nvidia/cuvs/lucene/Lucene104CuVSGPUSearchCodec.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +/** + * A codec that both builds and searches CAGRA indexes on the GPU. cuVS serialization formats are in + * experimental phase and hence backward compatibility cannot be guaranteed. + * + *

This codec wraps {@code Lucene104Codec}, the default codec of Lucene 10.4 and 10.5. Once a + * later Lucene release replaces that default, this codec can only read existing indexes; use {@link + * CuVSCodecs#gpuSearch} to write. + * + * @since 26.12 + */ +public class Lucene104CuVSGPUSearchCodec extends CuVSFilterCodec { + + private static final String NAME = "Lucene104CuVSGPUSearchCodec"; + + /** Creates the codec with default parameters. */ + public Lucene104CuVSGPUSearchCodec() { + this(new GPUSearchParams.Builder().build(), FilterBitsetCacheConfig.DEFAULT); + } + + /** + * Creates the codec. + * + * @param params GPU index and search parameters + * @param filterCacheConfig filter-bitset-cache configuration + */ + public Lucene104CuVSGPUSearchCodec( + GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + super( + NAME, + 104, + () -> LuceneCompat.lucene104Codec(), + () -> new CuVS2510GPUVectorsFormat(params, filterCacheConfig)); + } +} diff --git a/java/cuvs-lucene/lucene-10.5/pom.xml b/java/cuvs-lucene/lucene-10.5/pom.xml new file mode 100644 index 0000000000..2063de5dfb --- /dev/null +++ b/java/cuvs-lucene/lucene-10.5/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + com.nvidia.cuvs.lucene + cuvs-lucene-parent + + 26.12.0 + + cuvs-lucene-10.5 + cuvs-lucene-10.5 + jar + + + Pluggable codecs for Apache Lucene 10.5.x that enable GPU-based accelerated vector search. + + + + 10.5.1 + + knn-vectors-reader-103-acceptdocs + knn-vectors-writer-105-deferred-merge + knn-float-vector-query-103-acceptdocs + leaf-reader-103-acceptdocs + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + add-since-sources + generate-sources + + add-source + + + + ../lucene-10.3/src/since/java + ../lucene-10.4/src/since/java + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + diff --git a/java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java new file mode 100644 index 0000000000..2871ca0f0a --- /dev/null +++ b/java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import org.apache.lucene.backward_codecs.lucene101.Lucene101Codec; +import org.apache.lucene.backward_codecs.lucene103.Lucene103Codec; +import org.apache.lucene.backward_codecs.lucene99.Lucene99ScalarQuantizedVectorsFormat; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene104.Lucene104Codec; +import org.apache.lucene.codecs.lucene104.Lucene104HnswScalarQuantizedVectorsFormat; +import org.apache.lucene.codecs.lucene104.Lucene104ScalarQuantizedVectorsFormat; +import org.apache.lucene.store.DataAccessHint; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.util.quantization.QuantizedByteVectorValues.ScalarEncoding; + +/** + * The Lucene calls that differ between the Lucene releases cuvs-lucene supports, for Lucene 10.5. + * + *

Every lucene-X.Y module has its own copy of this class, compiled against that release. The + * shared sources go through it instead of calling those parts of Lucene directly. + */ +final class LuceneCompat { + + // The Lucene release this module is built for. Both must stay compile-time constants: javac + // copies them into the classes that read them, so LuceneVersionGuard can check the Lucene version + // without loading this class, which does not link against other Lucene releases. + static final int LUCENE_MAJOR = 10; + static final int LUCENE_MINOR = 5; + + // The codec generation that writes on this Lucene release: the version number of the Lucene + // codec that codecs of this generation wrap (Lucene104Codec). Codecs of other generations are + // read-only. Also a compile-time constant, for the same reason. + static final int CURRENT_CODEC_GENERATION = 104; + + private LuceneCompat() {} + + /** Returns {@code context} with a hint that the file will be read sequentially. */ + static IOContext sequentialReadContext(IOContext context) { + return context.withHints(DataAccessHint.SEQUENTIAL); + } + + /** Returns Lucene 10.2's {@code Lucene101Codec}, which Lucene 10.5 can only read. */ + static Codec lucene101Codec() { + return new Lucene101Codec(); + } + + /** Returns Lucene 10.3's {@code Lucene103Codec}, which Lucene 10.5 can only read. */ + static Codec lucene103Codec() { + return new Lucene103Codec(); + } + + /** Returns {@code Lucene104Codec}, which is still the default codec of Lucene 10.5. */ + static Codec lucene104Codec() { + return new Lucene104Codec(); + } + + /** + * Returns the flat format that stores the vectors of the Lucene 10.2 and 10.3 SQ codecs, which + * Lucene 10.5 can only read. + */ + static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { + return new Lucene99ScalarQuantizedVectorsFormat(); + } + + /** Lucene 10.5 cannot write {@link #lucene99ScalarQuantizedFlatFormat()}, so this throws. */ + static KnnVectorsFormat lucene99HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { + throw new UnsupportedOperationException("Old codecs may only be used for reading"); + } + + /** Returns the flat format that stores the vectors of the SQ codecs since Lucene 10.4. */ + static FlatVectorsFormat lucene104ScalarQuantizedFlatFormat() { + return new Lucene104ScalarQuantizedVectorsFormat(ScalarEncoding.SEVEN_BIT); + } + + /** + * Returns Lucene's HNSW format over {@link #lucene104ScalarQuantizedFlatFormat()}, which builds + * the graph on the CPU. + */ + static KnnVectorsFormat lucene104HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { + int numMergeWorkers = params.getNumMergeWorkers(); + return new Lucene104HnswScalarQuantizedVectorsFormat( + ScalarEncoding.SEVEN_BIT, + params.getMaxConn(), + params.getBeamWidth(), + numMergeWorkers, + numMergeWorkers > 1 ? params.getMergeExec() : null); + } + + // The codecs that write on this Lucene release, returned by CuVSCodecs. + + static Codec acceleratedHNSWCodec(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWCodec(params); + } + + static Codec acceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWScalarQuantizedCodec(params); + } + + static KnnVectorsFormat acceleratedHNSWScalarQuantizedFormat(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat(params); + } + + static Codec acceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams params) { + return new Lucene104AcceleratedHNSWBinaryQuantizedCodec(params); + } + + static Codec gpuSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + return new Lucene104CuVSGPUSearchCodec(params, filterCacheConfig); + } +} diff --git a/java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec b/java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec new file mode 100644 index 0000000000..57a189938f --- /dev/null +++ b/java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.Codec @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Every codec generation this Lucene release can read. Only the newest one can write. +com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.Lucene103CuVSGPUSearchCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.Lucene103AcceleratedHNSWScalarQuantizedCodec +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWCodec +com.nvidia.cuvs.lucene.Lucene104CuVSGPUSearchCodec +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWBinaryQuantizedCodec +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWScalarQuantizedCodec diff --git a/java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat b/java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat new file mode 100644 index 0000000000..02c0129db0 --- /dev/null +++ b/java/cuvs-lucene/lucene-10.5/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Only cuvs-lucene's own formats. Lucene registers its formats itself, and listing a Lucene class +# that a later release moved makes every KnnVectorsFormat lookup in the JVM fail. +com.nvidia.cuvs.lucene.CuVS2510GPUVectorsFormat +com.nvidia.cuvs.lucene.Lucene99AcceleratedHNSWVectorsFormat +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat +com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedVectorsFormat +com.nvidia.cuvs.lucene.Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat diff --git a/java/cuvs-lucene/pom.xml b/java/cuvs-lucene/pom.xml index a9c7483ea0..e292315afc 100644 --- a/java/cuvs-lucene/pom.xml +++ b/java/cuvs-lucene/pom.xml @@ -2,19 +2,26 @@ SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> + + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" + child.project.url.inherit.append.path="false"> 4.0.0 com.nvidia.cuvs.lucene - cuvs-lucene + cuvs-lucene-parent 26.12.0 - cuvs-lucene - jar + cuvs-lucene-parent + pom This project provides pluggable codecs for Apache Lucene to enable GPU-based accelerated vector search. + Lucene changes its codec APIs between minor releases, so there is one artifact per supported Lucene + minor version (cuvs-lucene-10.2, cuvs-lucene-10.3, ...), all built from the same sources. https://rapids.ai @@ -26,7 +33,9 @@ SPDX-License-Identifier: Apache-2.0 A business-friendly OSS license - + scm:git:https://github.com/rapidsai/cuvs.git scm:git:git@github.com:rapidsai/cuvs.git HEAD @@ -45,11 +54,37 @@ SPDX-License-Identifier: Apache-2.0 + + + lucene-10.2 + lucene-10.3 + lucene-10.4 + lucene-10.5 + + relocation + + - 22 - 22 + 21 UTF-8 UTF-8 + + 10.5.1 + + ${project.basedir}/../src + + ${project.basedir}/../compat @@ -106,148 +141,307 @@ SPDX-License-Identifier: Apache-2.0 + org.apache.lucene lucene-core - 10.2.0 + ${lucene.version} org.apache.lucene - lucene-codecs - 10.2.0 - test + lucene-backward-codecs + ${lucene.version} org.apache.lucene - lucene-backward-codecs - 10.2.0 + lucene-codecs + ${lucene.version} + test org.apache.lucene lucene-test-framework - 10.2.0 + ${lucene.version} test commons-io commons-io - 2.18.0 test com.nvidia.cuvs cuvs-java - 26.12.0 + + + + com.nvidia.cuvs + cuvs-java + 26.12.0 + + + commons-io + commons-io + 2.18.0 + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.2 + + + add-shared-sources + generate-sources + + add-source + + + + ${cuvs.lucene.shared.dir}/main/java + + ${cuvs.lucene.compat.dir}/${cuvs.lucene.compat.knnVectorsReader}/src/main/java + ${cuvs.lucene.compat.dir}/${cuvs.lucene.compat.knnVectorsWriter}/src/main/java + ${cuvs.lucene.compat.dir}/${cuvs.lucene.compat.knnFloatVectorQuery}/src/main/java + + + + + add-shared-resources + generate-resources + + add-resource + + + + + ${cuvs.lucene.shared.dir}/main/resources + + + + + + add-shared-test-sources + generate-test-sources + + add-test-source + + + + ${cuvs.lucene.shared.dir}/test/java + ${cuvs.lucene.compat.dir}/${cuvs.lucene.compat.leafReader}/src/test/java + + + + + add-shared-test-resources + generate-test-resources + + add-test-resource + + + + + ${cuvs.lucene.shared.dir}/test/resources + + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.8.0 + + ossrh + ${project.build.directory} + pom.xml + + + + flatten + process-resources + + flatten + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + ${cuvs.lucene.shared.dir}/assembly/jar-with-dependencies.xml + + + + + true + + + + true + + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + plain + false + false + false + 0 + + ${cuvs.lucene.shared.dir}/main/resources/logging.properties + + + ${project.build.directory}/cufile.log + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + attach-javadocs + + jar + + + com.nvidia.cuvs.lucene + all,-missing + ${project.build.directory}/javadocs + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + + prepare-agent + + + + report + prepare-package + + report + + + + + + + + - com.diffplug.spotless - spotless-maven-plugin - 2.44.5 - - - - src/**/*.java - - - 1.27.0 - - true - false - - - + org.codehaus.mojo + build-helper-maven-plugin - org.apache.maven.plugins - maven-assembly-plugin - 3.6.0 - - - jar-with-dependencies - - - - - make-assembly - package - - single - - - + org.codehaus.mojo + flatten-maven-plugin org.apache.maven.plugins maven-surefire-plugin - 3.5.4 - - plain - false - false - false - 0 - - ${project.basedir}/src/main/resources/logging.properties - - - ${project.build.directory}/cufile.log - - org.apache.maven.plugins maven-source-plugin - 3.3.1 - - - attach-sources - - jar-no-fork - - - org.apache.maven.plugins maven-javadoc-plugin - 3.12.0 - - - attach-javadocs - - jar - - - com.nvidia.cuvs.lucene - all,-missing - ${project.build.directory}/javadocs - - - - org.jacoco - jacoco-maven-plugin - 0.8.14 - - - - prepare-agent - - - - report - prepare-package - - report - - - + org.jacoco + jacoco-maven-plugin + + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.5 + + + + src/**/*.java + compat/**/*.java + + + 1.27.0 + + true + false + + + diff --git a/java/cuvs-lucene/relocation/pom.xml b/java/cuvs-lucene/relocation/pom.xml new file mode 100644 index 0000000000..05e896708a --- /dev/null +++ b/java/cuvs-lucene/relocation/pom.xml @@ -0,0 +1,129 @@ + + + + 4.0.0 + com.nvidia.cuvs.lucene + cuvs-lucene + + 26.12.0 + pom + cuvs-lucene + + Relocated: cuvs-lucene is now published once per Lucene minor release, as cuvs-lucene-10.2, + cuvs-lucene-10.3, cuvs-lucene-10.4 and cuvs-lucene-10.5. + + https://rapids.ai + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + scm:git:https://github.com/rapidsai/cuvs.git + scm:git:git@github.com:rapidsai/cuvs.git + HEAD + https://github.com/rapidsai/cuvs + + + + rapidsai + rapidsai + cuvs@rapids.ai + + + + + + cuvs-lucene-10.2 + cuvs-lucene is now published once per Lucene minor release: depend on cuvs-lucene-10.2, cuvs-lucene-10.3, cuvs-lucene-10.4 or cuvs-lucene-10.5, matching the Lucene version you use. This relocation points to cuvs-lucene-10.2, which matches the Lucene release the old artifact was built for. + + + + + + + gcs-maven-central-mirror + GCS Maven Central mirror + https://maven-central.storage-download.googleapis.com/maven2/ + + true + + + false + + + + central + Maven Plugin Repository + https://repo.maven.apache.org/maven2 + + true + + + false + + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.5 + + true + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-pom-to-target + package + + copy-resources + + + ${project.build.directory} + + + ${project.basedir} + + pom.xml + + + + + + + + + + diff --git a/java/cuvs-lucene/src/assembly/jar-with-dependencies.xml b/java/cuvs-lucene/src/assembly/jar-with-dependencies.xml new file mode 100644 index 0000000000..6ab70061da --- /dev/null +++ b/java/cuvs-lucene/src/assembly/jar-with-dependencies.xml @@ -0,0 +1,32 @@ + + + + jar-with-dependencies + + jar + + false + + + / + true + true + runtime + + + + + metaInf-services + + + 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 9c49c07fe0..8160667734 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 @@ -19,6 +19,7 @@ import java.util.Random; import java.util.SortedSet; import java.util.TreeSet; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.VectorSimilarityFunction; import org.apache.lucene.store.IndexOutput; @@ -36,17 +37,8 @@ public enum QuantizationType { NONE } - private static final LuceneProvider LUCENE_PROVIDER; - private static final List VECTOR_SIMILARITY_FUNCTIONS; - - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - VECTOR_SIMILARITY_FUNCTIONS = LUCENE_PROVIDER.getSimilarityFunctions(); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } + private static final List VECTOR_SIMILARITY_FUNCTIONS = + Lucene99HnswVectorsReader.SIMILARITY_FUNCTIONS; /** * Creates a dummy HNSW graph for a single vector. @@ -227,6 +219,21 @@ private static CuVSMatrix buildCagraGraphForSubset( return CuVSMatrix.ofArray(remappedAdjacency); } + /** + * Returns the nodes of a graph level in ascending order. + * + * @param nodesOnLevel iterates over the nodes of one level + * @return the sorted nodes + */ + private static int[] getSortedNodes(NodesIterator nodesOnLevel) { + int[] sortedNodes = new int[nodesOnLevel.size()]; + for (int n = 0; nodesOnLevel.hasNext(); n++) { + sortedNodes[n] = nodesOnLevel.nextInt(); + } + Arrays.sort(sortedNodes); + return sortedNodes; + } + /** * Returns a 2D array of offsets (information written while writing the meta info) * @@ -242,7 +249,7 @@ public static int[][] writeGraph(GPUBuiltHnswGraph graph, IndexOutput vectorInde int[][] offsets = new int[graph.numLevels()][]; int[] scratch = new int[graph.maxConn() * 2]; for (int level = 0; level < graph.numLevels(); level++) { - int[] sortedNodes = NodesIterator.getSortedNodes(graph.getNodesOnLevel(level)); + int[] sortedNodes = getSortedNodes(graph.getNodesOnLevel(level)); offsets[level] = new int[sortedNodes.length]; int nodeOffsetId = 0; diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java new file mode 100644 index 0000000000..896137c634 --- /dev/null +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; + +import java.io.IOException; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Logger; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.index.SegmentWriteState; + +/** + * Base class of the cuVS based scalar-quantized formats, which build the graph on the GPU and write + * it in Lucene's HNSW format for search on the CPU. The formats differ in the Lucene flat format + * that stores the quantized vectors. + */ +abstract class BaseAcceleratedHNSWScalarQuantizedVectorsFormat extends KnnVectorsFormat { + + private static final Logger log = + Logger.getLogger(BaseAcceleratedHNSWScalarQuantizedVectorsFormat.class.getName()); + private static final int MAX_DIMENSIONS = 4096; + + private final AcceleratedHNSWParams acceleratedHNSWParams; + private final Supplier flatVectorsFormat; + private final Function cpuFormat; + + /** + * Initializes the format. + * + * @param name the format's name + * @param acceleratedHNSWParams the index build parameters + * @param flatVectorsFormat creates the format that stores the quantized vectors + * @param cpuFormat returns the Lucene format to build the graph with when no GPU is available; + * it has to store the vectors with {@code flatVectorsFormat} + *

Both are only called when the format is used, never while Lucene's service loader creates + * it, so pass lambdas rather than method references to {@code LuceneCompat}, which may not + * link against the Lucene release in use (see {@link CuVSFilterCodec#delegate}). + */ + BaseAcceleratedHNSWScalarQuantizedVectorsFormat( + String name, + AcceleratedHNSWParams acceleratedHNSWParams, + Supplier flatVectorsFormat, + Function cpuFormat) { + super(name); + this.acceleratedHNSWParams = acceleratedHNSWParams; + this.flatVectorsFormat = flatVectorsFormat; + this.cpuFormat = cpuFormat; + } + + /** + * Returns a KnnVectorsWriter to write the scalar quantized vectors to the index. + */ + @Override + public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + LuceneVersionGuard.ensureCompatible(); + if (isSupported()) { + log.fine("cuVS is supported so using the Lucene99AcceleratedHNSWQuantizedVectorsWriter"); + return new LuceneAcceleratedHNSWScalarQuantizedVectorsWriter( + state, acceleratedHNSWParams, flatVectorsFormat.get().fieldsWriter(state)); + } else { + KnnVectorsFormat fallback = cpuFormat.apply(acceleratedHNSWParams); + // The class name, not getName(): in Lucene 10.4 and 10.5, + // Lucene104HnswScalarQuantizedVectorsFormat + // reports the name of its binary-quantized sibling. + log.warning( + "GPU based indexing not supported, falling back to using the " + + fallback.getClass().getSimpleName()); + return fallback.fieldsWriter(state); + } + } + + /** + * Returns a KnnVectorsReader to read the scalar quantized vectors from the index. + */ + @Override + public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException { + LuceneVersionGuard.ensureCompatible(); + return new Lucene99HnswVectorsReader(state, flatVectorsFormat.get().fieldsReader(state)); + } + + /** + * Returns the maximum number of vector dimensions supported by this Codec for the given field name. + */ + @Override + public int getMaxDimensions(String fieldName) { + return MAX_DIMENSIONS; + } +} diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java index ad286904cf..5b4e2c06c7 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUSearchCodec.java @@ -4,36 +4,26 @@ */ package com.nvidia.cuvs.lucene; -import com.nvidia.cuvs.LibraryException; -import java.util.logging.Level; -import java.util.logging.Logger; import org.apache.lucene.codecs.Codec; -import org.apache.lucene.codecs.FilterCodec; -import org.apache.lucene.codecs.KnnVectorsFormat; /** * cuVS based codec for GPU based vector search that enables both - indexing and search on the GPU. * cuVS serialization formats are in experimental phase and hence backward compatibility cannot be guaranteed. * + *

This codec wraps {@code Lucene101Codec}, the default codec of Lucene 10.2. On later Lucene + * releases it can only read existing indexes; use {@link CuVSCodecs#gpuSearch} to write. + * * @since 25.10 */ -public class CuVS2510GPUSearchCodec extends FilterCodec { +public class CuVS2510GPUSearchCodec extends CuVSFilterCodec { - private static final Logger log = Logger.getLogger(CuVS2510GPUSearchCodec.class.getName()); private static final String NAME = "CuVS2510GPUSearchCodec"; - private KnnVectorsFormat format; /** * Default constructor for {@link CuVS2510GPUSearchCodec}. - * - * @throws Exception */ - public CuVS2510GPUSearchCodec() throws Exception { - this( - NAME, - LuceneProvider.getCodec("101"), - new GPUSearchParams.Builder().build(), - FilterBitsetCacheConfig.DEFAULT); + public CuVS2510GPUSearchCodec() { + this(new GPUSearchParams.Builder().build(), FilterBitsetCacheConfig.DEFAULT); } /** @@ -52,10 +42,9 @@ public CuVS2510GPUSearchCodec(String name, Codec delegate) { * or overridden parameter values. * * @param params An instance of {@link GPUSearchParams} - * @throws Exception Exception raised when initializing the codec */ - public CuVS2510GPUSearchCodec(GPUSearchParams params) throws Exception { - this(NAME, LuceneProvider.getCodec("101"), params, FilterBitsetCacheConfig.DEFAULT); + public CuVS2510GPUSearchCodec(GPUSearchParams params) { + this(params, FilterBitsetCacheConfig.DEFAULT); } /** @@ -63,11 +52,13 @@ public CuVS2510GPUSearchCodec(GPUSearchParams params) throws Exception { * * @param params GPU index and search parameters * @param filterCacheConfig filter-bitset-cache configuration - * @throws Exception Exception raised when initializing the codec */ - public CuVS2510GPUSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) - throws Exception { - this(NAME, LuceneProvider.getCodec("101"), params, filterCacheConfig); + public CuVS2510GPUSearchCodec(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + super( + NAME, + 101, + () -> LuceneCompat.lucene101Codec(), + () -> new CuVS2510GPUVectorsFormat(params, filterCacheConfig)); } /** @@ -83,43 +74,6 @@ public CuVS2510GPUSearchCodec( Codec delegate, GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { - super(name, delegate); - initializeFormat(params, filterCacheConfig); - } - - /** - * Initialize the {@link CuVS2510GPUVectorsFormat} instance using {@link GPUSearchParams}. - * - * @param params an instance of {@link GPUSearchParams} - * @param filterCacheConfig filter-bitset-cache configuration - */ - private void initializeFormat(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { - try { - format = new CuVS2510GPUVectorsFormat(params, filterCacheConfig); - setKnnFormat(format); - } catch (LibraryException ex) { - log.log( - Level.SEVERE, - "Couldn't load native library, possible classloader issue. " + ex.getMessage()); - } - } - - /** - * Get the configured {@link KnnVectorsFormat}. - * - * @return the instance of the {@link KnnVectorsFormat} - */ - @Override - public KnnVectorsFormat knnVectorsFormat() { - return format; - } - - /** - * Set the {@link KnnVectorsFormat}. - * - * @param format the {@link KnnVectorsFormat} to set - */ - public void setKnnFormat(KnnVectorsFormat format) { - this.format = format; + super(name, delegate, () -> new CuVS2510GPUVectorsFormat(params, filterCacheConfig)); } } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java index 3e62bd71b5..d0a9ca1119 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVS2510GPUVectorsFormat.java @@ -13,6 +13,7 @@ import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.DefaultFlatVectorScorer; import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat; import org.apache.lucene.index.SegmentReadState; import org.apache.lucene.index.SegmentWriteState; @@ -25,8 +26,8 @@ public class CuVS2510GPUVectorsFormat extends KnnVectorsFormat { private static final int MAX_DIMENSIONS = 4096; - private static final LuceneProvider LUCENE_PROVIDER; - private static final FlatVectorsFormat FLAT_VECTORS_FORMAT; + private static final FlatVectorsFormat FLAT_VECTORS_FORMAT = + new Lucene99FlatVectorsFormat(DefaultFlatVectorScorer.INSTANCE); public static final String CUVS_META_CODEC_NAME = "Lucene102CuVSVectorsFormatMeta"; public static final String CUVS_META_CODEC_EXT = "vemc"; @@ -38,16 +39,6 @@ public class CuVS2510GPUVectorsFormat extends KnnVectorsFormat { private final GPUSearchParams gpuSearchParams; private final FilterBitsetCache filterBitsetCache; - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - FLAT_VECTORS_FORMAT = - LUCENE_PROVIDER.getLuceneFlatVectorsFormatInstance(DefaultFlatVectorScorer.INSTANCE); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } - /** * Initializes the {@link CuVS2510GPUVectorsFormat} with default parameter values. * @@ -86,6 +77,7 @@ public CuVS2510GPUVectorsFormat( */ @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + LuceneVersionGuard.ensureCompatible(); assertIsSupported(); var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state); return new CuVS2510GPUVectorsWriter(state, gpuSearchParams, flatWriter); @@ -96,6 +88,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException */ @Override public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException { + LuceneVersionGuard.ensureCompatible(); assertIsSupported(); return new CuVS2510GPUVectorsReader( state, FLAT_VECTORS_FORMAT.fieldsReader(state), filterBitsetCache); 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 05e26c261c..fbd052a9ea 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 @@ -28,8 +28,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apache.lucene.codecs.CodecUtil; -import org.apache.lucene.codecs.KnnVectorsReader; import org.apache.lucene.codecs.hnsw.FlatVectorsReader; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; import org.apache.lucene.index.ByteVectorValues; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.FieldInfo; @@ -46,7 +46,6 @@ import org.apache.lucene.store.IOContext; import org.apache.lucene.store.IOContext.Context; import org.apache.lucene.store.IndexInput; -import org.apache.lucene.store.ReadAdvice; import org.apache.lucene.util.Bits; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.hnsw.IntToIntFunction; @@ -56,10 +55,10 @@ * * @since 25.10 */ -public class CuVS2510GPUVectorsReader extends KnnVectorsReader { +public class CuVS2510GPUVectorsReader extends CompatKnnVectorsReader { - private static final LuceneProvider LUCENE_PROVIDER; - private static final List VECTOR_SIMILARITY_FUNCTIONS; + private static final List VECTOR_SIMILARITY_FUNCTIONS = + Lucene99HnswVectorsReader.SIMILARITY_FUNCTIONS; private final FlatVectorsReader flatVectorsReader; private final FieldInfos fieldInfos; @@ -68,15 +67,6 @@ public class CuVS2510GPUVectorsReader extends KnnVectorsReader { private final IndexInput cuvsIndexInput; private final FilterBitsetCache filterBitsetCache; - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - VECTOR_SIMILARITY_FUNCTIONS = LUCENE_PROVIDER.getSimilarityFunctions(); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } - /** * Initializes the {@link CuVS2510GPUVectorsReader}, checks and loads the index. * @@ -101,6 +91,7 @@ public CuVS2510GPUVectorsReader(SegmentReadState state, FlatVectorsReader flatRe CuVS2510GPUVectorsReader( SegmentReadState state, FlatVectorsReader flatReader, FilterBitsetCache filterBitsetCache) throws IOException { + super(flatReader); this.flatVectorsReader = flatReader; this.filterBitsetCache = filterBitsetCache; this.fieldInfos = state.fieldInfos; @@ -127,7 +118,7 @@ public CuVS2510GPUVectorsReader(SegmentReadState state, FlatVectorsReader flatRe } finally { CodecUtil.checkFooter(meta, priorException); } - var ioContext = state.context.withReadAdvice(ReadAdvice.SEQUENTIAL); + var ioContext = LuceneCompat.sequentialReadContext(state.context); cuvsIndexInput = openCuVSInput(state, versionMeta, ioContext); /* * Only load indexes on the GPU when this reader is opening for searches. @@ -454,7 +445,7 @@ private static FloatToFloatFunction getScoreNormalizationFunc(VectorSimilarityFu * Returns the k nearest neighbor documents using cuVS's CAGRA or brute force algorithm for this field, to the given vector. */ @Override - public void search(String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) + protected void doSearch(String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException { var fieldEntry = getFieldEntry(field, VectorEncoding.FLOAT32); if (fieldEntry.count() == 0 || knnCollector.k() == 0) { @@ -608,7 +599,7 @@ public void search(String field, float[] target, KnnCollector knnCollector, Bits * This is not supported. */ @Override - public void search(String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) + protected void doSearch(String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException { throw new UnsupportedOperationException("Byte vectors are not currently supported"); } 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..9accee085b 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 @@ -35,6 +35,7 @@ import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatFieldVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat; import org.apache.lucene.index.DocsWithFieldSet; import org.apache.lucene.index.FieldInfo; @@ -56,13 +57,13 @@ * * @since 25.10 */ -public class CuVS2510GPUVectorsWriter extends KnnVectorsWriter { +public class CuVS2510GPUVectorsWriter extends CompatKnnVectorsWriter { private static final long SHALLOW_RAM_BYTES_USED = shallowSizeOfInstance(CuVS2510GPUVectorsWriter.class); private static final String COMPONENT = "CuVS2510GPUVectorsWriter"; - private static final LuceneProvider LUCENE_PROVIDER; - private static final List VECTOR_SIMILARITY_FUNCTIONS; + private static final List VECTOR_SIMILARITY_FUNCTIONS = + Lucene99HnswVectorsReader.SIMILARITY_FUNCTIONS; private static final int MIN_CAGRA_INDEX_SIZE = 2; private final GPUSearchParams gpuSearchParams; @@ -72,15 +73,6 @@ public class CuVS2510GPUVectorsWriter extends KnnVectorsWriter { private IndexOutput meta = null, cuvsIndex = null; private boolean finished; - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - VECTOR_SIMILARITY_FUNCTIONS = LUCENE_PROVIDER.getSimilarityFunctions(); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } - /** * The cuVS index Types. */ @@ -684,7 +676,7 @@ private void vectorBasedMerge(FieldInfo fieldInfo, MergeState mergeState) throws * Write field for merging. */ @Override - public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { flatVectorsWriter.mergeOneField(fieldInfo, mergeState); if (mergeCagraIndexes(fieldInfo, mergeState) == false) { vectorBasedMerge(fieldInfo, mergeState); diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java new file mode 100644 index 0000000000..33f831d592 --- /dev/null +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSCodecs.java @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; + +/** + * Creates the cuvs-lucene codecs and vectors formats that write indexes on the Lucene release this + * artifact is built for. + * + *

Each cuvs-lucene codec wraps the default codec of one Lucene release, so the codec to write + * with changes along with Lucene: {@code Lucene101AcceleratedHNSWCodec} on Lucene 10.2, {@code + * Lucene103AcceleratedHNSWCodec} on 10.3, {@code Lucene104AcceleratedHNSWCodec} on 10.4 and later. + * The older codecs stay available for reading existing indexes. Creating codecs through this class + * instead of naming them keeps application code unchanged when it moves to another Lucene release + * and the matching cuvs-lucene artifact. + * + *

Every method throws {@link IllegalStateException} if the Lucene release in use is not the one + * this cuvs-lucene artifact is built for. + * + * @since 26.12 + */ +public final class CuVSCodecs { + + private CuVSCodecs() {} + + /** + * Checks that the Lucene release in use is the one this cuvs-lucene artifact is built for. + * + *

A cuvs-lucene artifact on the wrong Lucene release does not break Lucene: Lucene keeps + * working, and so do indexes that do not use the cuvs-lucene codecs, while using one of these + * codecs fails. Applications that would rather not start at all in that case can call this method + * during startup. + * + * @throws IllegalStateException if the artifact is not built for the Lucene release in use; the + * message names the artifact to use instead + */ + public static void checkLuceneVersion() { + LuceneVersionGuard.ensureCompatible(); + } + + /** + * Returns the codec that builds HNSW graphs on the GPU and searches them on the CPU, with default + * parameters. + * + * @return the codec + */ + public static Codec acceleratedHNSW() { + return acceleratedHNSW(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Returns the codec that builds HNSW graphs on the GPU and searches them on the CPU. + * + * @param params the index build parameters + * @return the codec + */ + public static Codec acceleratedHNSW(AcceleratedHNSWParams params) { + LuceneVersionGuard.ensureCompatible(); + return LuceneCompat.acceleratedHNSWCodec(params); + } + + /** + * Returns the codec that builds HNSW graphs on the GPU over scalar-quantized vectors, with + * default parameters. + * + * @return the codec + */ + public static Codec acceleratedHNSWScalarQuantized() { + return acceleratedHNSWScalarQuantized(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Returns the codec that builds HNSW graphs on the GPU over scalar-quantized vectors. + * + * @param params the index build parameters + * @return the codec + */ + public static Codec acceleratedHNSWScalarQuantized(AcceleratedHNSWParams params) { + LuceneVersionGuard.ensureCompatible(); + return LuceneCompat.acceleratedHNSWScalarQuantizedCodec(params); + } + + /** + * Returns the vectors format used by {@link + * #acceleratedHNSWScalarQuantized(AcceleratedHNSWParams)}, for use with a per-field codec. + * + * @param params the index build parameters + * @return the vectors format + */ + public static KnnVectorsFormat acceleratedHNSWScalarQuantizedFormat( + AcceleratedHNSWParams params) { + LuceneVersionGuard.ensureCompatible(); + return LuceneCompat.acceleratedHNSWScalarQuantizedFormat(params); + } + + /** + * Returns the codec that builds HNSW graphs on the GPU over binary-quantized vectors, with + * default parameters. + * + * @return the codec + */ + public static Codec acceleratedHNSWBinaryQuantized() { + return acceleratedHNSWBinaryQuantized(new AcceleratedHNSWParams.Builder().build()); + } + + /** + * Returns the codec that builds HNSW graphs on the GPU over binary-quantized vectors. + * + * @param params the index build parameters + * @return the codec + */ + public static Codec acceleratedHNSWBinaryQuantized(AcceleratedHNSWParams params) { + LuceneVersionGuard.ensureCompatible(); + return LuceneCompat.acceleratedHNSWBinaryQuantizedCodec(params); + } + + /** + * Returns the codec that both builds and searches CAGRA indexes on the GPU, with default + * parameters. + * + * @return the codec + */ + public static Codec gpuSearch() { + return gpuSearch(new GPUSearchParams.Builder().build()); + } + + /** + * Returns the codec that both builds and searches CAGRA indexes on the GPU, with the default + * filter-bitset-cache configuration. + * + * @param params the GPU index and search parameters + * @return the codec + */ + public static Codec gpuSearch(GPUSearchParams params) { + return gpuSearch(params, FilterBitsetCacheConfig.DEFAULT); + } + + /** + * Returns the codec that both builds and searches CAGRA indexes on the GPU. + * + * @param params the GPU index and search parameters + * @param filterCacheConfig the filter-bitset-cache configuration + * @return the codec + */ + public static Codec gpuSearch(GPUSearchParams params, FilterBitsetCacheConfig filterCacheConfig) { + LuceneVersionGuard.ensureCompatible(); + return LuceneCompat.gpuSearchCodec(params, filterCacheConfig); + } +} diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSFilterCodec.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSFilterCodec.java new file mode 100644 index 0000000000..40b717ae5c --- /dev/null +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/CuVSFilterCodec.java @@ -0,0 +1,270 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import com.nvidia.cuvs.LibraryException; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.CompoundFormat; +import org.apache.lucene.codecs.DocValuesFormat; +import org.apache.lucene.codecs.FieldInfosFormat; +import org.apache.lucene.codecs.FilterCodec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.codecs.LiveDocsFormat; +import org.apache.lucene.codecs.NormsFormat; +import org.apache.lucene.codecs.PointsFormat; +import org.apache.lucene.codecs.PostingsFormat; +import org.apache.lucene.codecs.SegmentInfoFormat; +import org.apache.lucene.codecs.StoredFieldsFormat; +import org.apache.lucene.codecs.TermVectorsFormat; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.index.SegmentWriteState; +import org.apache.lucene.util.Version; + +/** + * Base class of the cuvs-lucene codecs: a {@link FilterCodec} that replaces its delegate's + * {@link KnnVectorsFormat}. + * + *

Each cuvs-lucene codec wraps the default codec of one Lucene release, for example + * {@code Lucene101Codec} for Lucene 10.2; that codec's version number is the cuvs-lucene codec's + * generation. Once a newer Lucene release replaces that default codec, Lucene moves the + * old one to its backward codecs, which can read but not write. A cuvs-lucene codec of that + * generation is kept so that existing indexes stay readable, and like the codec it wraps it refuses + * to write: only the generation {@code LuceneCompat.CURRENT_CODEC_GENERATION} names writes. Use + * {@link CuVSCodecs} to get the codecs that can write on the Lucene release in use. + */ +abstract class CuVSFilterCodec extends FilterCodec { + + private static final Logger log = Logger.getLogger(CuVSFilterCodec.class.getName()); + // Every registered codec runs into the same problem, so it is logged only once. + private static final AtomicBoolean unavailableLogged = new AtomicBoolean(); + + private final boolean readOnly; + private KnnVectorsFormat format; + + /** + * Creates one of cuvs-lucene's own codecs, which wraps the default codec of a Lucene release and + * can only write on the Lucene release where that codec is still the default. + * + * @param name the codec's name + * @param generation the version number of the Lucene codec it wraps, such as 101 for + * {@code Lucene101Codec} + * @param delegate creates that Lucene codec, as a lambda; see {@link #delegate(String, Supplier)} + * @param format creates the {@link KnnVectorsFormat} to use for vectors + */ + protected CuVSFilterCodec( + String name, int generation, Supplier delegate, Supplier format) { + this( + name, + delegate(name, delegate), + generation != LuceneCompat.CURRENT_CODEC_GENERATION, + format); + } + + /** + * Creates a codec with a delegate chosen by the caller, which is never made read-only: whether + * the delegate can write is the caller's responsibility. + * + * @param name the codec's name + * @param delegate the codec to use for everything but vectors + * @param format creates the {@link KnnVectorsFormat} to use for vectors + */ + protected CuVSFilterCodec(String name, Codec delegate, Supplier format) { + this(name, delegate, false, format); + } + + private CuVSFilterCodec( + String name, Codec delegate, boolean readOnly, Supplier format) { + super(name, delegate); + this.readOnly = readOnly; + try { + setKnnFormat(format.get()); + } catch (LibraryException ex) { + log.log( + Level.SEVERE, + "Couldn't load native library, possible classloader issue. " + ex.getMessage()); + } + } + + /** + * Returns the Lucene codec a cuvs-lucene codec delegates to, or, if it cannot be created on the + * Lucene release in use, a codec that fails with an explanation when it is used. + * + *

Lucene's service loader creates every registered codec when it first looks one up, and an + * exception there breaks every codec lookup in the JVM, Lucene's own included. A cuvs-lucene + * artifact running on another Lucene release than it was built for must therefore still create its + * codecs, and only fail once they are used. + * + *

Pass {@code delegate} as a lambda, such as {@code () -> LuceneCompat.lucene101Codec()}, not + * as a method reference: the JVM resolves a method reference, and so loads {@code LuceneCompat}, + * before this method runs, which is exactly what can fail on the wrong Lucene release. + * + * @param name the name of the cuvs-lucene codec, for the error message + * @param delegate creates the Lucene codec to delegate to + *

The first time a codec is unavailable, the problem is logged at {@code SEVERE}, so that it + * shows up when Lucene first looks up its codecs, typically at startup, even though it only fails + * once the codec is used. Applications that would rather not start can call {@link + * CuVSCodecs#checkLuceneVersion()}. + * + * @return the Lucene codec, or a codec that throws {@link IllegalStateException} when used + */ + static Codec delegate(String name, Supplier delegate) { + String problem = LuceneVersionGuard.mismatchMessage(Version.LATEST); + if (problem == null) { + try { + return delegate.get(); + } catch (LinkageError e) { + problem = "The Lucene codec " + name + " delegates to is not available: " + e; + } + } + if (unavailableLogged.compareAndSet(false, true)) { + log.log( + Level.SEVERE, + "The cuvs-lucene codecs cannot be used, starting with " + + name + + ": " + + problem + + " Lucene and indexes that do not use these codecs are not affected; reading or" + + " writing an index that does fails."); + } + return new UnavailableCodec(name, problem); + } + + /** + * Get the configured {@link KnnVectorsFormat}. + * + * @return the instance of the {@link KnnVectorsFormat} + */ + @Override + public KnnVectorsFormat knnVectorsFormat() { + return format; + } + + /** + * Set the {@link KnnVectorsFormat}. + * + * @param format the {@link KnnVectorsFormat} to set + */ + public void setKnnFormat(KnnVectorsFormat format) { + // A format already made read-only, such as one read back from knnVectorsFormat(), is kept as + // is. + boolean wrap = readOnly && format != null && format instanceof ReadOnlyFormat == false; + this.format = wrap ? new ReadOnlyFormat(getName(), format) : format; + } + + /** Stands in for a delegate codec that cannot be created, and throws when any format is used. */ + private static final class UnavailableCodec extends Codec { + + private final String problem; + + UnavailableCodec(String name, String problem) { + super(name); + this.problem = problem; + } + + private IllegalStateException unavailable() { + return new IllegalStateException(getName() + " cannot be used: " + problem); + } + + @Override + public PostingsFormat postingsFormat() { + throw unavailable(); + } + + @Override + public DocValuesFormat docValuesFormat() { + throw unavailable(); + } + + @Override + public StoredFieldsFormat storedFieldsFormat() { + throw unavailable(); + } + + @Override + public TermVectorsFormat termVectorsFormat() { + throw unavailable(); + } + + @Override + public FieldInfosFormat fieldInfosFormat() { + throw unavailable(); + } + + @Override + public SegmentInfoFormat segmentInfoFormat() { + throw unavailable(); + } + + @Override + public NormsFormat normsFormat() { + throw unavailable(); + } + + @Override + public LiveDocsFormat liveDocsFormat() { + throw unavailable(); + } + + @Override + public CompoundFormat compoundFormat() { + throw unavailable(); + } + + @Override + public PointsFormat pointsFormat() { + throw unavailable(); + } + + @Override + public KnnVectorsFormat knnVectorsFormat() { + throw unavailable(); + } + } + + /** Reads through the wrapped format, and refuses to write. */ + private static final class ReadOnlyFormat extends KnnVectorsFormat { + + private final String codecName; + private final KnnVectorsFormat in; + + ReadOnlyFormat(String codecName, KnnVectorsFormat in) { + super(in.getName()); + this.codecName = codecName; + this.in = in; + } + + @Override + public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + throw new UnsupportedOperationException( + "The " + + codecName + + " codec can only read indexes on Lucene " + + Version.LATEST + + ". Use the codecs from CuVSCodecs to write."); + } + + @Override + public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException { + return in.fieldsReader(state); + } + + @Override + public int getMaxDimensions(String fieldName) { + return in.getMaxDimensions(fieldName); + } + + @Override + public String toString() { + return "ReadOnly(" + in + ")"; + } + } +} diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java index c29a43b16e..b532644ca4 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java @@ -32,6 +32,7 @@ import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.KnnCollector; import org.apache.lucene.search.KnnFloatVectorQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; @@ -40,9 +41,7 @@ import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.ScorerSupplier; -import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.Weight; -import org.apache.lucene.search.knn.KnnCollectorManager; import org.apache.lucene.util.Bits; import org.apache.lucene.util.FixedBitSet; @@ -79,7 +78,7 @@ * * @since 25.10 */ -public class GPUKnnFloatVectorQuery extends KnnFloatVectorQuery { +public class GPUKnnFloatVectorQuery extends CompatKnnFloatVectorQuery { private final int iTopK; private final int searchWidth; @@ -309,17 +308,9 @@ public Query rewrite(IndexSearcher indexSearcher) throws IOException { // ------------------------------------------------------------------------- @Override - protected TopDocs approximateSearch( - LeafReaderContext context, - Bits acceptDocs, - int visitedLimit, - KnnCollectorManager knnCollectorManager) - throws IOException { - GPUPerLeafCuVSKnnCollector results = - new GPUPerLeafCuVSKnnCollector( - k, visitedLimit, iTopK, searchWidth, threadBlockSize, maxIterations, searchAlgo); - context.reader().searchNearestVectors(field, getTargetCopy(), results, acceptDocs); - return results.topDocs(); + KnnCollector newPerLeafCollector(int visitedLimit) { + return new GPUPerLeafCuVSKnnCollector( + k, visitedLimit, iTopK, searchWidth, threadBlockSize, maxIterations, searchAlgo); } // ------------------------------------------------------------------------- diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java index e9f4d6fead..1340cf5c3f 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene101AcceleratedHNSWCodec.java @@ -4,33 +4,27 @@ */ package com.nvidia.cuvs.lucene; -import com.nvidia.cuvs.LibraryException; -import java.util.logging.Level; -import java.util.logging.Logger; import org.apache.lucene.codecs.Codec; -import org.apache.lucene.codecs.FilterCodec; -import org.apache.lucene.codecs.KnnVectorsFormat; /** * A codec that enables GPU-based accelerated HNSW capability and can be used * to accelerated indexing using GPUs and search using CPUs. Fallbacks to CPU * based indexing when used on a machine without a GPU and/or cuVS. * + *

This codec wraps {@code Lucene101Codec}, the default codec of Lucene 10.2. On later Lucene + * releases it can only read existing indexes; use {@link CuVSCodecs#acceleratedHNSW} to write. + * * @since 25.10 */ -public class Lucene101AcceleratedHNSWCodec extends FilterCodec { +public class Lucene101AcceleratedHNSWCodec extends CuVSFilterCodec { - private static final Logger log = Logger.getLogger(Lucene101AcceleratedHNSWCodec.class.getName()); private static final String NAME = "Lucene101AcceleratedHNSWCodec"; - private KnnVectorsFormat format; /** * Default constructor for {@link Lucene101AcceleratedHNSWCodec}. - * - * @throws Exception */ - public Lucene101AcceleratedHNSWCodec() throws Exception { - this(NAME, LuceneProvider.getCodec("101")); + public Lucene101AcceleratedHNSWCodec() { + this(new AcceleratedHNSWParams.Builder().build()); } /** @@ -40,62 +34,23 @@ public Lucene101AcceleratedHNSWCodec() throws Exception { * @param delegate the delegate codec to filter */ public Lucene101AcceleratedHNSWCodec(String name, Codec delegate) { - super(name, delegate); - initializeFormatDefaultValues(); + super( + name, + delegate, + () -> + new Lucene99AcceleratedHNSWVectorsFormat(new AcceleratedHNSWParams.Builder().build())); } /** * Constructor for {@link Lucene101AcceleratedHNSWCodec}. * * @param acceleratedHNSWParams instance of {@link AcceleratedHNSWParams} - * @throws Exception exception - */ - public Lucene101AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) - throws Exception { - this(NAME, LuceneProvider.getCodec("101")); - initializeFormat(acceleratedHNSWParams); - } - - /** - * Initialize an instance of {@link Lucene99AcceleratedHNSWVectorsFormat} - * with an instance of {@link AcceleratedHNSWParams} with default parameter values. - */ - private void initializeFormatDefaultValues() { - initializeFormat(new AcceleratedHNSWParams.Builder().build()); - } - - /** - * Initialize an instance of {@link Lucene99AcceleratedHNSWVectorsFormat}. - * - * @param acceleratedHNSWParams instance of {@link AcceleratedHNSWParams} to use - */ - private void initializeFormat(AcceleratedHNSWParams acceleratedHNSWParams) { - try { - format = new Lucene99AcceleratedHNSWVectorsFormat(acceleratedHNSWParams); - setKnnFormat(format); - } catch (LibraryException ex) { - log.log( - Level.SEVERE, - "Couldn't load native library, possible classloader issue. " + ex.getMessage()); - } - } - - /** - * Get the configured {@link KnnVectorsFormat}. - * - * @return the instance of the {@link KnnVectorsFormat} - */ - @Override - public KnnVectorsFormat knnVectorsFormat() { - return format; - } - - /** - * Set the {@link KnnVectorsFormat}. - * - * @param format the {@link KnnVectorsFormat} to set */ - public void setKnnFormat(KnnVectorsFormat format) { - this.format = format; + public Lucene101AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 101, + () -> LuceneCompat.lucene101Codec(), + () -> new Lucene99AcceleratedHNSWVectorsFormat(acceleratedHNSWParams)); } } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java index 1b1bc3cbd2..fed4bf56c5 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java @@ -15,9 +15,10 @@ import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.DefaultFlatVectorScorer; import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; import org.apache.lucene.index.SegmentReadState; import org.apache.lucene.index.SegmentWriteState; -import org.apache.lucene.search.TaskExecutor; /** * cuVS based KnnVectorsFormat for indexing on GPU and searching on the CPU. @@ -28,7 +29,8 @@ public class Lucene99AcceleratedHNSWVectorsFormat extends KnnVectorsFormat { private static final Logger log = Logger.getLogger(Lucene99AcceleratedHNSWVectorsFormat.class.getName()); - private static final FlatVectorsFormat FLAT_VECTORS_FORMAT; + private static final FlatVectorsFormat FLAT_VECTORS_FORMAT = + new Lucene99FlatVectorsFormat(DefaultFlatVectorScorer.INSTANCE); private static final int MAX_DIMENSIONS = 4096; private final AcceleratedHNSWParams acceleratedHNSWParams; @@ -36,17 +38,6 @@ public class Lucene99AcceleratedHNSWVectorsFormat extends KnnVectorsFormat { static final String HNSW_META_CODEC_EXT = "vem"; static final String HNSW_INDEX_CODEC_NAME = "Lucene99HnswVectorsFormatIndex"; static final String HNSW_INDEX_EXT = "vex"; - static final LuceneProvider LUCENE_PROVIDER; - - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - FLAT_VECTORS_FORMAT = - LUCENE_PROVIDER.getLuceneFlatVectorsFormatInstance(DefaultFlatVectorScorer.INSTANCE); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } /** * Initializes {@link Lucene99AcceleratedHNSWVectorsFormat} with an instance @@ -74,25 +65,16 @@ public Lucene99AcceleratedHNSWVectorsFormat(AcceleratedHNSWParams acceleratedHNS */ @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { - var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state); + LuceneVersionGuard.ensureCompatible(); if (isSupported()) { log.log(Level.FINE, "cuVS is supported so using the Lucene99AcceleratedHNSWVectorsWriter"); - return new Lucene99AcceleratedHNSWVectorsWriter(state, acceleratedHNSWParams, flatWriter); + return new Lucene99AcceleratedHNSWVectorsWriter( + state, acceleratedHNSWParams, FLAT_VECTORS_FORMAT.fieldsWriter(state)); } else { log.log( Level.WARNING, "GPU based indexing not supported, falling back to using the Lucene99HnswVectorsWriter"); - try { - return LUCENE_PROVIDER.getLuceneHnswVectorsWriterInstance( - state, - acceleratedHNSWParams.getMaxConn(), - acceleratedHNSWParams.getBeamWidth(), - flatWriter, - acceleratedHNSWParams.getNumMergeWorkers(), - new TaskExecutor(acceleratedHNSWParams.getMergeExec())); - } catch (Exception e) { - throw Utils.handleThrowable(e); - } + return Utils.cpuHnswFormat(acceleratedHNSWParams).fieldsWriter(state); } } @@ -101,12 +83,8 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException */ @Override public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException { - try { - return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( - state, FLAT_VECTORS_FORMAT.fieldsReader(state)); - } catch (Exception e) { - throw Utils.handleThrowable(e); - } + LuceneVersionGuard.ensureCompatible(); + return new Lucene99HnswVectorsReader(state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } /** 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 13edc64975..7e6a45485f 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 @@ -32,6 +32,7 @@ import org.apache.lucene.codecs.KnnFieldVectorsWriter; import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat; import org.apache.lucene.index.DocsWithFieldSet; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.IndexFileNames; @@ -49,13 +50,15 @@ * * @since 25.10 */ -public class Lucene99AcceleratedHNSWVectorsWriter extends KnnVectorsWriter { +public class Lucene99AcceleratedHNSWVectorsWriter extends CompatKnnVectorsWriter { private static final long SHALLOW_RAM_BYTES_USED = shallowSizeOfInstance(Lucene99AcceleratedHNSWVectorsWriter.class); private static final String COMPONENT = "Lucene99AcceleratedHNSWVectorsWriter"; - private static final LuceneProvider LUCENE_PROVIDER; - private static final Integer VERSION_CURRENT; + // The graph is written with plain vInt-encoded neighbor lists, the encoding of + // Lucene99HnswVectorsFormat.VERSION_START. Lucene 10.3 added a group-varint encoding and made it + // VERSION_CURRENT, so the version has to be pinned to the encoding actually written. + private static final int VERSION = Lucene99HnswVectorsFormat.VERSION_START; private final AcceleratedHNSWParams acceleratedHNSWParams; private final FlatVectorsWriter flatVectorsWriter; @@ -67,15 +70,6 @@ public class Lucene99AcceleratedHNSWVectorsWriter extends KnnVectorsWriter { private String vexFileName; private boolean finished; - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - VERSION_CURRENT = LUCENE_PROVIDER.getStaticIntParam("VERSION_CURRENT"); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } - /** * Initializes {@link Lucene99AcceleratedHNSWVectorsWriter} * @@ -103,15 +97,11 @@ public Lucene99AcceleratedHNSWVectorsWriter( hnswMeta = state.directory.createOutput(vemFileName, state.context); hnswVectorIndex = state.directory.createOutput(vexFileName, state.context); CodecUtil.writeIndexHeader( - hnswMeta, - HNSW_META_CODEC_NAME, - VERSION_CURRENT, - state.segmentInfo.getId(), - state.segmentSuffix); + hnswMeta, HNSW_META_CODEC_NAME, VERSION, state.segmentInfo.getId(), state.segmentSuffix); CodecUtil.writeIndexHeader( hnswVectorIndex, HNSW_INDEX_CODEC_NAME, - VERSION_CURRENT, + VERSION, state.segmentInfo.getId(), state.segmentSuffix); success = true; @@ -290,7 +280,7 @@ private void vectorBasedMerge(FieldInfo fieldInfo, MergeState mergeState) throws * Write field for merging. */ @Override - public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { flatVectorsWriter.mergeOneField(fieldInfo, mergeState); vectorBasedMerge(fieldInfo, mergeState); } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedCodec.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedCodec.java index 0b1653bc14..218f88a17c 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedCodec.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedCodec.java @@ -4,58 +4,39 @@ */ package com.nvidia.cuvs.lucene; -import com.nvidia.cuvs.LibraryException; -import java.util.logging.Level; -import java.util.logging.Logger; import org.apache.lucene.codecs.Codec; -import org.apache.lucene.codecs.FilterCodec; -import org.apache.lucene.codecs.KnnVectorsFormat; /** * CuVS based codec for GPU based vector search * + *

This codec, named {@code Lucene101AcceleratedHNSWBinaryQuantizedCodec}, wraps {@code + * Lucene101Codec}, the default codec of Lucene 10.2. On later Lucene releases it can only read + * existing indexes; use {@link CuVSCodecs#acceleratedHNSWBinaryQuantized} to write. + * * @since 26.02 */ -public class LuceneAcceleratedHNSWBinaryQuantizedCodec extends FilterCodec { +public class LuceneAcceleratedHNSWBinaryQuantizedCodec extends CuVSFilterCodec { - private static final Logger log = - Logger.getLogger(LuceneAcceleratedHNSWBinaryQuantizedCodec.class.getName()); private static final String NAME = "Lucene101AcceleratedHNSWBinaryQuantizedCodec"; - private KnnVectorsFormat format; - - public LuceneAcceleratedHNSWBinaryQuantizedCodec() throws Exception { - this(NAME, LuceneProvider.getCodec("101")); + public LuceneAcceleratedHNSWBinaryQuantizedCodec() { + this(new AcceleratedHNSWParams.Builder().build()); } public LuceneAcceleratedHNSWBinaryQuantizedCodec(String name, Codec delegate) { - super(name, delegate); - initializeFormat(new AcceleratedHNSWParams.Builder().build()); - } - - public LuceneAcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) - throws Exception { - this(NAME, LuceneProvider.getCodec("101")); - initializeFormat(acceleratedHNSWParams); - } - - private void initializeFormat(AcceleratedHNSWParams acceleratedHNSWParams) { - try { - format = new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat(acceleratedHNSWParams); - setKnnFormat(format); - } catch (LibraryException ex) { - log.log( - Level.SEVERE, - "Couldn't load native library, possible classloader issue. " + ex.getMessage()); - } - } - - @Override - public KnnVectorsFormat knnVectorsFormat() { - return format; + super( + name, + delegate, + () -> + new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat( + new AcceleratedHNSWParams.Builder().build())); } - public void setKnnFormat(KnnVectorsFormat format) { - this.format = format; + public LuceneAcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 101, + () -> LuceneCompat.lucene101Codec(), + () -> new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat(acceleratedHNSWParams)); } } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java index 40a818683d..124ac7d991 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java @@ -15,36 +15,30 @@ import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.DefaultFlatVectorScorer; import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; import org.apache.lucene.index.SegmentReadState; import org.apache.lucene.index.SegmentWriteState; /** * cuVS based Binary Quantized KnnVectorsFormat for indexing on GPU and searching on the CPU. * + *

The vectors are binary quantized only to build the graph on the GPU. The index stores them in + * full precision, with Lucene's {@code Lucene99FlatVectorsFormat}, and search scores them in full + * precision too. + * * @since 26.02 */ public class LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat extends KnnVectorsFormat { private static final Logger log = Logger.getLogger(LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.class.getName()); - private static final LuceneProvider LUCENE102_PROVIDER; - private static final LuceneProvider LUCENE99_PROVIDER; - private static final FlatVectorsFormat FLAT_VECTORS_FORMAT; + private static final FlatVectorsFormat FLAT_VECTORS_FORMAT = + new Lucene99FlatVectorsFormat(DefaultFlatVectorScorer.INSTANCE); private static final int MAX_DIMENSIONS = 4096; private final AcceleratedHNSWParams acceleratedHNSWParams; - static { - try { - LUCENE99_PROVIDER = LuceneProvider.getInstance("99"); - LUCENE102_PROVIDER = LuceneProvider.getInstance("102"); - FLAT_VECTORS_FORMAT = - LUCENE102_PROVIDER.getLuceneFlatVectorsFormatInstance(DefaultFlatVectorScorer.INSTANCE); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } - /** * Initializes {@link LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat} with default values. * @@ -70,27 +64,20 @@ public LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat( */ @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { - var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state); + LuceneVersionGuard.ensureCompatible(); if (isSupported()) { log.log( Level.FINE, "cuVS is supported so using the Lucene99AcceleratedHNSWBinaryQuantizedVectorsWriter"); return new LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter( - state, acceleratedHNSWParams, flatWriter); + state, acceleratedHNSWParams, FLAT_VECTORS_FORMAT.fieldsWriter(state)); } else { - try { - // Fallback to Lucene's Lucene102HnswBinaryQuantizedVectorsFormat format - log.log( - Level.WARNING, - "GPU based indexing not supported, falling back to using the" - + " Lucene102HnswBinaryQuantizedVectorsFormat"); - KnnVectorsFormat fallbackFormat = - LUCENE102_PROVIDER.getLuceneHnswBinaryQuantizedVectorsFormatInstance( - acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth()); - return fallbackFormat.fieldsWriter(state); - } catch (Exception e) { - throw Utils.handleThrowable(e); - } + // The GPU writer quantizes only to build the graph and stores full-precision vectors, which + // is exactly what Lucene's plain HNSW format writes. + log.log( + Level.WARNING, + "GPU based indexing not supported, falling back to using the Lucene99HnswVectorsWriter"); + return Utils.cpuHnswFormat(acceleratedHNSWParams).fieldsWriter(state); } } @@ -99,12 +86,8 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException */ @Override public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException { - try { - return LUCENE99_PROVIDER.getLuceneHnswVectorsReaderInstance( - state, FLAT_VECTORS_FORMAT.fieldsReader(state)); - } catch (Exception e) { - throw Utils.handleThrowable(e); - } + LuceneVersionGuard.ensureCompatible(); + return new Lucene99HnswVectorsReader(state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } /** 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 87907d2cbb..5e8692a12e 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 @@ -54,11 +54,15 @@ * * @since 26.02 */ -public class LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter extends KnnVectorsWriter { +public class LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter extends CompatKnnVectorsWriter { private static final long SHALLOW_RAM_BYTES_USED = shallowSizeOfInstance(LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.class); private static final String COMPONENT = "Lucene99AcceleratedHNSWBinaryQuantizedVectorsWriter"; + // The graph is written with plain vInt-encoded neighbor lists, the encoding of + // Lucene99HnswVectorsFormat.VERSION_START. Lucene 10.3 added a group-varint encoding and made it + // VERSION_CURRENT, so the version has to be pinned to the encoding actually written. + private static final int VERSION = Lucene99HnswVectorsFormat.VERSION_START; private final FlatVectorsWriter flatVectorsWriter; private final List fields = new ArrayList<>(); @@ -100,15 +104,11 @@ public LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter( hnswVectorIndex = state.directory.createOutput(vexFileName, state.context); CodecUtil.writeIndexHeader( - hnswMeta, - HNSW_META_CODEC_NAME, - Lucene99HnswVectorsFormat.VERSION_CURRENT, - state.segmentInfo.getId(), - state.segmentSuffix); + hnswMeta, HNSW_META_CODEC_NAME, VERSION, state.segmentInfo.getId(), state.segmentSuffix); CodecUtil.writeIndexHeader( hnswVectorIndex, HNSW_INDEX_CODEC_NAME, - Lucene99HnswVectorsFormat.VERSION_CURRENT, + VERSION, state.segmentInfo.getId(), state.segmentSuffix); @@ -301,7 +301,7 @@ private void writeSingleVectorGraph(FieldInfo fieldInfo, List vectors) * Write field for merging. */ @Override - public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { flatVectorsWriter.mergeOneField(fieldInfo, mergeState); vectorBasedMerge(fieldInfo, mergeState); } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedCodec.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedCodec.java index 0705ed0a55..4b88660f4d 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedCodec.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedCodec.java @@ -4,62 +4,39 @@ */ package com.nvidia.cuvs.lucene; -import com.nvidia.cuvs.LibraryException; -import java.util.logging.Level; -import java.util.logging.Logger; import org.apache.lucene.codecs.Codec; -import org.apache.lucene.codecs.FilterCodec; -import org.apache.lucene.codecs.KnnVectorsFormat; /** * CuVS based codec for GPU based vector search * + *

This codec, named {@code Lucene101AcceleratedHNSWScalarQuantizedCodec}, wraps {@code + * Lucene101Codec}, the default codec of Lucene 10.2. On later Lucene releases it can only read + * existing indexes; use {@link CuVSCodecs#acceleratedHNSWScalarQuantized} to write. + * * @since 26.02 */ -public class LuceneAcceleratedHNSWScalarQuantizedCodec extends FilterCodec { +public class LuceneAcceleratedHNSWScalarQuantizedCodec extends CuVSFilterCodec { - private static final Logger log = - Logger.getLogger(LuceneAcceleratedHNSWScalarQuantizedCodec.class.getName()); private static final String NAME = "Lucene101AcceleratedHNSWScalarQuantizedCodec"; - private KnnVectorsFormat format; - - public LuceneAcceleratedHNSWScalarQuantizedCodec() throws Exception { - this(NAME, LuceneProvider.getCodec("101")); + public LuceneAcceleratedHNSWScalarQuantizedCodec() { + this(new AcceleratedHNSWParams.Builder().build()); } public LuceneAcceleratedHNSWScalarQuantizedCodec(String name, Codec delegate) { - super(name, delegate); - initializeFormatDefaultValues(); - } - - public LuceneAcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) - throws Exception { - this(NAME, LuceneProvider.getCodec("101")); - initializeFormat(acceleratedHNSWParams); - } - - private void initializeFormatDefaultValues() { - initializeFormat(new AcceleratedHNSWParams.Builder().build()); - } - - private void initializeFormat(AcceleratedHNSWParams acceleratedHNSWParams) { - try { - format = new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(acceleratedHNSWParams); - setKnnFormat(format); - } catch (LibraryException ex) { - log.log( - Level.SEVERE, - "Couldn't load native library, possible classloader issue. " + ex.getMessage()); - } - } - - @Override - public KnnVectorsFormat knnVectorsFormat() { - return format; + super( + name, + delegate, + () -> + new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat( + new AcceleratedHNSWParams.Builder().build())); } - public void setKnnFormat(KnnVectorsFormat format) { - this.format = format; + public LuceneAcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) { + super( + NAME, + 101, + () -> LuceneCompat.lucene101Codec(), + () -> new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(acceleratedHNSWParams)); } } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java index ead6daeaad..acb8f71e91 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java @@ -4,41 +4,19 @@ */ package com.nvidia.cuvs.lucene; -import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; - import com.nvidia.cuvs.LibraryException; -import java.io.IOException; -import java.util.logging.Logger; -import org.apache.lucene.codecs.KnnVectorsFormat; -import org.apache.lucene.codecs.KnnVectorsReader; -import org.apache.lucene.codecs.KnnVectorsWriter; -import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; -import org.apache.lucene.index.SegmentReadState; -import org.apache.lucene.index.SegmentWriteState; /** * cuVS based Scalar Quantized KnnVectorsFormat for indexing on GPU and searching on the CPU. * + *

Stores the vectors with Lucene's {@code Lucene99ScalarQuantizedVectorsFormat}, which Lucene + * 10.4 moved to its backward codecs. On Lucene 10.4 and later this format can only read existing + * indexes; use {@link CuVSCodecs#acceleratedHNSWScalarQuantizedFormat} to write. + * * @since 26.02 */ -public class LuceneAcceleratedHNSWScalarQuantizedVectorsFormat extends KnnVectorsFormat { - - private static final Logger log = - Logger.getLogger(LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.class.getName()); - private static final LuceneProvider LUCENE_PROVIDER; - private static final FlatVectorsFormat FLAT_VECTORS_FORMAT; - private static final int MAX_DIMENSIONS = 4096; - - private final AcceleratedHNSWParams acceleratedHNSWParams; - - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - FLAT_VECTORS_FORMAT = LUCENE_PROVIDER.getLuceneScalarQuantizedVectorsFormatInstance(); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } +public class LuceneAcceleratedHNSWScalarQuantizedVectorsFormat + extends BaseAcceleratedHNSWScalarQuantizedVectorsFormat { /** * Initializes {@link LuceneAcceleratedHNSWScalarQuantizedVectorsFormat} with default values. @@ -56,54 +34,10 @@ public LuceneAcceleratedHNSWScalarQuantizedVectorsFormat() { */ public LuceneAcceleratedHNSWScalarQuantizedVectorsFormat( AcceleratedHNSWParams acceleratedHNSWParams) { - super("Lucene99AcceleratedHNSWScalarQuantizedVectorsFormat"); - this.acceleratedHNSWParams = acceleratedHNSWParams; - } - - /** - * Returns a KnnVectorsWriter to write the scalar quantized vectors to the index. - */ - @Override - public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { - var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state); - if (isSupported()) { - log.info("cuVS is supported so using the Lucene99AcceleratedHNSWQuantizedVectorsWriter"); - return new LuceneAcceleratedHNSWScalarQuantizedVectorsWriter( - state, acceleratedHNSWParams, flatWriter); - } else { - try { - // Fallback to Lucene's Lucene99HnswScalarQuantizedVectorsFormat - log.warning( - "GPU based indexing not supported, falling back to using the" - + " Lucene99HnswScalarQuantizedVectorsFormat"); - KnnVectorsFormat fallbackFormat = - LUCENE_PROVIDER.getLuceneHnswScalarQuantizedVectorsFormatInstance( - acceleratedHNSWParams.getBeamWidth(), acceleratedHNSWParams.getMaxConn()); - return fallbackFormat.fieldsWriter(state); - } catch (Exception e) { - throw Utils.handleThrowable(e); - } - } - } - - /** - * Returns a KnnVectorsReader to read the scalar quantized vectors from the index. - */ - @Override - public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException { - try { - return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( - state, FLAT_VECTORS_FORMAT.fieldsReader(state)); - } catch (Exception e) { - throw Utils.handleThrowable(e); - } - } - - /** - * Returns the maximum number of vector dimensions supported by this Codec for the given field name. - */ - @Override - public int getMaxDimensions(String fieldName) { - return MAX_DIMENSIONS; + super( + "Lucene99AcceleratedHNSWScalarQuantizedVectorsFormat", + acceleratedHNSWParams, + () -> LuceneCompat.lucene99ScalarQuantizedFlatFormat(), + params -> LuceneCompat.lucene99HnswScalarQuantizedFormat(params)); } } 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 7141af56ee..e6e9eb5d5d 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 @@ -33,6 +33,7 @@ import org.apache.lucene.codecs.KnnFieldVectorsWriter; import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat; import org.apache.lucene.index.DocsWithFieldSet; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FloatVectorValues; @@ -53,13 +54,15 @@ * * @since 26.02 */ -public class LuceneAcceleratedHNSWScalarQuantizedVectorsWriter extends KnnVectorsWriter { +public class LuceneAcceleratedHNSWScalarQuantizedVectorsWriter extends CompatKnnVectorsWriter { private static final long SHALLOW_RAM_BYTES_USED = shallowSizeOfInstance(LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.class); private static final String COMPONENT = "Lucene99AcceleratedHNSWQuantizedVectorsWriter"; - private static final LuceneProvider LUCENE_PROVIDER; - private static final Integer VERSION_CURRENT; + // The graph is written with plain vInt-encoded neighbor lists, the encoding of + // Lucene99HnswVectorsFormat.VERSION_START. Lucene 10.3 added a group-varint encoding and made it + // VERSION_CURRENT, so the version has to be pinned to the encoding actually written. + private static final int VERSION = Lucene99HnswVectorsFormat.VERSION_START; private final FlatVectorsWriter flatVectorsWriter; private final List fields = new ArrayList<>(); @@ -70,15 +73,6 @@ public class LuceneAcceleratedHNSWScalarQuantizedVectorsWriter extends KnnVector private String vemFileName; private String vexFileName; - static { - try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - VERSION_CURRENT = LUCENE_PROVIDER.getStaticIntParam("VERSION_CURRENT"); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); - } - } - /** * Initializes {@link LuceneAcceleratedHNSWScalarQuantizedVectorsWriter} * @@ -110,15 +104,11 @@ public LuceneAcceleratedHNSWScalarQuantizedVectorsWriter( hnswVectorIndex = state.directory.createOutput(vexFileName, state.context); CodecUtil.writeIndexHeader( - hnswMeta, - HNSW_META_CODEC_NAME, - VERSION_CURRENT, - state.segmentInfo.getId(), - state.segmentSuffix); + hnswMeta, HNSW_META_CODEC_NAME, VERSION, state.segmentInfo.getId(), state.segmentSuffix); CodecUtil.writeIndexHeader( hnswVectorIndex, HNSW_INDEX_CODEC_NAME, - VERSION_CURRENT, + VERSION, state.segmentInfo.getId(), state.segmentSuffix); success = true; @@ -325,7 +315,7 @@ private void writeSingleVectorGraph(FieldInfo fieldInfo, List vectors) * Write field for merging. */ @Override - public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + protected void doMergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { flatVectorsWriter.mergeOneField(fieldInfo, mergeState); vectorBasedMerge(fieldInfo, mergeState); } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java deleted file mode 100644 index 9caa2a6aa3..0000000000 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package com.nvidia.cuvs.lucene; - -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.lucene.codecs.Codec; -import org.apache.lucene.codecs.KnnVectorsReader; -import org.apache.lucene.codecs.KnnVectorsWriter; -import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; -import org.apache.lucene.codecs.hnsw.FlatVectorsReader; -import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; -import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; -import org.apache.lucene.index.SegmentReadState; -import org.apache.lucene.index.SegmentWriteState; -import org.apache.lucene.index.VectorSimilarityFunction; -import org.apache.lucene.search.TaskExecutor; - -/** - * Dynamically loads Lucene format, reader, and writer classes with a fallback mechanism. - * - * @since 25.12 - */ -public class LuceneProvider { - - static final Logger log = Logger.getLogger(LuceneProvider.class.getName()); - - private static final String BASE = "org.apache.lucene."; - private static String codecs = "codecs.lucene."; - private static String fallbackCodecs = "backward_codecs.lucene."; - - private static String luceneFlatVectorsFormat = - BASE + codecs + "LuceneFlatVectorsFormat"; - private static String luceneFlatVectorsFormatFallback = - BASE + fallbackCodecs + "LuceneFlatVectorsFormat"; - - private static String luceneHnswVectorsFormat = - BASE + codecs + "LuceneHnswVectorsFormat"; - private static String luceneHnswVectorsFormatFallback = - BASE + fallbackCodecs + "LuceneHnswVectorsFormat"; - - private static String luceneHnswVectorsReader = - BASE + codecs + "LuceneHnswVectorsReader"; - private static String luceneHnswVectorsReaderFallback = - BASE + fallbackCodecs + "LuceneHnswVectorsReader"; - - private static String luceneHnswVectorsWriter = - BASE + codecs + "LuceneHnswVectorsWriter"; - private static String luceneHnswVectorsWriterFallback = - BASE + fallbackCodecs + "LuceneHnswVectorsWriter"; - - private static String luceneBinaryQuantizedVectorsFormat = - BASE + codecs + "LuceneBinaryQuantizedVectorsFormat"; - private static String luceneBinaryQuantizedVectorsFormatFallback = - BASE + fallbackCodecs + "LuceneBinaryQuantizedVectorsFormat"; - - private static String luceneHnswBinaryQuantizedVectorsFormat = - BASE + codecs + "LuceneHnswBinaryQuantizedVectorsFormat"; - private static String luceneHnswBinaryQuantizedVectorsFormatFallback = - BASE + fallbackCodecs + "LuceneHnswBinaryQuantizedVectorsFormat"; - - private static String luceneScalarQuantizedVectorsFormat = - BASE + codecs + "LuceneScalarQuantizedVectorsFormat"; - private static String luceneScalarQuantizedVectorsFormatFallback = - BASE + fallbackCodecs + "LuceneScalarQuantizedVectorsFormat"; - - private static String luceneHnswScalarQuantizedVectorsFormat = - BASE + codecs + "LuceneHnswScalarQuantizedVectorsFormat"; - private static String luceneHnswScalarQuantizedVectorsFormatFallback = - BASE + fallbackCodecs + "LuceneHnswScalarQuantizedVectorsFormat"; - - private static String luceneCodec = BASE + codecs + "LuceneCodec"; - private static String luceneCodecFallback = BASE + fallbackCodecs + "LuceneCodec"; - - private static LuceneProvider instance; - - private static MethodHandles.Lookup lookup = MethodHandles.lookup(); - - private Class flatVectorsFormat; - private Class hnswVectorsFormat; - private Class hnswVectorsReader; - private Class hnswVectorsWriter; - private Class binaryQuantizedVectorsFormat; - private Class hnswBinaryQuantizedVectorsFormat; - private Class scalarQuantizedVectorsFormat; - private Class hnswScalarQuantizedVectorsFormat; - - public static LuceneProvider getInstance(String version) throws ClassNotFoundException { - if (instance == null) { - instance = new LuceneProvider(version); - } - return instance; - } - - private LuceneProvider(String version) throws ClassNotFoundException { - flatVectorsFormat = - loadClass( - setVersion(luceneFlatVectorsFormat, version), - setVersion(luceneFlatVectorsFormatFallback, version)); - hnswVectorsFormat = - loadClass( - setVersion(luceneHnswVectorsFormat, version), - setVersion(luceneHnswVectorsFormatFallback, version)); - hnswVectorsReader = - loadClass( - setVersion(luceneHnswVectorsReader, version), - setVersion(luceneHnswVectorsReaderFallback, version)); - hnswVectorsWriter = - loadClass( - setVersion(luceneHnswVectorsWriter, version), - setVersion(luceneHnswVectorsWriterFallback, version)); - scalarQuantizedVectorsFormat = - loadClass( - setVersion(luceneScalarQuantizedVectorsFormat, version), - setVersion(luceneScalarQuantizedVectorsFormatFallback, version)); - - hnswScalarQuantizedVectorsFormat = - loadClass( - setVersion(luceneHnswScalarQuantizedVectorsFormat, version), - setVersion(luceneHnswScalarQuantizedVectorsFormatFallback, version)); - - // TODO: Find a better way if possible, but as a separate initiative. - if ("102".equals(version)) { - binaryQuantizedVectorsFormat = - loadClass( - setVersion(luceneBinaryQuantizedVectorsFormat, version), - setVersion(luceneBinaryQuantizedVectorsFormatFallback, version)); - hnswBinaryQuantizedVectorsFormat = - loadClass( - setVersion(luceneHnswBinaryQuantizedVectorsFormat, version), - setVersion(luceneHnswBinaryQuantizedVectorsFormatFallback, version)); - } - } - - private static String setVersion(String pkg, String version) { - return pkg.replaceAll("", version); - } - - private static Class loadClass(String defaultClassName, String fallbackClassName) - throws ClassNotFoundException { - try { - return Class.forName(defaultClassName); - } catch (ClassNotFoundException e) { - // Load class from fallback package. - try { - return Class.forName(fallbackClassName); - } catch (ClassNotFoundException e1) { - // Should not reach here. - log.log(Level.SEVERE, "Unable to load class: " + fallbackClassName); - throw e1; - } - } - } - - public static Codec getCodec(String version) - throws ClassNotFoundException, - NoSuchMethodException, - SecurityException, - InstantiationException, - IllegalAccessException, - IllegalArgumentException, - InvocationTargetException { - Class codecClass = - loadClass(setVersion(luceneCodec, version), setVersion(luceneCodecFallback, version)); - Constructor codecClassConstructor = codecClass.getConstructor(); - return (Codec) codecClassConstructor.newInstance(); - } - - public FlatVectorsFormat getLuceneFlatVectorsFormatInstance(FlatVectorsScorer scorer) - throws Exception { - try { - Constructor luceneFlatVectorsFormatConstructor = - flatVectorsFormat.getConstructor(FlatVectorsScorer.class); - return (FlatVectorsFormat) luceneFlatVectorsFormatConstructor.newInstance(scorer); - } catch (Exception e) { - log.log(Level.SEVERE, "Unable to initialize LuceneFlatVectorsFormat: " + e.getMessage()); - throw e; - } - } - - public KnnVectorsReader getLuceneHnswVectorsReaderInstance( - SegmentReadState state, FlatVectorsReader reader) throws Exception { - try { - Constructor luceneHnswVectorsReaderConstructor = - hnswVectorsReader.getConstructor(SegmentReadState.class, FlatVectorsReader.class); - return (KnnVectorsReader) luceneHnswVectorsReaderConstructor.newInstance(state, reader); - } catch (Exception e) { - log.log(Level.SEVERE, "Unable to initialize LuceneHnswVectorsReader: " + e.getMessage()); - throw e; - } - } - - public KnnVectorsWriter getLuceneHnswVectorsWriterInstance( - SegmentWriteState state, - int maxConn, - int beamWidth, - FlatVectorsWriter writer, - int numMergeWorkers, - TaskExecutor executor) - throws Exception { - try { - Constructor luceneHnswVectorsWriterConstructor = - hnswVectorsWriter.getConstructor( - SegmentWriteState.class, - Integer.TYPE, - Integer.TYPE, - FlatVectorsWriter.class, - Integer.TYPE, - TaskExecutor.class); - return (KnnVectorsWriter) - luceneHnswVectorsWriterConstructor.newInstance( - state, maxConn, beamWidth, writer, numMergeWorkers, executor); - } catch (Exception e) { - log.log(Level.SEVERE, "Unable to initialize LuceneHnswVectorsWriter: " + e.getMessage()); - throw e; - } - } - - public int getStaticIntParam(String param) throws ReflectiveOperationException { - try { - VarHandle varHandle = lookup.findStaticVarHandle(hnswVectorsFormat, param, Integer.TYPE); - return (int) varHandle.get(); - } catch (NoSuchFieldException | IllegalAccessException e) { - log.log(Level.SEVERE, "Unable to get " + param + ": " + e.getMessage()); - throw e; - } - } - - public List getSimilarityFunctions() - throws ReflectiveOperationException { - try { - VarHandle varHandle = - lookup.findStaticVarHandle(hnswVectorsReader, "SIMILARITY_FUNCTIONS", List.class); - return (List) varHandle.get(); - } catch (NoSuchFieldException | IllegalAccessException e) { - log.log(Level.SEVERE, "Unable to get SIMILARITY_FUNCTIONS: " + e.getMessage()); - throw e; - } - } - - public FlatVectorsFormat getluceneBinaryQuantizedVectorsFormatInstance() throws Exception { - try { - Constructor luceneBinaryQuantizedVectorsFormatConstructor = - binaryQuantizedVectorsFormat.getConstructor(); - return (FlatVectorsFormat) luceneBinaryQuantizedVectorsFormatConstructor.newInstance(); - } catch (Exception e) { - log.log( - Level.SEVERE, - "Unable to initialize LuceneBinaryQuantizedVectorsFormat: " + e.getMessage()); - throw e; - } - } - - public FlatVectorsFormat getLuceneHnswBinaryQuantizedVectorsFormatInstance( - int maxConn, int beamWidth) throws Exception { - try { - Constructor luceneHnswBinaryQuantizedVectorsFormatConstructor = - hnswBinaryQuantizedVectorsFormat.getConstructor(Integer.TYPE, Integer.TYPE); - return (FlatVectorsFormat) - luceneHnswBinaryQuantizedVectorsFormatConstructor.newInstance(maxConn, beamWidth); - } catch (Exception e) { - log.log( - Level.SEVERE, - "Unable to initialize LuceneBinaryQuantizedVectorsFormat: " + e.getMessage()); - throw e; - } - } - - public FlatVectorsFormat getLuceneScalarQuantizedVectorsFormatInstance() throws Exception { - try { - Constructor luceneScalarQuantizedVectorsFormatConstructor = - scalarQuantizedVectorsFormat.getConstructor(); - return (FlatVectorsFormat) luceneScalarQuantizedVectorsFormatConstructor.newInstance(); - } catch (Exception e) { - log.log( - Level.SEVERE, - "Unable to initialize LuceneScalarQuantizedVectorsFormat: " + e.getMessage()); - throw e; - } - } - - public FlatVectorsFormat getLuceneHnswScalarQuantizedVectorsFormatInstance( - int beamWidth, int maxConn) throws Exception { - try { - Constructor luceneHnswScalarQuantizedVectorsFormatConstructor = - hnswScalarQuantizedVectorsFormat.getConstructor(Integer.TYPE, Integer.TYPE); - return (FlatVectorsFormat) - luceneHnswScalarQuantizedVectorsFormatConstructor.newInstance(beamWidth, maxConn); - } catch (Exception e) { - log.log( - Level.SEVERE, - "Unable to initialize LuceneHnswScalarQuantizedVectorsFormat: " + e.getMessage()); - throw e; - } - } -} diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneVersionGuard.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneVersionGuard.java new file mode 100644 index 0000000000..8c64fb5d1c --- /dev/null +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneVersionGuard.java @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import org.apache.lucene.util.Version; + +/** + * Detects a cuvs-lucene artifact running against a Lucene release other than the one it was built + * for. + * + *

Lucene changes its codec APIs between minor releases, and some of those changes do not show + * up as linkage errors: a method whose signature changed simply stops overriding its Lucene + * counterpart, and Lucene silently runs its own implementation instead. Checking the version turns + * that into a clear error. + * + *

The check must only run where cuvs-lucene is actually used, never while Lucene's service loader + * instantiates the registered codecs and formats: an exception there would break every codec and + * format lookup in the JVM, including Lucene's own. + */ +final class LuceneVersionGuard { + + private LuceneVersionGuard() {} + + /** + * Throws unless the Lucene on the classpath is the release this artifact is built for. + * + * @throws IllegalStateException if another Lucene release is on the classpath + */ + static void ensureCompatible() { + String message = mismatchMessage(Version.LATEST); + if (message != null) { + throw new IllegalStateException(message); + } + } + + /** + * Returns why this artifact cannot run on the given Lucene release, or {@code null} if it can. + * + * @param running the Lucene release in use + * @return the error message, or null + */ + static String mismatchMessage(Version running) { + int major = LuceneCompat.LUCENE_MAJOR; + int minor = LuceneCompat.LUCENE_MINOR; + if (running.major == major && running.minor == minor) { + return null; + } + // Only this artifact's own release is known here, so name the one to use without claiming it + // exists: this cuvs-lucene release may not support that Lucene release at all. + return String.format( + "cuvs-lucene-%d.%d is built for Lucene %d.%d.x, but Lucene %s is in use. Use the" + + " cuvs-lucene-%d.%d artifact of this cuvs-lucene release; if there is none, this" + + " cuvs-lucene release does not support Lucene %d.%d.", + major, + minor, + major, + minor, + running, + running.major, + running.minor, + running.major, + running.minor); + } +} diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java index 034f8aa5d8..12a2a03d2f 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat; import org.apache.lucene.index.FloatVectorValues; import org.apache.lucene.index.KnnVectorValues; import org.apache.lucene.util.InfoStream; @@ -50,6 +52,24 @@ static RuntimeException handleThrowable(Throwable t) throws IOException { } } + /** + * Returns Lucene's own HNSW vectors format, configured like the given accelerated one. Used to + * build the graph on the CPU when no GPU is available. It writes the same files as the GPU + * writers, so the accelerated formats read its segments like their own. + * + * @param params the accelerated format's parameters + * @return the CPU vectors format + */ + static KnnVectorsFormat cpuHnswFormat(AcceleratedHNSWParams params) { + int numMergeWorkers = params.getNumMergeWorkers(); + // Lucene rejects an executor when merging on a single thread. + return new Lucene99HnswVectorsFormat( + params.getMaxConn(), + params.getBeamWidth(), + numMergeWorkers, + numMergeWorkers > 1 ? params.getMergeExec() : null); + } + /** * A method to build a CuVSMatrix from a list of float vectors. * diff --git a/java/cuvs-lucene/src/test/backcompat/GenerateReleasedIndices.java b/java/cuvs-lucene/src/test/backcompat/GenerateReleasedIndices.java new file mode 100644 index 0000000000..18c3569bda --- /dev/null +++ b/java/cuvs-lucene/src/test/backcompat/GenerateReleasedIndices.java @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Random; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.FilterCodec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.KnnFloatVectorField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.NoMergePolicy; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.util.Version; + +/** + * Writes the back-compat indexes of a released cuvs-lucene artifact, for TestBackCompatIndices. + * + *

Not part of the build: generate-released-indices.sh compiles and runs it against the published + * jars of one release. It writes the same documents as TestBackCompatIndices#writeIndex, so that the + * same checks apply to indexes written by released code. + * + *

It writes one index with each cuvs-lucene codec the release registers. Releases that registered + * no codecs (25.12 and earlier) instead get one index per cuvs-lucene vectors format, used per field + * by Lucene's default codec, which is how those releases were used. The GPU search codec and format + * are left out: their on-disk format is documented as experimental. + * + *

Arguments: the output directory and the cuvs-lucene version. + */ +public class GenerateReleasedIndices { + + // Keep in sync with TestBackCompatIndices. + private static final int NUM_DOCS = 200; + private static final int DIMENSIONS = 8; + + public static void main(String[] args) throws Exception { + Path outDir = Path.of(args[0]); + String cuvsVersion = args[1]; + Files.createDirectories(outDir); + String lucene = Version.LATEST.major + "." + Version.LATEST.minor; + List codecs = + Codec.availableCodecs().stream() + .filter(GenerateReleasedIndices::isCuVSCodec) + .sorted() + .toList(); + List formats = + codecs.isEmpty() + ? KnnVectorsFormat.availableKnnVectorsFormats().stream() + .filter(GenerateReleasedIndices::isCuVSFormat) + .sorted() + .toList() + : List.of(); + for (String name : codecs) { + write(Codec.forName(name), outDir, lucene + "-" + name + "-cuvs-" + cuvsVersion + ".zip"); + } + for (String name : formats) { + Codec base = Codec.getDefault(); + KnnVectorsFormat format = KnnVectorsFormat.forName(name); + Codec codec = + new FilterCodec(base.getName(), base) { + private final KnnVectorsFormat perField = + new PerFieldKnnVectorsFormat() { + @Override + public KnnVectorsFormat getKnnVectorsFormatForField(String field) { + return format; + } + }; + + @Override + public KnnVectorsFormat knnVectorsFormat() { + return perField; + } + }; + String fileName = + lucene + "-" + base.getName() + "-" + name + "-cuvs-" + cuvsVersion + ".zip"; + write(codec, outDir, fileName); + } + } + + private static boolean isCuVSCodec(String name) { + String cls = Codec.forName(name).getClass().getName(); + return cls.startsWith("com.nvidia.cuvs.") && !cls.contains("GPU"); + } + + private static boolean isCuVSFormat(String name) { + String cls = KnnVectorsFormat.forName(name).getClass().getName(); + return cls.startsWith("com.nvidia.cuvs.") && !cls.contains("GPU"); + } + + private static void write(Codec codec, Path outDir, String fileName) throws Exception { + Path indexDir = Files.createTempDirectory("backcompat"); + writeIndex(indexDir, codec); + zip(indexDir, outDir.resolve(fileName)); + System.out.println("wrote " + fileName); + } + + private static void writeIndex(Path indexDir, Codec codec) throws Exception { + Random random = new Random(42); + IndexWriterConfig config = + new IndexWriterConfig() + .setCodec(codec) + .setUseCompoundFile(false) + .setMergePolicy(NoMergePolicy.INSTANCE); + try (Directory dir = FSDirectory.open(indexDir); + IndexWriter writer = new IndexWriter(dir, config)) { + for (int i = 0; i < NUM_DOCS; i++) { + float[] vector = new float[DIMENSIONS]; + for (int d = 0; d < DIMENSIONS; d++) { + vector[d] = random.nextFloat(); + } + Document doc = new Document(); + doc.add(new StringField("id", Integer.toString(i), Field.Store.YES)); + doc.add(new KnnFloatVectorField("vector", vector, VectorSimilarityFunction.EUCLIDEAN)); + writer.addDocument(doc); + } + writer.commit(); + } + } + + private static void zip(Path indexDir, Path zip) throws Exception { + List files; + try (Directory dir = FSDirectory.open(indexDir)) { + files = SegmentInfos.readLatestCommit(dir).files(true).stream().sorted().toList(); + } + try (OutputStream out = Files.newOutputStream(zip); + ZipOutputStream zipOut = new ZipOutputStream(out)) { + for (String file : files) { + ZipEntry entry = new ZipEntry(file); + entry.setTime(0); + zipOut.putNextEntry(entry); + try (InputStream in = Files.newInputStream(indexDir.resolve(file))) { + in.transferTo(zipOut); + } + zipOut.closeEntry(); + } + } + } +} diff --git a/java/cuvs-lucene/src/test/backcompat/generate-released-indices.sh b/java/cuvs-lucene/src/test/backcompat/generate-released-indices.sh new file mode 100755 index 0000000000..b6e0401cf6 --- /dev/null +++ b/java/cuvs-lucene/src/test/backcompat/generate-released-indices.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Writes the back-compat indexes of a released cuvs-lucene version into src/test/resources/backcompat, +# using the jars published to Maven Central. Released cuvs-lucene versions up to 26.08 were a single +# com.nvidia.cuvs.lucene:cuvs-lucene artifact built for one Lucene release. +# +# Usage: generate-released-indices.sh +# Needs a GPU, a JDK 22+ and the native libcuvs libraries on LD_LIBRARY_PATH. + +set -euo pipefail + +VERSION="${1:?Usage: $0 }" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="${HERE}/../resources/backcompat" +CENTRAL="https://repo.maven.apache.org/maven2" +WORK="$(mktemp -d)" +trap 'rm -rf "${WORK}"' EXIT + +fetch() { # group/path artifact version + curl -sfL -o "${WORK}/$2-$3.jar" "${CENTRAL}/$1/$2/$3/$2-$3.jar" + echo "${WORK}/$2-$3.jar" +} + +POM="$(curl -sfL "${CENTRAL}/com/nvidia/cuvs/lucene/cuvs-lucene/${VERSION}/cuvs-lucene-${VERSION}.pom")" +dependency_version() { # artifactId + echo "${POM}" | grep -A2 "$1" | grep -o '[^<]*' | head -1 | sed 's///' +} +LUCENE_VERSION="$(dependency_version lucene-core)" +CUVS_JAVA_VERSION="$(dependency_version cuvs-java)" + +CP="$(fetch com/nvidia/cuvs/lucene cuvs-lucene "${VERSION}")" +CP="${CP}:$(fetch com/nvidia/cuvs cuvs-java "${CUVS_JAVA_VERSION}")" +CP="${CP}:$(fetch org/apache/lucene lucene-core "${LUCENE_VERSION}")" +CP="${CP}:$(fetch org/apache/lucene lucene-backward-codecs "${LUCENE_VERSION}")" + +javac -cp "${CP}" -d "${WORK}" "${HERE}/GenerateReleasedIndices.java" +java --enable-native-access=ALL-UNNAMED -cp "${CP}:${WORK}" GenerateReleasedIndices "${OUT}" "${VERSION}" diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java new file mode 100644 index 0000000000..2ba0a40634 --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; + +import org.apache.lucene.index.VectorEncoding; +import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; +import org.junit.BeforeClass; +import org.junit.Ignore; + +/** + * Runs Lucene's vectors-format test suite against a cuvs-lucene format, adapted to what all of them + * have in common: they need cuVS, and they index float vectors only. + * + *

It also absorbs the differences between the versions of Lucene's test suite that cuvs-lucene + * supports, so that the subclasses compile against all of them. + */ +public abstract class BaseCuVSKnnVectorsFormatTestCase extends BaseKnnVectorsFormatTestCase { + + @BeforeClass + public static void assumeCuVSSupported() { + assumeTrue("cuVS is not supported", isSupported()); + } + + @Override + protected VectorEncoding randomVectorEncoding() { + return VectorEncoding.FLOAT32; + } + + // BaseKnnVectorsFormatTestCase declares this abstract since Lucene 10.5 and not at all before, so + // it cannot be marked @Override. Lucene's own HNSW formats return false as well. + protected boolean supportsFloatVectorFallback() { + return false; + } + + // The tests below index byte vectors, which the cuvs-lucene formats do not support. + + // Lucene 10.5 added this test. It indexes byte vectors whatever randomVectorEncoding() returns. + // Not marked @Override, so that it compiles against earlier Lucene releases, where it is simply + // one more skipped test. + public void testWriterByteVectorRamEstimate() { + assumeTrue("byte vectors are not supported", false); + } + + @Ignore + @Override + public void testByteVectorScorerIteration() {} + + @Ignore + @Override + public void testEmptyByteVectorData() {} + + @Ignore + @Override + public void testMergingWithDifferentByteKnnFields() {} + + @Ignore + @Override + public void testMismatchedFields() {} + + @Ignore + @Override + public void testRandomBytes() {} + + @Ignore + @Override + public void testSortedIndexBytes() {} +} diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWOddGraphDegree.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWOddGraphDegree.java index 89012b1930..cfab27e91e 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWOddGraphDegree.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWOddGraphDegree.java @@ -70,7 +70,7 @@ public void testOddGraphDegreeIndexesAndSearches() throws Exception { .withGraphDegree(ODD_GRAPH_DEGREE) .build(); - Codec codec = new Lucene101AcceleratedHNSWCodec(params); + Codec codec = CuVSCodecs.acceleratedHNSW(params); IndexWriterConfig config = new IndexWriterConfig().setCodec(codec).setUseCompoundFile(false); int numDocs = 1000; diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java index 97fb066bd5..89bad0c8ca 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java @@ -52,7 +52,8 @@ private static KnnVectorsFormat[] readerFormats() { return new KnnVectorsFormat[] { new Lucene99AcceleratedHNSWVectorsFormat(), new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(), - new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat() + new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat(), + CuVSCodecs.acceleratedHNSWScalarQuantizedFormat(new AcceleratedHNSWParams.Builder().build()) }; } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java index d638180b06..12bf5dd909 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java @@ -5,37 +5,134 @@ package com.nvidia.cuvs.lucene; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.lucene.codecs.Codec; -import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.util.Version; import org.junit.Test; /** - * Tests the backward compatibility mechanism. + * Tests how the codecs of successive Lucene releases coexist: only the codecs from {@link + * CuVSCodecs} write, and every older codec this Lucene release can read is registered, read-only. * * @since 25.12 */ public class TestBackCompat { + private static List writableCodecs() { + return List.of( + CuVSCodecs.acceleratedHNSW(), + CuVSCodecs.acceleratedHNSWScalarQuantized(), + CuVSCodecs.acceleratedHNSWBinaryQuantized(), + CuVSCodecs.gpuSearch()); + } + + private static List registeredCodecs() { + List codecs = new ArrayList<>(); + for (String name : Codec.availableCodecs()) { + if (Codec.forName(name) instanceof CuVSFilterCodec codec) { + codecs.add(codec); + } + } + return codecs; + } + + @Test + public void testWritableCodecsAreRegistered() { + for (Codec codec : writableCodecs()) { + Codec registered = Codec.forName(codec.getName()); + assertSame(codec.getName(), codec.getClass(), registered.getClass()); + } + } + + @Test + public void testOlderCodecsAreReadOnly() { + Set writable = + writableCodecs().stream().map(Codec::getName).collect(Collectors.toSet()); + List registered = registeredCodecs(); + // One codec per family and Lucene codec generation this release can read. + assertEquals(0, registered.size() % writable.size()); + for (CuVSFilterCodec codec : registered) { + KnnVectorsFormat format = codec.knnVectorsFormat(); + assertNotNull(codec.getName(), format); + if (writable.contains(codec.getName())) { + continue; + } + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> format.fieldsWriter(null)); + assertTrue(e.getMessage(), e.getMessage().contains(codec.getName())); + } + } + @Test - public void testFallback() throws Exception { - // Lucene99Codec exists in the org.apache.lucene.backward_codecs.lucene99 - Codec c = LuceneProvider.getCodec("99"); - assertEquals(c.getName(), "Lucene99"); + public void testCallerChosenDelegateIsNeverReadOnly() { + // Lucene101Codec is a read-only backward codec from Lucene 10.3 on, but a codec built with a + // delegate chosen by the caller leaves that to the caller instead of guessing from the class. + Codec codec = new Lucene101AcceleratedHNSWCodec("CustomCodec", Codec.forName("Lucene101")); + assertTrue( + codec.knnVectorsFormat().toString(), + codec.knnVectorsFormat() instanceof Lucene99AcceleratedHNSWVectorsFormat); } - @Test(expected = ClassNotFoundException.class) - public void testNonexistentCodec() throws Exception { - LuceneProvider.getCodec("0"); + @Test + public void testReadOnlyFormatIsNotWrappedTwice() { + for (CuVSFilterCodec codec : registeredCodecs()) { + KnnVectorsFormat format = codec.knnVectorsFormat(); + codec.setKnnFormat(format); + // Handing back the format a read-only codec returned must not wrap it once more. + assertSame(codec.getName(), format, codec.knnVectorsFormat()); + } + } + + @Test + public void testLuceneFormatsStillResolve() { + // Listing a Lucene format in this module's services file used to break every lookup once a + // later Lucene release moved that format. + String name = "Lucene99HnswVectorsFormat"; + assertEquals(name, KnnVectorsFormat.forName(name).getName()); + } + + @Test + public void testMismatchMessage() { + assertNull(LuceneVersionGuard.mismatchMessage(Version.LATEST)); + String message = LuceneVersionGuard.mismatchMessage(Version.fromBits(9, 12, 1)); + assertNotNull(message); + assertTrue(message, message.contains("Lucene 9.12.1 is in use")); + assertTrue(message, message.contains("cuvs-lucene-9.12 artifact")); + assertTrue( + message, + message.contains( + "cuvs-lucene-" + LuceneCompat.LUCENE_MAJOR + "." + LuceneCompat.LUCENE_MINOR)); + } + + @Test + public void testUnavailableDelegate() { + // What a codec gets when its Lucene delegate does not link, e.g. on another Lucene release: + // creating the codec must not throw, since Lucene's service loader does it for every codec. + Codec codec = + CuVSFilterCodec.delegate( + "TestCodec", + () -> { + throw new NoClassDefFoundError("org/apache/lucene/codecs/Missing"); + }); + assertEquals("TestCodec", codec.getName()); + IllegalStateException e = assertThrows(IllegalStateException.class, codec::postingsFormat); + assertTrue(e.getMessage(), e.getMessage().contains("org/apache/lucene/codecs/Missing")); + assertThrows(IllegalStateException.class, codec::knnVectorsFormat); } @Test - public void testExistingComponents() throws Exception { - LuceneProvider provider = LuceneProvider.getInstance("99"); - assertTrue(provider.getLuceneFlatVectorsFormatInstance(null) instanceof FlatVectorsFormat); - assertEquals(provider.getStaticIntParam("VERSION_CURRENT"), 0); - assertNotEquals(provider.getSimilarityFunctions().size(), 0); + public void testLuceneVersionMatches() { + // Loading the compatibility layer checks that this is the Lucene release it was built for. + CuVSCodecs.checkLuceneVersion(); } } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompatIndices.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompatIndices.java new file mode 100644 index 0000000000..1c0008271e --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompatIndices.java @@ -0,0 +1,292 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; +import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; +import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.KnnFloatVectorField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FloatVectorValues; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.KnnVectorValues; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.NoMergePolicy; +import org.apache.lucene.index.SegmentCommitInfo; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.StoredFields; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.KnnFloatVectorQuery; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks; +import org.apache.lucene.util.Version; + +/** + * Opens and upgrades indexes written with the cuvs-lucene codecs on earlier Lucene releases and by + * earlier cuvs-lucene releases, to make sure an application can still use its indexes after it + * moves to a later Lucene release, cuvs-lucene release, or both. + * + *

The indexes are in {@code src/test/resources/backcompat}, one zip each, named {@code -[-][-cuvs-].zip}: the Lucene + * release they were written on, the codec recorded in their segments, the cuvs-lucene vectors format + * used per field if the codec is Lucene's own, and the released cuvs-lucene version that wrote them, + * if it was not the code in this repository. Each module reads the ones written on its own Lucene + * release and all earlier ones. See the {@code README.md} next to them for how to add more. + * + *

Reading and upgrading need no GPU, so those tests also run where cuVS is not available. + */ +@SuppressSysoutChecks(bugUrl = "") +public class TestBackCompatIndices extends LuceneTestCase { + + private static final String CREATE_PROPERTY = "tests.cuvs.createBackCompatIndices"; + private static final String RESOURCE_DIR = "/backcompat"; + private static final Pattern FILE_NAME = + Pattern.compile("(\\d+)\\.(\\d+)-(\\w+)(?:-(\\w+))?(?:-cuvs-([\\d.]+))?\\.zip"); + private static final String ID_FIELD = "id"; + private static final String VECTOR_FIELD = "vector"; + private static final int NUM_DOCS = 200; + private static final int DIMENSIONS = 8; + private static final int TOP_K = 10; + private static final int UPGRADE_DOCS = 50; + + /** The codecs that write the indexes; the GPU search codec's format makes no such promise. */ + private static List codecsToTest() { + return List.of( + CuVSCodecs.acceleratedHNSW(), + CuVSCodecs.acceleratedHNSWScalarQuantized(), + CuVSCodecs.acceleratedHNSWBinaryQuantized()); + } + + public void testReadIndices() throws Exception { + for (Path zip : indicesReadableByThisRelease()) { + Matcher matcher = FILE_NAME.matcher(zip.getFileName().toString()); + assertTrue(zip.toString(), matcher.matches()); + Path indexDir = createTempDir(zip.getFileName().toString()); + unzip(zip, indexDir); + try (Directory dir = newFSDirectory(indexDir)) { + checkIndex(dir, matcher.group(3), matcher.group(4), NUM_DOCS); + } catch (AssertionError | Exception e) { + throw new AssertionError("Failed to read " + zip.getFileName(), e); + } + } + } + + /** + * Opens each index for writing with the codec that writes on this Lucene release, adds documents + * and merges everything into one segment, which rewrites the old segments with the current codec, + * as an application does after moving to a later release. + */ + public void testUpgradeIndices() throws Exception { + for (Path zip : indicesReadableByThisRelease()) { + Matcher matcher = FILE_NAME.matcher(zip.getFileName().toString()); + assertTrue(zip.toString(), matcher.matches()); + Codec codec = currentCodecFor(matcher.group(3)); + Path indexDir = createTempDir(zip.getFileName().toString()); + unzip(zip, indexDir); + try (Directory dir = newFSDirectory(indexDir)) { + IndexWriterConfig config = + new IndexWriterConfig() + .setCodec(codec) + .setOpenMode(IndexWriterConfig.OpenMode.APPEND) + .setUseCompoundFile(false); + try (IndexWriter writer = new IndexWriter(dir, config)) { + Random random = new Random(7); + for (int i = NUM_DOCS; i < NUM_DOCS + UPGRADE_DOCS; i++) { + writer.addDocument(document(i, randomVector(random))); + } + writer.forceMerge(1); + } + checkIndex(dir, codec.getName(), null, NUM_DOCS + UPGRADE_DOCS); + } catch (AssertionError | Exception e) { + throw new AssertionError("Failed to upgrade " + zip.getFileName(), e); + } + } + } + + public void testCreateIndices() throws Exception { + // Written on the GPU, like the indexes of the applications this test stands for. + assumeTrue("cuVS not supported", isSupported()); + String target = System.getProperty(CREATE_PROPERTY); + assumeTrue("set -D" + CREATE_PROPERTY + " to write the indexes", target != null); + Path targetDir = Path.of(target); + Files.createDirectories(targetDir); + for (Codec codec : codecsToTest()) { + Path indexDir = createTempDir(codec.getName()); + writeIndex(indexDir, codec); + String name = Version.LATEST.major + "." + Version.LATEST.minor + "-" + codec.getName(); + zip(indexDir, targetDir.resolve(name + ".zip")); + } + } + + /** Returns the codec that writes on this Lucene release in the family of the given codec. */ + private static Codec currentCodecFor(String codecName) { + if (codecName.contains("ScalarQuantized")) { + return CuVSCodecs.acceleratedHNSWScalarQuantized(); + } else if (codecName.contains("BinaryQuantized")) { + return CuVSCodecs.acceleratedHNSWBinaryQuantized(); + } + // The accelerated HNSW codecs, and Lucene's own codecs with its vectors format used per field. + return CuVSCodecs.acceleratedHNSW(); + } + + private static List indicesReadableByThisRelease() throws IOException, URISyntaxException { + URL url = TestBackCompatIndices.class.getResource(RESOURCE_DIR); + assertNotNull(RESOURCE_DIR + " not found on the test classpath", url); + try (Stream files = Files.list(Path.of(url.toURI()))) { + List zips = + files + .filter( + f -> { + Matcher matcher = FILE_NAME.matcher(f.getFileName().toString()); + if (matcher.matches() == false) { + return false; + } + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + return major < Version.LATEST.major + || (major == Version.LATEST.major && minor <= Version.LATEST.minor); + }) + .sorted() + .toList(); + assertFalse("no back-compat indexes found under " + RESOURCE_DIR, zips.isEmpty()); + return zips; + } + } + + private static void writeIndex(Path indexDir, Codec codec) throws IOException { + Random random = new Random(42); + IndexWriterConfig config = + new IndexWriterConfig() + .setCodec(codec) + .setUseCompoundFile(false) + .setMergePolicy(NoMergePolicy.INSTANCE); + try (Directory dir = FSDirectory.open(indexDir); + IndexWriter writer = new IndexWriter(dir, config)) { + for (int i = 0; i < NUM_DOCS; i++) { + writer.addDocument(document(i, randomVector(random))); + } + writer.commit(); + } + } + + private static float[] randomVector(Random random) { + float[] vector = new float[DIMENSIONS]; + for (int d = 0; d < DIMENSIONS; d++) { + vector[d] = random.nextFloat(); + } + return vector; + } + + private static Document document(int id, float[] vector) { + Document doc = new Document(); + doc.add(new StringField(ID_FIELD, Integer.toString(id), Field.Store.YES)); + doc.add(new KnnFloatVectorField(VECTOR_FIELD, vector, EUCLIDEAN)); + return doc; + } + + /** + * Checks that every segment was written by the expected codec (and, for Lucene's own codecs, that + * the vector field uses the expected cuvs-lucene format), and that searching for each indexed + * vector finds its own document. + * + * @param perFieldFormat the vectors format name recorded for the vector field, or null to skip + */ + private static void checkIndex( + Directory dir, String codecName, String perFieldFormat, int numDocs) throws IOException { + for (SegmentCommitInfo info : SegmentInfos.readLatestCommit(dir)) { + assertEquals(codecName, info.info.getCodec().getName()); + } + try (DirectoryReader reader = DirectoryReader.open(dir)) { + assertEquals(numDocs, reader.numDocs()); + if (perFieldFormat != null) { + for (var context : reader.leaves()) { + FieldInfo field = context.reader().getFieldInfos().fieldInfo(VECTOR_FIELD); + assertEquals( + perFieldFormat, field.getAttribute(PerFieldKnnVectorsFormat.PER_FIELD_FORMAT_KEY)); + } + } + IndexSearcher searcher = new IndexSearcher(reader); + int found = 0; + for (var context : reader.leaves()) { + LeafReader leaf = context.reader(); + FloatVectorValues vectors = leaf.getFloatVectorValues(VECTOR_FIELD); + assertEquals(DIMENSIONS, vectors.dimension()); + StoredFields storedFields = leaf.storedFields(); + KnnVectorValues.DocIndexIterator it = vectors.iterator(); + for (int doc = it.nextDoc(); doc != NO_MORE_DOCS; doc = it.nextDoc()) { + String id = storedFields.document(doc).get(ID_FIELD); + float[] query = vectors.vectorValue(it.index()).clone(); + ScoreDoc[] hits = + searcher.search(new KnnFloatVectorQuery(VECTOR_FIELD, query, TOP_K), TOP_K).scoreDocs; + StoredFields allStoredFields = searcher.storedFields(); + for (ScoreDoc hit : hits) { + if (id.equals(allStoredFields.document(hit.doc).get(ID_FIELD))) { + found++; + break; + } + } + } + } + // Quantized codecs may rank a few near-duplicates ahead of the exact match. + assertTrue( + "only " + found + " of " + numDocs + " vectors found themselves", found >= numDocs * 0.9); + } + } + + private static void unzip(Path zip, Path targetDir) throws IOException { + try (ZipInputStream in = new ZipInputStream(Files.newInputStream(zip))) { + for (ZipEntry entry = in.getNextEntry(); entry != null; entry = in.getNextEntry()) { + Files.copy(in, targetDir.resolve(entry.getName())); + } + } + } + + /** Zips the files of the index's latest commit, leaving out anything else in the directory. */ + private static void zip(Path indexDir, Path zip) throws IOException { + List files; + try (Directory dir = FSDirectory.open(indexDir)) { + files = SegmentInfos.readLatestCommit(dir).files(true).stream().sorted().toList(); + } + try (OutputStream out = Files.newOutputStream(zip); + ZipOutputStream zipOut = new ZipOutputStream(out)) { + for (String file : files) { + ZipEntry entry = new ZipEntry(file); + // A fixed time keeps regenerated zips identical when the index is. + entry.setTime(0); + zipOut.putNextEntry(entry); + try (InputStream in = Files.newInputStream(indexDir.resolve(file))) { + in.transferTo(zipOut); + } + zipOut.closeEntry(); + } + } + } +} diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearch.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearch.java index 934e7c9244..acbda63fe6 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearch.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearch.java @@ -60,7 +60,7 @@ public void beforeTest() throws Exception { @Test public void testCagraToHnswSerializationAndSearch() throws Exception { AcceleratedHNSWParams params = new AcceleratedHNSWParams.Builder().build(); - Codec codec = new Lucene101AcceleratedHNSWCodec(params); + Codec codec = CuVSCodecs.acceleratedHNSW(params); IndexWriterConfig config = new IndexWriterConfig().setCodec(codec).setUseCompoundFile(false); final int COMMIT_FREQ = 2000; @@ -151,7 +151,7 @@ public void testSingleVectorIndex() throws Exception { // Test single vector index support with dummy HNSW graph // TODO: This test can be removed once https://github.com/rapidsai/cuvs/pull/1256 is merged // and CAGRA natively supports single vector indexes - Codec codec = new Lucene101AcceleratedHNSWCodec(); + Codec codec = CuVSCodecs.acceleratedHNSW(); final String ID_FIELD = "id"; final String VECTOR_FIELD = "vector_field"; diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearchWithFallbackWriter.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearchWithFallbackWriter.java index 996c1d7575..4edb963e14 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearchWithFallbackWriter.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCagraToHnswSerializationAndSearchWithFallbackWriter.java @@ -70,7 +70,7 @@ public void testCagraToHnswSerializationAndSearchWithFallbackWriter() throws Exc .withMaxConn(16) .withBeamWidth(100) .build(); - Codec codec = new Lucene101AcceleratedHNSWCodec(params); + Codec codec = CuVSCodecs.acceleratedHNSW(params); IndexWriterConfig config = new IndexWriterConfig().setCodec(codec).setUseCompoundFile(false); diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSDeletedDocuments.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSDeletedDocuments.java index e2acf61e25..246e480d00 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSDeletedDocuments.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSDeletedDocuments.java @@ -429,9 +429,12 @@ public void testSearchWithAcceptDocsMatchingNoVectors() throws IOException { float[] queryVector = generateRandomVector(dimensions, random); for (LeafReaderContext ctx : reader.leaves()) { TopKnnCollector collector = new TopKnnCollector(topK, Integer.MAX_VALUE); - ctx.reader() - .searchNearestVectors( - "vector", queryVector, collector, new Bits.MatchNoBits(ctx.reader().maxDoc())); + TestLuceneCompat.searchNearestVectors( + ctx.reader(), + "vector", + queryVector, + collector, + new Bits.MatchNoBits(ctx.reader().maxDoc())); assertEquals( "An empty accepted-ordinal set should collect nothing", 0, diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSVectorsFormat.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSVectorsFormat.java index 6db380a3ce..947d0da805 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSVectorsFormat.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSVectorsFormat.java @@ -5,7 +5,6 @@ package com.nvidia.cuvs.lucene; import static com.nvidia.cuvs.lucene.TestUtils.assertVectorsKeepTheirDocuments; -import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; import java.util.List; @@ -18,21 +17,12 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.index.VectorEncoding; import org.apache.lucene.store.Directory; -import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks; import org.apache.lucene.tests.util.TestUtil; -import org.junit.BeforeClass; -import org.junit.Ignore; @SuppressSysoutChecks(bugUrl = "") -public class TestCuVSVectorsFormat extends BaseKnnVectorsFormatTestCase { - - @BeforeClass - public static void beforeClass() { - assumeTrue("cuVS is not supported", isSupported()); - } +public class TestCuVSVectorsFormat extends BaseCuVSKnnVectorsFormatTestCase { @Override protected Codec getCodec() { @@ -98,46 +88,11 @@ public void testTwoVectorFieldsPerDoc() throws Exception { assertVectorsKeepTheirDocuments(r, "f2", f2); // opportunistically check boundary condition - search with a 0 topK - var topDocs = r.searchNearestVectors("f1", randomVector(384), 0, null, 10); + var topDocs = + TestLuceneCompat.searchNearestVectors(r, "f1", randomVector(384), 0, null, 10); assertEquals(0, topDocs.scoreDocs.length); assertEquals(0, topDocs.totalHits.value()); } } } - - @Override - // Overriding this method from superclass for the tests to only use float vector encoding - protected VectorEncoding randomVectorEncoding() { - return VectorEncoding.FLOAT32; - } - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testByteVectorScorerIteration() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testEmptyByteVectorData() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testMergingWithDifferentByteKnnFields() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testMismatchedFields() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testRandomBytes() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testSortedIndexBytes() {} } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestGPUSelectiveFilterFallback.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestGPUSelectiveFilterFallback.java index a031a10669..9e705f37b1 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestGPUSelectiveFilterFallback.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestGPUSelectiveFilterFallback.java @@ -82,7 +82,7 @@ public static void beforeClass() throws Exception { // Disable merges so the commits below yield one GPU segment each -> multi-partition search. IndexWriterConfig config = new IndexWriterConfig() - .setCodec(new CuVS2510GPUSearchCodec()) + .setCodec(CuVSCodecs.gpuSearch()) .setMergePolicy(NoMergePolicy.INSTANCE); try (IndexWriter writer = new IndexWriter(directory, config)) { for (int i = 0; i < DATASET_SIZE; i++) { @@ -199,7 +199,7 @@ public void singleSegmentSelectiveFilterIsAnsweredExactly() throws Exception { try (Directory dir = newDirectory(new ByteBuffersDirectory())) { IndexWriterConfig config = new IndexWriterConfig() - .setCodec(new CuVS2510GPUSearchCodec()) + .setCodec(CuVSCodecs.gpuSearch()) .setMergePolicy(NoMergePolicy.INSTANCE); try (IndexWriter writer = new IndexWriter(dir, config)) { for (int i = 0; i < DOCS_PER_SEGMENT; i++) { diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestLucene99AcceleratedHNSWVectorsFormat.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestLucene99AcceleratedHNSWVectorsFormat.java index 707ee35709..ed95b2c31b 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestLucene99AcceleratedHNSWVectorsFormat.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestLucene99AcceleratedHNSWVectorsFormat.java @@ -5,7 +5,6 @@ package com.nvidia.cuvs.lucene; import static com.nvidia.cuvs.lucene.TestUtils.assertVectorsKeepTheirDocuments; -import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; import java.util.List; @@ -18,21 +17,12 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.index.VectorEncoding; import org.apache.lucene.store.Directory; -import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks; import org.apache.lucene.tests.util.TestUtil; -import org.junit.BeforeClass; -import org.junit.Ignore; @SuppressSysoutChecks(bugUrl = "") -public class TestLucene99AcceleratedHNSWVectorsFormat extends BaseKnnVectorsFormatTestCase { - - @BeforeClass - public static void beforeClass() { - assumeTrue("cuVS is not supported", isSupported()); - } +public class TestLucene99AcceleratedHNSWVectorsFormat extends BaseCuVSKnnVectorsFormatTestCase { @Override protected Codec getCodec() { @@ -98,46 +88,11 @@ public void testTwoVectorFieldsPerDoc() throws Exception { assertVectorsKeepTheirDocuments(r, "f2", f2); // opportunistically check boundary condition - search with a 0 topK - var topDocs = r.searchNearestVectors("f1", randomVector(384), 0, null, 10); + var topDocs = + TestLuceneCompat.searchNearestVectors(r, "f1", randomVector(384), 0, null, 10); assertEquals(0, topDocs.scoreDocs.length); assertEquals(0, topDocs.totalHits.value()); } } } - - @Override - // Overriding this method from superclass for the tests to only use float vector encoding - protected VectorEncoding randomVectorEncoding() { - return VectorEncoding.FLOAT32; - } - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testByteVectorScorerIteration() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testEmptyByteVectorData() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testMergingWithDifferentByteKnnFields() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testMismatchedFields() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testRandomBytes() {} - - @Ignore - @Override - // Ignoring this test from superclass as we do not support byte vectors - public void testSortedIndexBytes() {} } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestMultiSegmentGPUFilterConcurrency.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestMultiSegmentGPUFilterConcurrency.java index 7c61e50817..14eb82c41e 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestMultiSegmentGPUFilterConcurrency.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestMultiSegmentGPUFilterConcurrency.java @@ -83,7 +83,7 @@ public static void beforeClass() throws Exception { assumeTrue("cuVS not supported: " + unsupported.getMessage(), false); } assumeTrue("cuVS not supported", isSupported()); - codec = new CuVS2510GPUSearchCodec(new GPUSearchParams.Builder().build(), TEST_CACHE_CONFIG); + codec = CuVSCodecs.gpuSearch(new GPUSearchParams.Builder().build(), TEST_CACHE_CONFIG); int datasetSize = 2000; int dimensions = 128; @@ -114,8 +114,7 @@ public static void beforeClass() throws Exception { // DirectoryReader resolves the stored codec name through SPI. Give that singleton the same // deliberately small cache budget while it constructs this test's segment readers, then restore // its previous format so the test does not alter later reader construction in this JVM. - CuVS2510GPUSearchCodec spiCodec = - (CuVS2510GPUSearchCodec) Codec.forName("CuVS2510GPUSearchCodec"); + CuVSFilterCodec spiCodec = (CuVSFilterCodec) Codec.forName(codec.getName()); KnnVectorsFormat previousFormat = spiCodec.knnVectorsFormat(); spiCodec.setKnnFormat( new CuVS2510GPUVectorsFormat(new GPUSearchParams.Builder().build(), TEST_CACHE_CONFIG)); diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestPerSegmentGPUFilterSearch.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestPerSegmentGPUFilterSearch.java index df2ba21189..89ba3989a4 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestPerSegmentGPUFilterSearch.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestPerSegmentGPUFilterSearch.java @@ -53,7 +53,7 @@ public void testSelectiveFilterDoesNotLeakTrailingOrdinals() throws Exception { try (Directory directory = newDirectory(new ByteBuffersDirectory())) { float[][] dataset = generateDataset(random(), datasetSize, dimensions); - IndexWriterConfig config = new IndexWriterConfig().setCodec(new CuVS2510GPUSearchCodec()); + IndexWriterConfig config = new IndexWriterConfig().setCodec(CuVSCodecs.gpuSearch()); try (IndexWriter writer = new IndexWriter(directory, config)) { for (int i = 0; i < datasetSize; i++) { Document doc = new Document(); @@ -77,7 +77,7 @@ public void testSelectiveFilterDoesNotLeakTrailingOrdinals() throws Exception { } for (float[] q : queries) { TopKnnCollector collector = new TopKnnCollector(topK, Integer.MAX_VALUE); - leaf.searchNearestVectors(VECTOR_FIELD, q, collector, acceptDocs); + TestLuceneCompat.searchNearestVectors(leaf, VECTOR_FIELD, q, collector, acceptDocs); for (ScoreDoc hit : collector.topDocs().scoreDocs) { assertTrue( "per-segment search returned doc " + hit.doc + " outside filter category " + c, diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java new file mode 100644 index 0000000000..4c85214aff --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.closeCuVSResourcesInstance; +import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; +import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.setCuVSResourcesInstance; +import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.KnnFloatVectorField; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.SerialMergeScheduler; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.KnnFloatVectorQuery; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +/** + * Checks that the accelerated HNSW codecs build the graph on the CPU when cuVS is not available, and + * that they read back what their CPU fallback wrote. + */ +@SuppressSysoutChecks(bugUrl = "") +public class TestQuantizedFallbackWriters extends LuceneTestCase { + + private static final int NUM_DOCS = 300; + private static final int DIMENSIONS = 16; + private static final int TOP_K = 10; + + @BeforeClass + public static void beforeClass() { + assumeTrue("cuVS not supported", isSupported()); + // Makes isSupported() false on this thread, like on a host without a GPU. Close the resources + // isSupported() just created first, so that they do not leak. + closeCuVSResourcesInstance(); + setCuVSResourcesInstance(null); + } + + @AfterClass + public static void afterClass() { + // Forget the null set above; the next use on this thread creates resources again, if any. + closeCuVSResourcesInstance(); + } + + public void testFallbackWriters() throws Exception { + AcceleratedHNSWParams params = + new AcceleratedHNSWParams.Builder().withMaxConn(16).withBeamWidth(100).build(); + for (Codec codec : + List.of( + CuVSCodecs.acceleratedHNSW(params), + CuVSCodecs.acceleratedHNSWScalarQuantized(params), + CuVSCodecs.acceleratedHNSWBinaryQuantized(params))) { + checkWritesAndReads(codec); + } + } + + private void checkWritesAndReads(Codec codec) throws Exception { + Random random = new Random(random().nextLong()); + float[][] vectors = new float[NUM_DOCS][DIMENSIONS]; + AtomicInteger fallbacks = new AtomicInteger(); + Handler countFallbacks = + new Handler() { + @Override + public void publish(LogRecord record) { + if (record.getMessage() != null && record.getMessage().contains("falling back")) { + fallbacks.incrementAndGet(); + } + } + + @Override + public void flush() {} + + @Override + public void close() {} + }; + Logger logger = Logger.getLogger("com.nvidia.cuvs.lucene"); + logger.addHandler(countFallbacks); + try (Directory dir = newDirectory()) { + // Flushes and merges on this thread, where cuVS is unavailable. + IndexWriterConfig config = + new IndexWriterConfig() + .setCodec(codec) + .setUseCompoundFile(false) + .setMergeScheduler(new SerialMergeScheduler()); + try (IndexWriter writer = new IndexWriter(dir, config)) { + for (int i = 0; i < NUM_DOCS; i++) { + for (int d = 0; d < DIMENSIONS; d++) { + vectors[i][d] = random.nextFloat(); + } + Document doc = new Document(); + doc.add(new StoredField("id", i)); + doc.add(new KnnFloatVectorField("vector", vectors[i], EUCLIDEAN)); + writer.addDocument(doc); + } + } + assertTrue(codec.getName() + " did not fall back to the CPU", fallbacks.get() > 0); + try (DirectoryReader reader = DirectoryReader.open(dir)) { + assertEquals(codec.getName(), NUM_DOCS, reader.numDocs()); + IndexSearcher searcher = new IndexSearcher(reader); + int found = 0; + for (int i = 0; i < NUM_DOCS; i++) { + ScoreDoc[] hits = + searcher.search(new KnnFloatVectorQuery("vector", vectors[i], TOP_K), TOP_K) + .scoreDocs; + for (ScoreDoc hit : hits) { + if (searcher.storedFields().document(hit.doc).getField("id").numericValue().intValue() + == i) { + found++; + break; + } + } + } + assertTrue( + codec.getName() + ": only " + found + " of " + NUM_DOCS + " vectors found themselves", + found >= NUM_DOCS * 0.9); + } + } finally { + logger.removeHandler(countFallbacks); + } + } +} diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java index 3b05beaacf..2a1be0b20e 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java @@ -5,7 +5,6 @@ package com.nvidia.cuvs.lucene; import static com.nvidia.cuvs.lucene.TestUtils.assertVectorsKeepTheirDocuments; -import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; import static org.apache.lucene.index.VectorSimilarityFunction.COSINE; import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; @@ -26,17 +25,13 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.index.VectorEncoding; import org.apache.lucene.store.ByteBuffersDirectory; import org.apache.lucene.store.Directory; -import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks; import org.apache.lucene.tests.util.TestUtil; -import org.junit.BeforeClass; -import org.junit.Ignore; @SuppressSysoutChecks(bugUrl = "") -public class TestQuantizedVectorsFormats extends BaseKnnVectorsFormatTestCase { +public class TestQuantizedVectorsFormats extends BaseCuVSKnnVectorsFormatTestCase { private static final Logger log = Logger.getLogger(TestQuantizedVectorsFormats.class.getName()); @@ -51,15 +46,13 @@ public static List parameters() { return Arrays.asList( new Object[][] { {new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat()}, - {new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat()} + { + CuVSCodecs.acceleratedHNSWScalarQuantizedFormat( + new AcceleratedHNSWParams.Builder().build()) + } }); } - @BeforeClass - public static void beforeClass() { - assumeTrue("cuVS is not supported so skipping these tests", isSupported()); - } - @Override protected Codec getCodec() { log.log(Level.FINE, "Running tests for: " + knnVectorsFormat.getName()); @@ -159,39 +152,10 @@ public void testCosineSimilarity() throws Exception { assertEquals(R, values.size()); float[] queryVector = randomVector(D); - var topDocs = r.searchNearestVectors(F, queryVector, 2, null, 10); + var topDocs = TestLuceneCompat.searchNearestVectors(r, F, queryVector, 2, null, 10); assertTrue("Should return at least one result", topDocs.scoreDocs.length > 0); assertTrue("Scores should be non-negative", topDocs.scoreDocs[0].score >= 0); } } } - - @Override - protected VectorEncoding randomVectorEncoding() { - return VectorEncoding.FLOAT32; - } - - @Ignore - @Override - public void testByteVectorScorerIteration() {} - - @Ignore - @Override - public void testEmptyByteVectorData() {} - - @Ignore - @Override - public void testMergingWithDifferentByteKnnFields() {} - - @Ignore - @Override - public void testMismatchedFields() {} - - @Ignore - @Override - public void testRandomBytes() {} - - @Ignore - @Override - public void testSortedIndexBytes() {} } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java index 445e1da9b3..47a3f30320 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java @@ -68,7 +68,7 @@ public void testRecordedMMatchesEachSegmentsGraph() throws Exception { IndexWriterConfig cfg = new IndexWriterConfig() .setCodec( - new Lucene101AcceleratedHNSWCodec( + CuVSCodecs.acceleratedHNSW( new AcceleratedHNSWParams.Builder() .withStrategy(AcceleratedHNSWParams.Strategy.HEURISTIC) .withMaxConn(MAX_CONN) @@ -135,8 +135,7 @@ public void testSingleVectorSegmentDoesNotUseConfiguredGraphDegree() throws Exce .build(); try (Directory dir = newDirectory()) { - IndexWriterConfig cfg = - new IndexWriterConfig().setCodec(new Lucene101AcceleratedHNSWCodec(params)); + IndexWriterConfig cfg = new IndexWriterConfig().setCodec(CuVSCodecs.acceleratedHNSW(params)); cfg.setMergePolicy(NoMergePolicy.INSTANCE); try (IndexWriter w = new IndexWriter(dir, cfg)) { addDocs(w, 1); diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101-Lucene99AcceleratedHNSWVectorsFormat-cuvs-25.12.0.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101-Lucene99AcceleratedHNSWVectorsFormat-cuvs-25.12.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..c78e454f0f12a6e5af19d5472d55cb7b68f27ebf GIT binary patch literal 12484 zcmbt)WmH{D5-#oz!Gi^NcXxujLvVM3ySuwfa0za~-3cDtHMj>j@JMFXjm*q_@2=OU z&kxpFWxKzsT}^#*lAvH{Ku}OnKtMo*FW;XF0tgyNm!8(x&`Mbe90>UP549axZ)i^x z(=yVM&CS-6Qq$rUj#883G&a&T;s!fAJ2B_IOh}l#_o$H-<6{7ytiUm`vDIXONSb_7 zphMs>Kl=tVWkAO>`$Lm6H@_2aspU5n?MID(WG3zK8R9y7;DZ2?lZ1piJ!YuTf3cSJ z@|F9)yXW|qdli#B7D@tW{pU>Xwff0Wevzz;YGBQmPR4~uIBm3XGJP_}5)DXxVd{K- zA@!LBnVD^YUa&Y+R3i__j}xSQ{Y^-{zI&rIUfjt&%ip|Qb`Dp+0{SjR4)AGpTQzXu zCeVW+#d;o1U7g!A!b4o|Zw892A69}#7`X56cW$Bv2VnJSWg1nAmRw|?4^}y!0p8;$ zTbi=$wF^g2N2@N}nIL1hM22#Hb&HIA3-~ z568dvfNRFf#-}6XnR@emGZKI@F2|g6GOmNr{%da%(N1C7A!BT@)5juX#Uhu8i%=@W zhACmcDq`=Wpxr?E_b!R(Atpp+BxGWI^Y0yrm28KlY773?zHJ7Qj2*uLC^=Ev^~UoP z$;XLjg|QDFD6JOMHuSU3n_6X_&}CNi0FYNdT(D+d&(=3@+?I{oPA#}~FDU6i*5#dN zP#r8NS;3mkvd(*FvpP0p)g^gkq%8?H-?Oq+D9v$d%5r2T7gQ(>H>RI@4-GANUtV_E zd_VHdAphfr7l^5X{XZSK%U?(S1ChJ`1(B%=T|i)V$IdE@m(JrT3@p$K9ZM=c8$SKp z7;tj7iU;$MKd^=o(pJR%f;0aAC#+fjkqX=YHoW;g6*g0olXOR4vHATs!dSGOG`{ue}X(e8e==d;@+1(18%(ox`$Xb)@meNGUJ0L zT)W+Q;Oq9jLbkSIMHMF66pVJn44qL&vlW`ZUfG-8yayOmD_FO2lK zSu$^EP2`iI%0O*Z$07lh=l07^LlOufPV)+YfE$3yaoSn zysY6dxB~ycOL2&lyXXsEWdAlL{);=d4u7P_@&9n=f4wc5HV(I5-V#213A{NB-@v7$ z+VVz0e)tsHFNi1k;*aS+^~urfr%#b_6X7!Qm;{M`Vh{mycfKf*0fIL?ew= zdgo5@qjLVoD{7-sH8Ft-aVQe#VTpZJXBY(16jXhSg60XI*0a=Gr(4%%ZyjukP93sN zIc{9_o>#lNXU}&g6{aK73i>R1B)7h*C%xKB?%<}nEni(DkkHx&MmzE1VVsB4@Hj2F5K(m6A}>N7Zwy{m$D=Bk>==8D}CaL{P0FdE~nd~ zgm1Nvnd>^&Ifom|TN*6n^5Vf(l(sO*NP)2}+thZ*4JkK?Moe#2Xo6 ziF^?hrolKtY$1gJOqLtyT$H%-H-d5faRHbp2Cn1bd#>lpHeMf39{Nrl*5nC^+-d_WH7`W>}ui8~%aJDHp^fL%Je%Q{GRF(Vfi& z(Xplguv@LG0kCuMYU!iKlJfq^H9a?~pCPV(isd;g#n_Sn#UV~(jrjd8i^_FARv zzQ%sa1@qZNSX~;Pu|qlcu9(sTLzRf-h#{-eM#QH9D>=m}?^)K#QOOPQw70LE`J_K= z);yw(GoENn&re~&Hbe*{;Jl=^smK$CEU}|IgUp)wOLq9iCwSMr97qrcgg34u?B(G) z-JV=RY6)LJQzuP6Je1fyEV>u#%&IC(4}?_X_g@|tS1&iN#uUZB{rt$T%FZC^|0(@B z;B=steLe$G?@Eicaz>~Zj84XU%ul)=@0E9%|3&U4(u4;I_gJ05jth;)BJCDfbVafI zy01>bWRo$ozf&WdfPxi&$ITG;{tdPOExM&NZNOT|juwM8ki%h-owG0UZ9=r(4jQ-; zTjr}U$@YAQC#8m2?j&5kGt@HFU>IfmHKL;{vYBYtz{fYc9yu|Q4RH`3_S*f2vO`D5 zmHLKM-`nOOdh3`Df3-eHfI{WCnNjm{$L(8jgAYmJjmT?GM*@&XYEE6Nt+gnc>MDm~ zLB|&>*g?Rv;Mlnsae5x>yC`l(eE4$d(r&}sxMtQR5E&Vh`Z^$U!jW>EqHBB6?BcbV zm`(r`<>w#PWc*aHG`SF#$b8;-7^CArr1Si{(NI&8LgVdRjxz|_q@uH0V*d?}>g56yp>(}q`S%NfQzZ;22YT=lCwExPF6}xs zbY*l@mQFc^cIXq|5H_1*a<8&H@nx;7zI`AJ*bF>OXa$7OU(XBft!ocbPUr1^MG%-J zN;63ktKlB?A)(E0IvoPP)2ZIuORdf@E0cZ(uf^okgyobeBWPLsTFPYsJ@81s>tCjGe@qMM99Q$-8ZqGMRud$}tPfNMDCLZh; z?NK%GVf2L+nHo|}V%U_`PRj$}3cCgbqSa5O0tzQU_lt7;x9PRfyyiFd9wmcefl8Mqz?Z z1ZJLn!XAtA01g~-t~TB>MR;b50XIJWoZ$qeJi&Ed*NT;v<{6{5OSe5Ds&C$DYfde$ zSI|t-(5ND`C4wkCMN*0AY(_7@fCvwp&@@rY#{$VwQzg*;W_J$FA{ct(SimtYJCaV7 zn|!jI|GfwA?hRZ~XtWYj#q^RNr&JHM@BorR<^U`Hh&tTitX5 zT4N67mZVy@uL48$7#dnD6CvJ3(uLExVuLb@n_Y=5)F+ppV$#b8A_{G67uhjNgpL{P z3V95iyv1%wQKKU23!VqrzLeOEx3t)nssaKxWLG;yT?t;-D{wIDgM1Rghp7QEa?;L5 zByol`6vJbQQ!lF+N%$FATzEwRkz`#x9OYy#7IG)hiQlR6uyy^!WH)}D~5*jA>TC4D9v zwG>or0&_^s!qht8;XAo%)478S7&?Ru&RE9jRMiJ1gqat^0r@;TpII}z3ymGZW|Y+h zZ8Qu4!5fN9`vxc73{GLAmfu2Qvq`H}rwC@A6C;tog2_~{>CKIb$+dNa;N;z(eeh$| zTnazC26hosfvkSd_lUPIHNVTid~d^Iy^0s{b^-TfX*oOZ$vAFP)gquVAB3F3dAF$} z=<{~Ps~$)1SZWohobq<5v(q*I)jWGJFCHj$e=iS@pUn1)$0`Q09)2VBt@ z?c@%f)vE05byznN432Bf0exm%44b1wm2*mauvC0bpHclI)Ss` zUE;`Qbs(#iHt>l#wdiVP+M>T>gZ75i81E*hDQ|yMMM4VSP)!8}J79)MZ--cebIeiI z6{D`)6L=*-JIZjG+jXcK2jV~7G&ztyl}{N**?ijegeOFkO@LRK7LBYOHUrb*!_m`< zwT!RE=YqxLET+(*6WNta%1?X|bG2&U6Hpoph5Nz3N*8k7d;)4tMe}^Gu+MV!-N>Cg zxT!i{_K8YRIkz)x%^q6-NClJ^djl+J8p^(deDG`4Ab!tg9=e0~1Y0YVN@QjRQ}x)~ ztyCNCks2AJTH(*ez13W9IXPEfskQ21_Bf)EL=hD9)XW*?vO+f3)~vqls=i6(Q4GhQ z;j~-updB+55ucJZ%FX135p&Uh+W?FT*>oYJhH=r}TWjVAFUeVSzC;Rd939&AB*k-r zlQLL(fH11?9KzSIFzsoJ-PkU_c^+z4+{7DnA|#}SnHjp?@U3`hBam>w1M^ZTJV+3! z&*8dRubSiO4p(#~Zv->7Y&b?s%z9F4FF}Rl8LPPG03gt{D}}}A?C$jBLd3v4Mkj$8qDs40_%5}kDUUnsb2Nrt zjTr>p1XF(B5pvTz^S%;P4Eu+)jRcve2slEtsXCR7pyh_cY+wUO&Cd(wlp^4Yww~J> zA?RAp0BlCIy#90c?X~DMjKx@Vz6dsqdjSPJND~o;$eX)buNcysv>}ceF?=0TK6%rW zagsE^!udBs>^-TTNDv0LC$r(23>vj$H>CL;kE-)-FL4_an2sVdh508+EhlT=5IpX%ySa$wxs`)E_e0lJMf9~q zhB?-=<~j1`*>3itIDNvoJln@6yh{g3xA`cdI8mBcK0SqczkV3a)p+wl(n~d3&0R?dmZ!8jLtJk70~ig2#IGV3;*#!(40Xf zF8d5(I)nOKbQW?pHN0M)9?P)Is0puvOZNx-=Njpvoevz$m`g;&PfF2~DhA}(F5TBc zcC6iV04bcWv>`UnNKSjkLLcS*y=Gz6n!FH6D;o?e<5(W7t2O}DOexRN*re+?yV}D8 z=Uyif)f)bb8(A6ZrvlHOYgO8p$O%$AE>=$Olvd4%xnR$h(R!WCpc@tqFZ>O_ z6}qWSV;_ZF+;ZW+f?=TV;_*K$d@OFc0A`?am~^out9erE)`&dMFxP^W4;-9jo(`?B zdM0A1K`SBBwWgId@)ZSM7Hv8M@L`vye}p~nCnZ1GPgqMJ9LRLoLM=)zFjQh}xvuiK(mXiA7z^C@BiCGm)xb9mED`$6Am!>E!KZtKkLL z0L-T0VA-{-pr{LX!kvN)L>Wk;vn!e0BjxzBlS@ahV#N=>TSB364{sb7%or^PXuEQ@ zh;iHY6nAbF#g!8$FKan995YKaDo4YT1ADvZmwYwG=&$3J$=|UBMpx^4g zZdlc)&o38ht}VE~T-kJ}cEkVJ8h614CfN|LXCV0Aqr;UZF5S9Q#fnIgjoPqvgKkeIzg%9b!nYvg&K(!NB^(Tp>EzK)mDpZ-;XJOy3TTT+s;6S`D&~_OhA`;X zM>d*2(oDK>mWe#`^4$-ZJYJ$i3RaAx376&I@Ht7FPv4q%&>dz%BGLiL--bY-@H;q9aLYnoi` z>lpWKi8ffymWs)LfT2nrK67>l5^+yVeW38wrh9am{tP^16Je*bH0kxId78xJji_xz|tzBT>{T-%&UkF^3BZ`GoD$rGZZK+Jwb(WG$q zNXTIW$#iTw-`<68u_0AY&Kd+@qwSj+lmT3}rOkQs)TuQOwkv(p#|im9=$ePJx-B7`o>rSZPa z<5tK*y`6bC3XN0Qf!;HMC7|V@5D2IoGkjbpS#~)@g;`7G=aI=)M?SG!;qpyh!MMoH zXM63%ww2sOB_5(|{P77{Yx+QPEjshFt1V0mp>8anJ>%jHVqe==>*WuV!94a(SoUK5 ztS>KWadG2lS<-ovfvL^YX~9*xn-oR;u9LRl&#PR45OhzSV>wc}l>#`?&SAxO9-Q+w z3R?|ts_V3rqWTt)l`L_^SiUaAcs+|vP04~=k-ot+b=QT`oC|S&1KM1w+#i5pv0>-R z-ui|spmzt>1v46af@;!ZWg^aXnkHZsxzV=&b0`am)98eSK#~!6w)yt@M^LBb&!tfW z?%$#;?`f3^ck*PbF~|-r6qpM>zO%O^oErGbNHiFTDpD$?&H9>E8qyZfPoBjHm#cX0 zfT?<@D1QJ$ZGPOGr$^dYp|UqWf_Q!fKq9|lGVC1-M_Nh|JUk|qUvk6hV|A#q8D1M4 z&LlZ1e!tgXU_9uq70J>t-E?Y=+r|kvaODYoqH(vCEuh;%L1{i-aM`X%g>Dr1#9$jq z75*?H$8h%=EjdA8Mpad=*;OqBL1mq$*0Dh?XzIKo1@H85huQ&55^prG%XbAT(WI=< zhXAjw34H9Cb^X&KI`y+F`gJy)-qu$ojZ8J4vNsQBai?A(5uZp}#s}72BoeK_3;nkF zp}iu#;mh7*#H4KZx&uU+EX)G7xA_gCTvNIPU7yrzxdX|BXi_Je@x2mRs_^3jC<-Aw z$l@J7Cny9UaDxWl9%dvO^o^ydSEVeHGuv{YW>ahdwwKiK>LYqo;SG|Gn#*pov6uBz z#G^q60_gHjh!(tQYv3*9GUCX{Eu-@QMX#!mPle`6W-Czu{BD74LTDMj-j^oy>utdE zLmV6H#K`XWt=-KOK?#;Jh3qrJn9DrrM!~!1C-HnSZu%@rw1@?Q^FSw|48|vXsjZK1 ze4rtn2-~=E;F)@wzKsd0kiPGJ-@wy{5ocRR0IP9uo{;GVJCxdUW|0kQb^_-Fk087Q z#$d!{0glh&TWW)w(x?y2DZ^*@bgJCP+k5!Kl3zjK96J+IT=#=?Gb@PMT*~8HO$J-9UP@!w30ldS!)NvA+ z9%9LaEB+-H1Nsg7NqJ1>Xa;V1Uo2aE=ATJMl&mK8wgNVjr0^;#gpO8#=A5-rnMdY( zu0N`&z-7EG2@EHGFUaGD&P?zw5Cs27{DWdNC2Is`Upjb@(iKb2(TSyXc2QUP{Wrgb zFuLMBr>=3GVD#AsaEUR;nG7xQEd`5q=r-R@v)ma3jPog{*C&WrGOr`ssyrDJ4~v!$ z5?dnZLkyoj#oy^;3j!QiTU~o0d8RR{-=Ar5oTNE? zJ$8~`F_&Jhahpfvv+32KSxCEh9qL_E$rL3icH0Ixtuqn`~cJG}?Un7<2dlRnL z?8N8w*x;kl0~_Pq7YG=Is2#5d3%?1r+Z_5P*K_3V_OO(( zgl?1dDoaR*bC}D3FsMi%(>yH0jZ#O0@}l0E!BA4lkTg{;lVuvqR@q>yrh2l?&dexqai3M6SgpS^dyRH?$T z!-#5TyF_2a{91ca2${*E&L^Do%@Pyf&2G3K^*EWCF{%a&!O3=b?d|F@%B=h4jgD!R z_-@7W?C_#=mi(|9P`uE@6!&hQ9`ER5h~9V2Uh*?vXdJuV45H1AFb%m!FBLa@nIgPj zgQ4!gzT^8oxv&&&LzzJb0*F(pSE8@@!{6>xn%0H5wbkGh&9G3pF8ne0v z>cRn1=3UdFhoiFw|KGgRKA#gnVffo<_k3m6(O5Sti z%aEYsZwCdm|8P)XW&Hc7YK}LC{<+O6C5x>#BFq>(G=c9~s z5tAO{Hp3(_s02Na47M<7bZP`PQa7_-URh`!tN>%JtsWF7G8?5nq_bZPvH-X?*tsuk z>XGA~*-W4aO({ar)AK{e>BF<4W=ER}Jl^y2EI{>EHoN`o5J%U=`uT8d-n#XC--1o2 z+5WOxRd?09HnvjMa|dzF|EO$@c20)(@-o}{KK0qN#^L(#sUCo%YJJk-u`wj8^VIQC zS7RKBG-439S{b{VEZ3KT$cF}`EI*cM3q>!C7)i>P3n49v>TBttDLyF_a6TsHJ8+@( zLqTCo+ZJDfoRvZWl7|Tt(JY4C!CvtkhcE)7z>3GxK^%Uppo9;>R@z4N1=T!ZV@O;O zmzwN#$V3T+ufX_21)407Uga%Nvj`053XGL!s6d0!#>hv?qQ+CwjI8q@3L3&kx_UDnUm#yMkt@9BXJ>=1 z`tVkK4oxv>D+7YZxIBH*lsyhAqW@iYTpwV(34Q3LsKm?pOY`P=L2zTY1Feu+Q%nRMCXbhVP*~FV7lm1P`e+#=1N2j9 z)>lM`rYOhgj5R3x<$ZA%nW4ZMh+@sIaA5MgZ5LHM*2Dv&iuVULT)W_!wwM_8LqJG& zjHbv`-ZKDM%Rw6GkRL!YUV$$JrGS`M#in8m*e zK#DMG{@CDR52_uH(@GPPv8usPuqD{9KWw`XGa4sq;3XJI-5JLGT3KBvtwj((vMiNg zKPEQK9{0gD{HsJ}_mpBO)1*H6VTQBtnB%JTCsr|`ISXu1Nw-Pr0#aWDVm*rrWBP2_ zna&)VDtr>Pe*4{kfjv4(jw+PvvR+yJ^vV!V+O#h+<1%R7n}WGep7^%Ki;Pl*lz{4O z18L;!w497sOmb{Z<`V76gkpG)tS{vDVa^3P8uc+2iQIIUWn6JYR#K=Ya42H3Bpu}Y zl5;{dzM@37kUSu=wiJiJff9%zM0S1~Km{_a6|aWmbIo9EWk{+>iR|HC7Y)o%c*-3vr48&|>#xF!!$y6HCv&d&j*2E471-LR>-7s0Lq>QSDW;&cP zo=a#65{#cwH~X1FlG0 z7ld_8yUQF+!zBrllZbNO8!sJ>Oh6fChCJGiH9?ga4If)Bsp)~RJ{Ss#UvFOMYm3^q z7@`1N1zeEjh@#!YPBAO+Ir8(rw~V5Y92UDs;FR9zKO}AUA<--ZpC`6abW?B1uslp<{LD#PGi?1@?eT*ko$n88>5-Pc6ZYcgllySM8(cKCfeqx zVKo6hx4%w*a?zKXq2iyL-i6}-b>V{tN9dsSf=2#cZ)%o)nYwE`|27sY3sJ7ta^g2} zy1eR~A$Qaf`$~!r!cL25hQ^u(eBj4~=4|j=PGw zy$Pp2meuIUrfFe9?{SCe8P7kawo`7(=w#2B8F)|utNE>b-!#f%{K3p?7cFT7BiFnG zy!GL-Ys&SQ~DZ#Lp|wv zu4Bse>&pa7>VC6UFW_vNAr&>PPP*%NnY@rD6VZBYmmqF98+0;k*zWMoIAgq>Mb=$C z$%D1-S2ZT{Csh-8H8{bKDm3kn{2n7^&gR!AJl7{y*Q>erT|C!sK6<-|J#Wk5vyRf; zjM~sHtgY2}w`@ICX9651ra$SdUOxdo^FZyqC3hB##X}JM>PF6c!5eVY-gxTnx|Xg- zzJ}n5_BC3_h9t^qQ#eGiN9|*8sz19kQjyc<=#t&Jf76wn);&2bZzf&7$A(u1Ur6oz z*5eAhJBf@J66d_o1A!xgpdCr?*W6~*P0x?c-1p@E4+|Z%Mu;a(9UUi6ZX*DrB|!7W zlgrYR^}^G=(B+8au0Q0xN$~xE`~Ac1jC1lVm-99oe#pl*+V&YF8_z+$({J@lM@;UP zr$YCSjGl*h3!XqY@F}|eZ9EApocF%mxZYbfk`$Gzqq4e**w@W!U^ui12mH#4$DHe32>b&$y z=|B1pNf9HseNzfe=hWwO#4gGKVmigY2(++(eE~72>+RlpEtli zZTxy+`F_p%x%i0wp^cxHu0M_ZT7-TN1o6#3GxERF?$=84y`24AIaEOZt+>fag1>+e S2ng}zWAnll=cE4q*Z%=m_H%py literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWBinaryQuantizedCodec-cuvs-26.08.0.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWBinaryQuantizedCodec-cuvs-26.08.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..3f64f576008485159e7eb6b4eebabf7c8c4eb556 GIT binary patch literal 14327 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 z�O;P<6Lxj`))|MEXIcT7a}N9ErgM=WC{jFt#n9PbedN$~ zH8rleqZ=PajS{W-m&o+e&W=x>bvtU7uXaIfmjO9NxGauLvPqN=HZ1lfg9Q6CAj>e|e#x_oldQ87A zYFyzD!HnXsBgFZiA|F%M9ROep;0J^*M%V?3svp52pu{OU@P(C&jbU!``y>+=7aK(b zWeq7?Uk?N4DisSuO$l=~D^nF|9W`4Y7e5)>13#)C>`N@T03gb&ST1nUSHSQRrbG#| zxUg3T`1ftui~xTMc#o^&cjUkzs6hYH<^5F~_ls!y`}#wrColDP%wI)Tf5)V}!+&F( z{)zdkQ0ng(e~>>hZvVvmRi*TI%*CG=8|!~!{;IC|rC<8jDtWJy-)g3RLjKB8{@o4f z`!Bph-bFb7#Qc>+{5u8-_fO2fh4xpr?-#xJuT}VeF(&_X^w*a5cSj6(e>(cNefw(z z`qgIswS1X>VgA|r{^|3tUE}X~u=mFL5Bz)6`X~OcC)3~YasQ5g?==6!|MeL8JKl=z z-~7LyX8*+hwNn3%fBO%3YomYS|5`7<>iJ)5mHn^PEiVQ3`<5U8Apl9gXIBcxuW$Yz D>9;ln literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWCodec-cuvs-26.08.0.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWCodec-cuvs-26.08.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..8e75d3c622a06bf85eab16cd397919c278acd090 GIT binary patch literal 15868 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 T6bAZLCFcr4V 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 literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWCodec.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.2-Lucene101AcceleratedHNSWCodec.zip new file mode 100644 index 0000000000000000000000000000000000000000..6e86f693887e96c7e736e3be870feaceb186d445 GIT binary patch literal 15619 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 literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.3-Lucene103AcceleratedHNSWBinaryQuantizedCodec.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.3-Lucene103AcceleratedHNSWBinaryQuantizedCodec.zip new file mode 100644 index 0000000000000000000000000000000000000000..109f8bfb586460ca458e2b6cca334bb45263e68b GIT binary patch literal 15016 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 R8~_}E_=mboS$_TU{{heIKX?ED literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.3-Lucene103AcceleratedHNSWScalarQuantizedCodec.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.3-Lucene103AcceleratedHNSWScalarQuantizedCodec.zip new file mode 100644 index 0000000000000000000000000000000000000000..661d5bde3290c98867e99e52b084bb47ecee2ee7 GIT binary patch literal 17629 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 literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.4-Lucene104AcceleratedHNSWBinaryQuantizedCodec.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.4-Lucene104AcceleratedHNSWBinaryQuantizedCodec.zip new file mode 100644 index 0000000000000000000000000000000000000000..ab1e87beb471afd4d4af89a15960015443eda716 GIT binary patch literal 14986 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%0A2F<@_q-x^S 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 literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.5-Lucene104AcceleratedHNSWBinaryQuantizedCodec.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.5-Lucene104AcceleratedHNSWBinaryQuantizedCodec.zip new file mode 100644 index 0000000000000000000000000000000000000000..8fcc0b343e2887e42ce0eeb3a2fb85baf6cf2982 GIT binary patch literal 15013 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 literal 0 HcmV?d00001 diff --git a/java/cuvs-lucene/src/test/resources/backcompat/10.5-Lucene104AcceleratedHNSWCodec.zip b/java/cuvs-lucene/src/test/resources/backcompat/10.5-Lucene104AcceleratedHNSWCodec.zip new file mode 100644 index 0000000000000000000000000000000000000000..14d5803584c94851c0f413e892c14446c6364bba GIT binary patch literal 15571 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[-][-cuvs-].zip`. They all +hold the same 200 documents (see `TestBackCompatIndices#writeIndex`). Every module reads the zips +written on its own Lucene release and on all earlier ones. + +## Written by this repository + +`-.zip`: written by the `lucene-X.Y` module with each codec from `CuVSCodecs` except the +GPU search one. To add them for a new Lucene release, see `MAINTAINING.md` step 6: + +```bash +mvn -pl lucene-10.X test -Dtest=TestBackCompatIndices -Dtests.method=testCreateIndices \ + -Dtests.cuvs.createBackCompatIndices=$PWD/src/test/resources/backcompat +``` + +## Written by released cuvs-lucene versions + +`-...-cuvs-.zip`: written by the jars of a cuvs-lucene release published to Maven +Central, with `src/test/backcompat/generate-released-indices.sh `. They catch changes that make +indexes written by released code unreadable. Releases up to 26.08 were a single +`com.nvidia.cuvs.lucene:cuvs-lucene` artifact, built for Lucene 10.2. + +| Release | Written with | Notes | +| --- | --- | --- | +| 26.08.0 | each of its codecs, except the GPU search codec | built on the GPU | +| 25.12.0 | Lucene's default codec, with `Lucene99AcceleratedHNSWVectorsFormat` per field (the release registered no codecs; this is how Solr uses it) | built by the release's CPU fallback: its cuvs-java cannot load against libcuvs 26.06 or later, whose CUDA runtime is linked statically | From 8bf9ca157dbfe04ba5f4075cdadf3fd1b032557b Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Wed, 30 Sep 2026 10:04:07 -1000 Subject: [PATCH 02/15] Bring back LuceneProvider to ease migration for Lucene 10.2 users and improve robustness of builds. --- ci/test_lucene_prebuilt.sh | 4 +- java/cuvs-lucene/MAINTAINING.md | 4 +- java/cuvs-lucene/build.sh | 6 +- .../nvidia/cuvs/lucene/LuceneProvider.java | 319 ++++++++++++++++++ 4 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java diff --git a/ci/test_lucene_prebuilt.sh b/ci/test_lucene_prebuilt.sh index 1a0800d29b..df81928e61 100755 --- a/ci/test_lucene_prebuilt.sh +++ b/ci/test_lucene_prebuilt.sh @@ -103,9 +103,9 @@ rapids-logger "Run cuvs-lucene tests against the amd64-built classes" # -Dskip.compile activates the pom's "skip-compile" profile, which disables all # compilation for this run, forcing test to use amd64-compiled jar instead of -# local code. +# local code. --fail-at-end tests every module even if an earlier one fails. pushd java/cuvs-lucene -mvn --batch-mode test -Dskip.compile=true +mvn --batch-mode --fail-at-end test -Dskip.compile=true popd rapids-logger "Test script exiting with value: $EXITCODE" diff --git a/java/cuvs-lucene/MAINTAINING.md b/java/cuvs-lucene/MAINTAINING.md index 427014c95c..01a6eaa4d5 100644 --- a/java/cuvs-lucene/MAINTAINING.md +++ b/java/cuvs-lucene/MAINTAINING.md @@ -238,4 +238,6 @@ artifacts. Record the new zips in `src/test/resources/backcompat/README.md`. must read indexes written with it. 4. Update the documentation listed in step 8 above. 5. When dropping 10.2, the relocation in `relocation/pom.xml` would point to an artifact that is no longer - published: remove the `relocation` module (and stop publishing it) at that point. + published: remove the `relocation` module (and stop publishing it) at that point. The deprecated + `LuceneProvider` in `lucene-10.2/src/main/java`, kept only for callers of the old artifact, goes + with the module. diff --git a/java/cuvs-lucene/build.sh b/java/cuvs-lucene/build.sh index 3418b2f600..adcce00ba5 100755 --- a/java/cuvs-lucene/build.sh +++ b/java/cuvs-lucene/build.sh @@ -33,9 +33,11 @@ if [ -d "${CUVS_LIB_DIR}" ]; then export LD_LIBRARY_PATH="${CUVS_LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" fi -MAVEN_INSTALL_ARGS=() +# A test failure in one module doesn't stop the others: every module is tested and reported, +# and the build still fails at the end. +MAVEN_INSTALL_ARGS=("--fail-at-end") if ! hasArg --run-java-tests; then - MAVEN_INSTALL_ARGS=("-DskipTests") + MAVEN_INSTALL_ARGS+=("-DskipTests") fi cd "${LUCENE_DIR}" diff --git a/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java new file mode 100644 index 0000000000..c02091a3ce --- /dev/null +++ b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java @@ -0,0 +1,319 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.hnsw.FlatVectorsReader; +import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; +import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.index.SegmentWriteState; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.search.TaskExecutor; + +/** + * Dynamically loads Lucene format, reader, and writer classes with a fallback mechanism. + * + *

Ramains unchanged, for source compatibility, only in {@code cuvs-lucene-10.2}; the artifacts for + * later Lucene releases do not have it. To migrate off it: + * + *

+ * + * @since 25.12 + * @deprecated only available with Lucene 10.2 and will be removed together with it; see above for + * replacements. + */ +@Deprecated(since = "26.12", forRemoval = true) +public class LuceneProvider { + + static final Logger log = Logger.getLogger(LuceneProvider.class.getName()); + + private static final String BASE = "org.apache.lucene."; + private static String codecs = "codecs.lucene."; + private static String fallbackCodecs = "backward_codecs.lucene."; + + private static String luceneFlatVectorsFormat = + BASE + codecs + "LuceneFlatVectorsFormat"; + private static String luceneFlatVectorsFormatFallback = + BASE + fallbackCodecs + "LuceneFlatVectorsFormat"; + + private static String luceneHnswVectorsFormat = + BASE + codecs + "LuceneHnswVectorsFormat"; + private static String luceneHnswVectorsFormatFallback = + BASE + fallbackCodecs + "LuceneHnswVectorsFormat"; + + private static String luceneHnswVectorsReader = + BASE + codecs + "LuceneHnswVectorsReader"; + private static String luceneHnswVectorsReaderFallback = + BASE + fallbackCodecs + "LuceneHnswVectorsReader"; + + private static String luceneHnswVectorsWriter = + BASE + codecs + "LuceneHnswVectorsWriter"; + private static String luceneHnswVectorsWriterFallback = + BASE + fallbackCodecs + "LuceneHnswVectorsWriter"; + + private static String luceneBinaryQuantizedVectorsFormat = + BASE + codecs + "LuceneBinaryQuantizedVectorsFormat"; + private static String luceneBinaryQuantizedVectorsFormatFallback = + BASE + fallbackCodecs + "LuceneBinaryQuantizedVectorsFormat"; + + private static String luceneHnswBinaryQuantizedVectorsFormat = + BASE + codecs + "LuceneHnswBinaryQuantizedVectorsFormat"; + private static String luceneHnswBinaryQuantizedVectorsFormatFallback = + BASE + fallbackCodecs + "LuceneHnswBinaryQuantizedVectorsFormat"; + + private static String luceneScalarQuantizedVectorsFormat = + BASE + codecs + "LuceneScalarQuantizedVectorsFormat"; + private static String luceneScalarQuantizedVectorsFormatFallback = + BASE + fallbackCodecs + "LuceneScalarQuantizedVectorsFormat"; + + private static String luceneHnswScalarQuantizedVectorsFormat = + BASE + codecs + "LuceneHnswScalarQuantizedVectorsFormat"; + private static String luceneHnswScalarQuantizedVectorsFormatFallback = + BASE + fallbackCodecs + "LuceneHnswScalarQuantizedVectorsFormat"; + + private static String luceneCodec = BASE + codecs + "LuceneCodec"; + private static String luceneCodecFallback = BASE + fallbackCodecs + "LuceneCodec"; + + private static LuceneProvider instance; + + private static MethodHandles.Lookup lookup = MethodHandles.lookup(); + + private Class flatVectorsFormat; + private Class hnswVectorsFormat; + private Class hnswVectorsReader; + private Class hnswVectorsWriter; + private Class binaryQuantizedVectorsFormat; + private Class hnswBinaryQuantizedVectorsFormat; + private Class scalarQuantizedVectorsFormat; + private Class hnswScalarQuantizedVectorsFormat; + + public static LuceneProvider getInstance(String version) throws ClassNotFoundException { + if (instance == null) { + instance = new LuceneProvider(version); + } + return instance; + } + + private LuceneProvider(String version) throws ClassNotFoundException { + flatVectorsFormat = + loadClass( + setVersion(luceneFlatVectorsFormat, version), + setVersion(luceneFlatVectorsFormatFallback, version)); + hnswVectorsFormat = + loadClass( + setVersion(luceneHnswVectorsFormat, version), + setVersion(luceneHnswVectorsFormatFallback, version)); + hnswVectorsReader = + loadClass( + setVersion(luceneHnswVectorsReader, version), + setVersion(luceneHnswVectorsReaderFallback, version)); + hnswVectorsWriter = + loadClass( + setVersion(luceneHnswVectorsWriter, version), + setVersion(luceneHnswVectorsWriterFallback, version)); + scalarQuantizedVectorsFormat = + loadClass( + setVersion(luceneScalarQuantizedVectorsFormat, version), + setVersion(luceneScalarQuantizedVectorsFormatFallback, version)); + + hnswScalarQuantizedVectorsFormat = + loadClass( + setVersion(luceneHnswScalarQuantizedVectorsFormat, version), + setVersion(luceneHnswScalarQuantizedVectorsFormatFallback, version)); + + // TODO: Find a better way if possible, but as a separate initiative. + if ("102".equals(version)) { + binaryQuantizedVectorsFormat = + loadClass( + setVersion(luceneBinaryQuantizedVectorsFormat, version), + setVersion(luceneBinaryQuantizedVectorsFormatFallback, version)); + hnswBinaryQuantizedVectorsFormat = + loadClass( + setVersion(luceneHnswBinaryQuantizedVectorsFormat, version), + setVersion(luceneHnswBinaryQuantizedVectorsFormatFallback, version)); + } + } + + private static String setVersion(String pkg, String version) { + return pkg.replaceAll("", version); + } + + private static Class loadClass(String defaultClassName, String fallbackClassName) + throws ClassNotFoundException { + try { + return Class.forName(defaultClassName); + } catch (ClassNotFoundException e) { + // Load class from fallback package. + try { + return Class.forName(fallbackClassName); + } catch (ClassNotFoundException e1) { + // Should not reach here. + log.log(Level.SEVERE, "Unable to load class: " + fallbackClassName); + throw e1; + } + } + } + + public static Codec getCodec(String version) + throws ClassNotFoundException, + NoSuchMethodException, + SecurityException, + InstantiationException, + IllegalAccessException, + IllegalArgumentException, + InvocationTargetException { + Class codecClass = + loadClass(setVersion(luceneCodec, version), setVersion(luceneCodecFallback, version)); + Constructor codecClassConstructor = codecClass.getConstructor(); + return (Codec) codecClassConstructor.newInstance(); + } + + public FlatVectorsFormat getLuceneFlatVectorsFormatInstance(FlatVectorsScorer scorer) + throws Exception { + try { + Constructor luceneFlatVectorsFormatConstructor = + flatVectorsFormat.getConstructor(FlatVectorsScorer.class); + return (FlatVectorsFormat) luceneFlatVectorsFormatConstructor.newInstance(scorer); + } catch (Exception e) { + log.log(Level.SEVERE, "Unable to initialize LuceneFlatVectorsFormat: " + e.getMessage()); + throw e; + } + } + + public KnnVectorsReader getLuceneHnswVectorsReaderInstance( + SegmentReadState state, FlatVectorsReader reader) throws Exception { + try { + Constructor luceneHnswVectorsReaderConstructor = + hnswVectorsReader.getConstructor(SegmentReadState.class, FlatVectorsReader.class); + return (KnnVectorsReader) luceneHnswVectorsReaderConstructor.newInstance(state, reader); + } catch (Exception e) { + log.log(Level.SEVERE, "Unable to initialize LuceneHnswVectorsReader: " + e.getMessage()); + throw e; + } + } + + public KnnVectorsWriter getLuceneHnswVectorsWriterInstance( + SegmentWriteState state, + int maxConn, + int beamWidth, + FlatVectorsWriter writer, + int numMergeWorkers, + TaskExecutor executor) + throws Exception { + try { + Constructor luceneHnswVectorsWriterConstructor = + hnswVectorsWriter.getConstructor( + SegmentWriteState.class, + Integer.TYPE, + Integer.TYPE, + FlatVectorsWriter.class, + Integer.TYPE, + TaskExecutor.class); + return (KnnVectorsWriter) + luceneHnswVectorsWriterConstructor.newInstance( + state, maxConn, beamWidth, writer, numMergeWorkers, executor); + } catch (Exception e) { + log.log(Level.SEVERE, "Unable to initialize LuceneHnswVectorsWriter: " + e.getMessage()); + throw e; + } + } + + public int getStaticIntParam(String param) throws ReflectiveOperationException { + try { + VarHandle varHandle = lookup.findStaticVarHandle(hnswVectorsFormat, param, Integer.TYPE); + return (int) varHandle.get(); + } catch (NoSuchFieldException | IllegalAccessException e) { + log.log(Level.SEVERE, "Unable to get " + param + ": " + e.getMessage()); + throw e; + } + } + + public List getSimilarityFunctions() + throws ReflectiveOperationException { + try { + VarHandle varHandle = + lookup.findStaticVarHandle(hnswVectorsReader, "SIMILARITY_FUNCTIONS", List.class); + return (List) varHandle.get(); + } catch (NoSuchFieldException | IllegalAccessException e) { + log.log(Level.SEVERE, "Unable to get SIMILARITY_FUNCTIONS: " + e.getMessage()); + throw e; + } + } + + public FlatVectorsFormat getluceneBinaryQuantizedVectorsFormatInstance() throws Exception { + try { + Constructor luceneBinaryQuantizedVectorsFormatConstructor = + binaryQuantizedVectorsFormat.getConstructor(); + return (FlatVectorsFormat) luceneBinaryQuantizedVectorsFormatConstructor.newInstance(); + } catch (Exception e) { + log.log( + Level.SEVERE, + "Unable to initialize LuceneBinaryQuantizedVectorsFormat: " + e.getMessage()); + throw e; + } + } + + public FlatVectorsFormat getLuceneHnswBinaryQuantizedVectorsFormatInstance( + int maxConn, int beamWidth) throws Exception { + try { + Constructor luceneHnswBinaryQuantizedVectorsFormatConstructor = + hnswBinaryQuantizedVectorsFormat.getConstructor(Integer.TYPE, Integer.TYPE); + return (FlatVectorsFormat) + luceneHnswBinaryQuantizedVectorsFormatConstructor.newInstance(maxConn, beamWidth); + } catch (Exception e) { + log.log( + Level.SEVERE, + "Unable to initialize LuceneBinaryQuantizedVectorsFormat: " + e.getMessage()); + throw e; + } + } + + public FlatVectorsFormat getLuceneScalarQuantizedVectorsFormatInstance() throws Exception { + try { + Constructor luceneScalarQuantizedVectorsFormatConstructor = + scalarQuantizedVectorsFormat.getConstructor(); + return (FlatVectorsFormat) luceneScalarQuantizedVectorsFormatConstructor.newInstance(); + } catch (Exception e) { + log.log( + Level.SEVERE, + "Unable to initialize LuceneScalarQuantizedVectorsFormat: " + e.getMessage()); + throw e; + } + } + + public FlatVectorsFormat getLuceneHnswScalarQuantizedVectorsFormatInstance( + int beamWidth, int maxConn) throws Exception { + try { + Constructor luceneHnswScalarQuantizedVectorsFormatConstructor = + hnswScalarQuantizedVectorsFormat.getConstructor(Integer.TYPE, Integer.TYPE); + return (FlatVectorsFormat) + luceneHnswScalarQuantizedVectorsFormatConstructor.newInstance(beamWidth, maxConn); + } catch (Exception e) { + log.log( + Level.SEVERE, + "Unable to initialize LuceneHnswScalarQuantizedVectorsFormat: " + e.getMessage()); + throw e; + } + } +} From fc0d601ec90a134b6f2749e8ab1987e31871bfd3 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Wed, 30 Sep 2026 10:38:04 -1000 Subject: [PATCH 03/15] Make sure that cuvs-lucene fallbacks gracefully in JDK 21 --- .github/workflows/pr.yaml | 21 ++++++++ ci/test_lucene_jdk21.sh | 86 +++++++++++++++++++++++++++++++++ dependencies.yaml | 11 +++++ java/cuvs-lucene/MAINTAINING.md | 6 +-- 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100755 ci/test_lucene_jdk21.sh diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 7faa9b52d9..4c2a81677b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -31,6 +31,7 @@ jobs: - conda-java-tests-other-arch - conda-lucene-build-and-tests - conda-lucene-tests-other-arch + - conda-lucene-tests-jdk21 - rust-build-matrix - rust-build - go-build-matrix @@ -751,6 +752,26 @@ jobs: arch: "${{ matrix.ARCH }}" container_image: "rapidsai/ci-conda:26.10-cuda${{ matrix.CUDA_VER }}-${{ matrix.LINUX_VER }}-py${{ matrix.PY_VER }}" script: "ci/test_lucene_prebuilt.sh ${{ matrix.ARCH }} cuvs-java-cuda${{ matrix.CUDA_VER }} cuvs-lucene-cuda${{ matrix.CUDA_VER }}" + conda-lucene-tests-jdk21: + # Verifies that cuvs-lucene, compiled with --release 21, loads and works without the GPU on + # JDK 21, the oldest JDK it supports, by running its test suite against the amd64-built + # classes without recompiling -- see ci/test_lucene_jdk21.sh. It runs once, not per CUDA + # version: it uses no GPU, so the artifacts of any entry of the Java job's matrix will do. + needs: [conda-java-build-and-tests-matrix, conda-java-build-and-tests, conda-lucene-build-and-tests] + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + secrets: inherit # zizmor: ignore[secrets-inherit] + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main + with: + build_type: pull-request + node_type: "cpu8" + arch: "amd64" + container_image: "rapidsai/ci-conda:26.12-cuda${{ fromJSON(needs.conda-java-build-and-tests-matrix.outputs.matrix).include[0].CUDA_VER }}-${{ fromJSON(needs.conda-java-build-and-tests-matrix.outputs.matrix).include[0].LINUX_VER }}-py${{ fromJSON(needs.conda-java-build-and-tests-matrix.outputs.matrix).include[0].PY_VER }}" + script: "ci/test_lucene_jdk21.sh cuvs-java-cuda${{ fromJSON(needs.conda-java-build-and-tests-matrix.outputs.matrix).include[0].CUDA_VER }} cuvs-lucene-cuda${{ fromJSON(needs.conda-java-build-and-tests-matrix.outputs.matrix).include[0].CUDA_VER }}" rust-build-matrix: needs: [conda-cpp-build, changed-files] permissions: diff --git a/ci/test_lucene_jdk21.sh b/ci/test_lucene_jdk21.sh new file mode 100755 index 0000000000..b97cf8ef1b --- /dev/null +++ b/ci/test_lucene_jdk21.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +# Runs cuvs-lucene's test suite on JDK 21 against the classes from a prior amd64 build job, +# without recompiling. cuvs-lucene is compiled with --release 21 so that applications on JDK 21 +# (Solr, for example) can load it, but cuvs-java needs JDK 22+ to reach the GPU. On JDK 21 the +# GPU tests skip themselves, and what runs checks that the codecs still load, read existing +# indexes and fall back to the CPU. None of that depends on the GPU or on the Lucene release, so +# this runs on a CPU node and tests only the newest Lucene module. +# +# Takes the names of the cuvs-java and cuvs-lucene artifacts uploaded by the amd64 jobs. + +CUVS_JAVA_ARTIFACT="${1:?Usage: $0 }" +CUVS_LUCENE_ARTIFACT="${2:?Usage: $0 }" + +if [ -e "/opt/conda/etc/profile.d/conda.sh" ]; then + . /opt/conda/etc/profile.d/conda.sh +fi + +rapids-logger "Configuring conda strict channel priority" +conda config --set channel_priority strict + +rapids-logger "Downloading artifacts from previous jobs" +CUVS_JAVA_DIR=$(rapids-download-from-github "${CUVS_JAVA_ARTIFACT}") +CUVS_LUCENE_DIR=$(rapids-download-from-github "${CUVS_LUCENE_ARTIFACT}") + +rapids-logger "Generate JDK 21 testing dependencies" + +ENV_YAML_DIR="$(mktemp -d)" + +rapids-dependency-file-generator \ + --output conda \ + --file-key java_jdk21 \ + --matrix "" | tee "${ENV_YAML_DIR}/env.yaml" + +rapids-mamba-retry env create --yes -f "${ENV_YAML_DIR}/env.yaml" -n java_jdk21 + +# Temporarily allow unbound variables for conda activation. +set +u +conda activate java_jdk21 +set -u + +rapids-print-env +java -version + +rapids-logger "Install the amd64-built cuvs-java artifact into the local Maven repository" + +CUVS_JAVA_POM="${CUVS_JAVA_DIR}/pom.xml" +if [ ! -f "${CUVS_JAVA_POM}" ]; then + echo "Could not find pom.xml in the cuvs-java artifact at ${CUVS_JAVA_DIR}" >&2 + exit 1 +fi + +mapfile -t CUVS_JAVA_JARS < <(find "${CUVS_JAVA_DIR}" -maxdepth 1 -name 'cuvs-java-*.jar' \ + ! -name '*-sources.jar' ! -name '*-javadoc.jar' ! -name '*-tests.jar' ! -name '*-cuda*.jar') +if [ "${#CUVS_JAVA_JARS[@]}" -ne 1 ]; then + echo "Expected exactly one cuvs-java jar in ${CUVS_JAVA_DIR}, found: ${CUVS_JAVA_JARS[*]:-none}" >&2 + exit 1 +fi + +# cd is needed to pick up pom.xml in order to avoid rate limit of main maven repo +pushd java/cuvs-lucene +mvn --batch-mode install:install-file -Dfile="${CUVS_JAVA_JARS[0]}" -DpomFile="${CUVS_JAVA_POM}" +popd + +rapids-logger "Restore the amd64-built cuvs-lucene target/ directories so Maven can run the tests without recompiling" + +rm -rf java/cuvs-lucene/*/target +cp -a "${CUVS_LUCENE_DIR}/." java/cuvs-lucene/ + +EXITCODE=0 +trap "EXITCODE=1" ERR +set +e + +rapids-logger "Run cuvs-lucene tests on JDK 21 against the amd64-built classes" + +# -Dskip.compile disables all compilation, see ci/test_lucene_prebuilt.sh. +pushd java/cuvs-lucene +mvn --batch-mode test -Dskip.compile=true -pl lucene-10.5 +popd + +rapids-logger "Test script exiting with value: $EXITCODE" +exit ${EXITCODE} diff --git a/dependencies.yaml b/dependencies.yaml index c3c3e3c39e..50fc2bcd94 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -129,6 +129,10 @@ files: - depends_on_nccl - java - rapids_build + java_jdk21: + output: none + includes: + - java_jdk21 py_build_libcuvs: output: pyproject pyproject_dir: python/libcuvs @@ -671,6 +675,13 @@ dependencies: - libboost-devel - maven - *openjdk + java_jdk21: + common: + - output_types: conda + packages: + - maven + # The oldest JDK cuvs-lucene supports; cuvs-java needs 22+ to use the GPU. + - openjdk=21.* bench: common: - output_types: [conda, pyproject, requirements] diff --git a/java/cuvs-lucene/MAINTAINING.md b/java/cuvs-lucene/MAINTAINING.md index 01a6eaa4d5..30ac2f01d8 100644 --- a/java/cuvs-lucene/MAINTAINING.md +++ b/java/cuvs-lucene/MAINTAINING.md @@ -46,9 +46,9 @@ Work through the steps in order. Steps 1–3 always apply; the others depend on and the Lucene version in the javadoc. Keep `LUCENE_MAJOR`/`LUCENE_MINOR` compile-time constants: the version check reads them without loading `LuceneCompat`. -Nothing else in the build needs to know about the new module: `ci/release/update-version.sh`, -`build.sh`, the CI workflows (`java/cuvs-lucene/*/target/`) and `ci/test_lucene_prebuilt.sh` -all match `lucene-*`. +`ci/release/update-version.sh`, `build.sh`, the CI workflows (`java/cuvs-lucene/*/target/`) and +`ci/test_lucene_prebuilt.sh` all match `lucene-*`. The one exception is `ci/test_lucene_jdk21.sh`, +which runs only the newest module on JDK 21: point its `-pl lucene-10.latest` at the new module. ### 2. Make it compile From 9daf755236457666d575fbc5f1cbce6e330a9765 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 2 Oct 2026 06:16:23 -1000 Subject: [PATCH 04/15] Fix "No growth is allowed" error when merging on Lucene 10.4+ Lucene 10.4+ sizes the merged graph from the M recorded in a source segment's metadata. If that M falls below maxConn, which happens for small segments where cuVS truncates the graph degree, CPU merges of such segments fail. Always record an M of at least maxConn, as Lucene's own HNSW writer does. --- ...nvidia-cuvs-lucene-acceleratedhnswutils.md | 16 +- ...ne-lucene99acceleratedhnswvectorswriter.md | 10 +- ...leratedhnswbinaryquantizedvectorswriter.md | 10 +- ...leratedhnswscalarquantizedvectorswriter.md | 10 +- .../cuvs/lucene/AcceleratedHNSWUtils.java | 22 ++- .../Lucene99AcceleratedHNSWVectorsWriter.java | 8 +- ...ratedHNSWBinaryQuantizedVectorsWriter.java | 8 +- ...ratedHNSWScalarQuantizedVectorsWriter.java | 8 +- .../lucene/TestSegmentMaxConnConsistency.java | 143 +++++++++++++++--- 9 files changed, 177 insertions(+), 58 deletions(-) diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md index ab57636443..3f599c9e4b 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-acceleratedhnswutils.md @@ -67,7 +67,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWU ### writeMeta ```java -public static void writeMeta( IndexOutput vectorIndex, IndexOutput meta, FieldInfo field, long vectorIndexOffset, long vectorIndexLength, int count, HnswGraph graph, int[][] graphLevelNodeOffsets) throws IOException +public static void writeMeta( IndexOutput vectorIndex, IndexOutput meta, FieldInfo field, long vectorIndexOffset, long vectorIndexLength, int count, HnswGraph graph, int[][] graphLevelNodeOffsets, int maxConn) throws IOException ``` Writes the meta information for the index. @@ -84,6 +84,7 @@ Writes the meta information for the index. | `count` | the count of vectors | | `graph` | instance of HnswGraph | | `graphLevelNodeOffsets` | graph level node offsets | +| `maxConn` | the configured maxConn, which a CPU writer uses when it merges this segment | **Throws** @@ -91,7 +92,7 @@ Writes the meta information for the index. | --- | --- | | `IOException` | I/O Exceptions | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:309`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:310`_ ### printInfoStream @@ -107,12 +108,12 @@ A utility method to print info/debugging messages using InfoStream. | --- | --- | | `msg` | the debugging message to print | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:391`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:397`_ ### writeEmpty ```java -public static void writeEmpty(FieldInfo fieldInfo, IndexOutput op) throws IOException +public static void writeEmpty(FieldInfo fieldInfo, IndexOutput op, int maxConn) throws IOException ``` Writes an empty meta information for the field. @@ -122,6 +123,7 @@ Writes an empty meta information for the field. | Name | Description | | --- | --- | | `fieldInfo` | instance of FieldInfo | +| `maxConn` | the configured maxConn, recorded as the field's M | **Throws** @@ -129,7 +131,7 @@ Writes an empty meta information for the field. | --- | --- | | `IOException` | I/O Exceptions | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:403`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:410`_ ### quantizeFloatVectorsToBinary @@ -152,7 +154,7 @@ Bits are packed: 8 dimensions per byte. A list of byte binary representation for the input vectors -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:416`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:424`_ ### quantizeFloatVectorsToScalar @@ -172,6 +174,6 @@ Scalar quantization. A list of byte scalar representation for the input vectors -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:458`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:466`_ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/AcceleratedHNSWUtils.java:32`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md index 9dcea78384..ecd568ac89 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-lucene99acceleratedhnswvectorswriter.md @@ -57,7 +57,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Build the indexes and writes it to the disk. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:193`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:194`_ ### doMergeOneField @@ -67,7 +67,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Write field for merging. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:281`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:283`_ ### finish @@ -77,7 +77,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Called once at the end before close. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:290`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:292`_ ### close @@ -87,7 +87,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Closes the resources. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:310`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:312`_ ### ramBytesUsed @@ -97,6 +97,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99Accelera Returns the memory usage of this object in bytes. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:320`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:322`_ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsWriter.java:53`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md index 58f7d4cf89..0c59e367c9 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswbinaryquantizedvectorswriter.md @@ -57,7 +57,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Build the indexes and writes it to the disk. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:215`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:216`_ ### doMergeOneField @@ -67,7 +67,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Write field for merging. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:302`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:304`_ ### finish @@ -77,7 +77,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Called once at the end before close. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:333`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:335`_ ### close @@ -87,7 +87,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Closes the resources. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:353`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:355`_ ### ramBytesUsed @@ -97,6 +97,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Returns the memory usage of this object in bytes. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:362`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:364`_ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter.java:57`_ diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md index f8523edb9e..519eda2ce4 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorswriter.md @@ -57,7 +57,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Build the indexes and writes it to the disk. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:231`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:232`_ ### doMergeOneField @@ -67,7 +67,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Write field for merging. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:316`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:318`_ ### finish @@ -77,7 +77,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Called once at the end before close. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:347`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:349`_ ### close @@ -87,7 +87,7 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Closes the resources. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:367`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:369`_ ### ramBytesUsed @@ -97,6 +97,6 @@ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAccelerate Returns the memory usage of this object in bytes. -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:376`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:378`_ _Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsWriter.java:57`_ 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 8160667734..8df8a849d6 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 @@ -305,6 +305,7 @@ public static int[][] writeGraph(GPUBuiltHnswGraph graph, IndexOutput vectorInde * @param count the count of vectors * @param graph instance of HnswGraph * @param graphLevelNodeOffsets graph level node offsets + * @param maxConn the configured maxConn, which a CPU writer uses when it merges this segment * @throws IOException I/O Exceptions */ public static void writeMeta( @@ -315,7 +316,8 @@ public static void writeMeta( long vectorIndexLength, int count, HnswGraph graph, - int[][] graphLevelNodeOffsets) + int[][] graphLevelNodeOffsets, + int maxConn) throws IOException { meta.writeInt(field.number); @@ -325,10 +327,14 @@ public static void writeMeta( meta.writeVLong(vectorIndexLength); meta.writeVInt(field.getVectorDimension()); meta.writeInt(count); - // M = ceil(cagraGraphDegree / 2), derived from the graph being written rather than from a - // caller-supplied degree: graph.maxConn() is the widest layer-0 adjacency row, which is the - // degree cuVS actually built (it may truncate the requested one for small datasets). - meta.writeVInt(graph == null ? 0 : Math.ceilDiv(graph.maxConn(), 2)); + // The reader sizes its arc buffer as M * 2, so M must cover ceil(cagraGraphDegree / 2), taken + // from the graph being written: graph.maxConn() is the widest layer-0 adjacency row, which is + // the degree cuVS actually built (it may truncate the requested one for small datasets). + // M must not fall below maxConn either: on Lucene 10.4+ a CPU merge sizes the merged graph's + // neighbor arrays from the M of its largest source segment but fills them up to 2 * maxConn, + // so a smaller M makes the merge throw. Like Lucene, record maxConn even without a graph. + int graphM = graph == null ? 0 : Math.ceilDiv(graph.maxConn(), 2); + meta.writeVInt(Math.max(maxConn, graphM)); // write graph nodes on each level if (graph == null) { @@ -399,10 +405,12 @@ public static void printInfoStream(InfoStream infoStream, String component, Stri * Writes an empty meta information for the field. * * @param fieldInfo instance of FieldInfo + * @param maxConn the configured maxConn, recorded as the field's M * @throws IOException I/O Exceptions */ - public static void writeEmpty(FieldInfo fieldInfo, IndexOutput op) throws IOException { - writeMeta(null, op, fieldInfo, 0, 0, 0, null, null); + public static void writeEmpty(FieldInfo fieldInfo, IndexOutput op, int maxConn) + throws IOException { + writeMeta(null, op, fieldInfo, 0, 0, 0, null, null, maxConn); } /** 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 7e6a45485f..01c1c4bf43 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 @@ -137,7 +137,7 @@ public KnnFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException */ private void writeFieldInternal(FieldInfo fieldInfo, List vectors) throws IOException { if (vectors.size() == 0) { - writeEmpty(fieldInfo, hnswMeta); + writeEmpty(fieldInfo, hnswMeta, acceleratedHNSWParams.getMaxConn()); return; } if (vectors.size() < 2) { @@ -181,7 +181,8 @@ private void writeFieldInternal(FieldInfo fieldInfo, List vectors) thro vectorIndexLength, size, hnswGraph, - graphLevelNodeOffsets); + graphLevelNodeOffsets, + acceleratedHNSWParams.getMaxConn()); cagraIndex.close(); } catch (Throwable t) { Utils.handleThrowable(t); @@ -256,7 +257,8 @@ private void writeSingleVectorGraph(FieldInfo fieldInfo, List vectors) vectorIndexLength, size, hnswGraph, - graphLevelNodeOffsets); + graphLevelNodeOffsets, + acceleratedHNSWParams.getMaxConn()); } catch (Throwable t) { Utils.handleThrowable(t); } 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 5e8692a12e..9185380764 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 @@ -147,7 +147,7 @@ public KnnFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException */ private void writeFieldInternal(FieldInfo fieldInfo, List vectors) throws IOException { if (vectors.size() == 0) { - writeEmpty(fieldInfo, hnswMeta); + writeEmpty(fieldInfo, hnswMeta, acceleratedHNSWParams.getMaxConn()); return; } @@ -201,7 +201,8 @@ private void writeFieldInternal(FieldInfo fieldInfo, List vectors) throw vectorIndexLength, size, hnswGraph, - graphLevelNodeOffsets); + graphLevelNodeOffsets, + acceleratedHNSWParams.getMaxConn()); cagraIndex.close(); @@ -290,7 +291,8 @@ private void writeSingleVectorGraph(FieldInfo fieldInfo, List vectors) vectorIndexLength, size, hnswGraph, - graphLevelNodeOffsets); + graphLevelNodeOffsets, + acceleratedHNSWParams.getMaxConn()); } catch (Throwable t) { Utils.handleThrowable(t); 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 e6e9eb5d5d..9a0687954e 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 @@ -157,7 +157,7 @@ private static byte[] convertSignedToUnsigned(byte[] signedVector) { */ private void writeFieldInternal(FieldInfo fieldInfo, List vectors) throws IOException { if (vectors.size() == 0) { - writeEmpty(fieldInfo, hnswMeta); + writeEmpty(fieldInfo, hnswMeta, acceleratedHNSWParams.getMaxConn()); return; } @@ -218,7 +218,8 @@ private void writeFieldInternal(FieldInfo fieldInfo, List vectors) throws IOE vectorIndexLength, size, hnswGraph, - graphLevelNodeOffsets); + graphLevelNodeOffsets, + acceleratedHNSWParams.getMaxConn()); cagraIndex.close(); } catch (Throwable t) { @@ -304,7 +305,8 @@ private void writeSingleVectorGraph(FieldInfo fieldInfo, List vectors) vectorIndexLength, size, hnswGraph, - graphLevelNodeOffsets); + graphLevelNodeOffsets, + acceleratedHNSWParams.getMaxConn()); } catch (Throwable t) { Utils.handleThrowable(t); diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java index 47a3f30320..6da158aa03 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestSegmentMaxConnConsistency.java @@ -8,8 +8,10 @@ import static com.nvidia.cuvs.lucene.ThreadLocalCuVSResourcesProvider.isSupported; import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS; +import com.nvidia.cuvs.CagraIndexParams.CagraGraphBuildAlgo; import org.apache.lucene.codecs.KnnVectorsReader; import org.apache.lucene.codecs.hnsw.HnswGraphProvider; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat; import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat; import org.apache.lucene.document.Document; import org.apache.lucene.document.KnnFloatVectorField; @@ -24,23 +26,26 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks; +import org.apache.lucene.tests.util.TestUtil; import org.apache.lucene.util.hnsw.HnswGraph; import org.junit.Test; /** - * Verifies that the HNSW {@code M} recorded in a segment's metadata describes the graph that - * segment actually contains, for every segment an accelerated-HNSW writer can produce. + * Verifies the HNSW {@code M} recorded in a segment's metadata, for every segment an + * accelerated-HNSW writer can produce. * - *

{@code M} is written as {@code ceil(cagraGraphDegree / 2)} and bounds what the reader accepts: - * Lucene sizes its arc buffer as {@code M * 2} and asserts that every stored adjacency row fits. An - * {@code M} taken from configuration rather than from the built graph can understate the graph, - * because cuVS is free to build a degree other than the one requested -- and under the HEURISTIC - * strategy it derives the degree from maxConn and ignores the configured graph degree outright. + *

{@code M} has two lower bounds. It bounds what the reader accepts: Lucene sizes its arc buffer + * as {@code M * 2} and asserts that every stored adjacency row fits, so {@code M} must cover {@code + * ceil(cagraGraphDegree / 2)} of the graph actually built. An {@code M} taken from the configured + * graph degree can understate the graph, because cuVS is free to build a degree other than the one + * requested -- and under the HEURISTIC strategy it derives the degree from maxConn and ignores the + * configured graph degree outright. * - *

The invariant checked here is per-segment self-consistency, not cross-segment equality. - * Segments of the same field legitimately record different values of {@code M}: cuVS truncates the - * CAGRA graph degree to {@code dataset_size - 1} for small datasets, so with maxConn 16 a 20-vector - * segment records {@code M = 10} while a 3000-vector segment records {@code M = 16}. + *

{@code M} must also not fall below maxConn. On Lucene 10.4+ a CPU merge sizes the merged + * graph's neighbor arrays from the {@code M} of its largest source segment but fills them up to + * {@code 2 * maxConn}. cuVS truncates the CAGRA graph degree to {@code dataset_size - 1} for small + * datasets, so the graph alone would give a 20-vector segment {@code M = 10} with maxConn 16, and a + * single-vector segment an even smaller one; merging either throws. * *

A non-default maxConn is used throughout. At stock defaults the configured graph degree (64) * and the degree cuVS derives from maxConn (2 * 32) coincide, so an {@code M} read from the wrong @@ -53,11 +58,11 @@ public class TestSegmentMaxConnConsistency extends LuceneTestCase { private static final int MAX_CONN = 16; /** - * Every segment -- including a degenerate single-vector one -- must record an M consistent with - * its own widest adjacency row. + * Every segment -- including a degenerate single-vector one -- must record an M that covers both + * its own widest adjacency row and maxConn. */ @Test - public void testRecordedMMatchesEachSegmentsGraph() throws Exception { + public void testRecordedMCoversEachSegmentsGraphAndMaxConn() throws Exception { assumeTrue("cuVS not supported", isSupported()); // Sizes span the interesting cases: the single-vector special path, a segment small enough for @@ -100,8 +105,9 @@ public void testRecordedMMatchesEachSegmentsGraph() throws Exception { + recordedM + " but its widest adjacency row holds " + widestRow - + " arcs", - Math.ceilDiv(widestRow, 2), + + " arcs and maxConn is " + + MAX_CONN, + Math.max(MAX_CONN, Math.ceilDiv(widestRow, 2)), recordedM); // The reader sizes its arc buffer as M*2 and asserts every arc count fits, so an M that @@ -145,15 +151,112 @@ public void testSingleVectorSegmentDoesNotUseConfiguredGraphDegree() throws Exce try (DirectoryReader reader = DirectoryReader.open(dir)) { LeafReader leaf = getOnlyLeafReader(reader); HnswGraph graph = graphOf(leaf); - assertEquals( - "single-vector segment must not record M derived from the ignored graphDegree", - Math.ceilDiv(widestAdjacencyRow(graph), 2), - graph.maxConn()); + assertEquals("single-vector segment must record maxConn as M", MAX_CONN, graph.maxConn()); assertNotEquals("M leaked from the configured graphDegree", 256 / 2, graph.maxConn()); } } } + /** + * When the built graph is wider than {@code 2 * maxConn}, the recorded M must follow the graph: + * maxConn is only a lower bound. + */ + @Test + public void testRecordedMCoversGraphWiderThanMaxConn() throws Exception { + assumeTrue("cuVS not supported", isSupported()); + + int graphDegree = 4 * MAX_CONN; + AcceleratedHNSWParams params = + new AcceleratedHNSWParams.Builder() + .withStrategy(AcceleratedHNSWParams.Strategy.CUSTOM) + .withCagraGraphBuildAlgo(CagraGraphBuildAlgo.NN_DESCENT) + .withIntermediateGraphDegree(2 * graphDegree) + .withGraphDegree(graphDegree) + .withMaxConn(MAX_CONN) + .build(); + + try (Directory dir = newDirectory()) { + IndexWriterConfig cfg = new IndexWriterConfig().setCodec(CuVSCodecs.acceleratedHNSW(params)); + try (IndexWriter w = new IndexWriter(dir, cfg)) { + addDocs(w, 3000); + w.forceMerge(1); + } + + try (DirectoryReader reader = DirectoryReader.open(dir)) { + HnswGraph graph = graphOf(getOnlyLeafReader(reader)); + assertEquals(graphDegree, widestAdjacencyRow(graph)); + assertEquals(Math.ceilDiv(graphDegree, 2), graph.maxConn()); + } + } + } + + /** + * A CPU merge of GPU-written segments, which the formats' fallback writers and stock Lucene do, + * must have room for {@code 2 * maxConn} neighbors per node. On Lucene 10.4+ the merged graph is + * sized from the M recorded by its largest source segment, so merging small segments that recorded + * their truncated degree failed with "No growth is allowed". Lucene 10.2 and 10.3 size it from the + * merging writer's own M, so there this test always passes. + * + *

Single-vector segments are left out: their graph stores a placeholder neighbor {@code -1}, + * which a CPU merge rejects on every Lucene release regardless of M. + */ + @Test + public void testCpuMergeOfSmallGpuSegments() throws Exception { + assumeTrue("cuVS not supported", isSupported()); + + // High-dimensional random vectors pass the diversity check often enough to fill a node's + // neighbor array, which a low-dimensional dataset would not. + int dimensions = 256; + // Lucene 10.4+ skips the graph of a segment smaller than about 650 vectors (see + // Lucene99HnswVectorsFormat.HNSW_GRAPH_THRESHOLD), so the merged segment must be larger for the + // merge to build one. + int segments = 50; + int segmentSize = 20; + AcceleratedHNSWParams params = + new AcceleratedHNSWParams.Builder() + .withStrategy(AcceleratedHNSWParams.Strategy.HEURISTIC) + .withMaxConn(MAX_CONN) + .build(); + + try (Directory dir = newDirectory()) { + IndexWriterConfig gpuCfg = + new IndexWriterConfig().setCodec(CuVSCodecs.acceleratedHNSW(params)); + gpuCfg.setMergePolicy(NoMergePolicy.INSTANCE); + try (IndexWriter w = new IndexWriter(dir, gpuCfg)) { + for (int s = 0; s < segments; s++) { + addRandomDocs(w, segmentSize, dimensions); + w.commit(); + } + } + + IndexWriterConfig cpuCfg = + new IndexWriterConfig() + .setCodec( + TestUtil.alwaysKnnVectorsFormat(new Lucene99HnswVectorsFormat(MAX_CONN, 100))); + try (IndexWriter w = new IndexWriter(dir, cpuCfg)) { + w.forceMerge(1); + } + + TestUtil.checkIndex(dir); + try (DirectoryReader reader = DirectoryReader.open(dir)) { + LeafReader leaf = getOnlyLeafReader(reader); + assertEquals(segments * segmentSize, graphOf(leaf).size()); + } + } + } + + private static void addRandomDocs(IndexWriter w, int count, int dimensions) throws Exception { + for (int i = 0; i < count; i++) { + float[] vector = new float[dimensions]; + for (int d = 0; d < dimensions; d++) { + vector[d] = random().nextFloat(); + } + Document doc = new Document(); + doc.add(new KnnFloatVectorField(FIELD, vector, VectorSimilarityFunction.EUCLIDEAN)); + w.addDocument(doc); + } + } + private static void addDocs(IndexWriter w, int count) throws Exception { for (int i = 0; i < count; i++) { Document doc = new Document(); From 9fa24663fd59617c0b404d28f2cf984bac8a91b5 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 2 Oct 2026 06:53:39 -1000 Subject: [PATCH 05/15] Don't use optimistic collector starting with 10.3 It will only make the same query to run twice. --- .../lucene/CompatKnnFloatVectorQuery.java | 14 ++++ .../lucene/TestCompatKnnFloatVectorQuery.java | 81 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCompatKnnFloatVectorQuery.java diff --git a/java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java b/java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java index c917eec727..d4bf48f375 100644 --- a/java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java +++ b/java/cuvs-lucene/compat/knn-float-vector-query-103-acceptdocs/src/main/java/com/nvidia/cuvs/lucene/CompatKnnFloatVectorQuery.java @@ -7,11 +7,13 @@ import java.io.IOException; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.AcceptDocs; +import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.KnnCollector; import org.apache.lucene.search.KnnFloatVectorQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.knn.KnnCollectorManager; +import org.apache.lucene.search.knn.TopKnnCollectorManager; /** * A {@link KnnFloatVectorQuery} whose per-segment search has the same signature on every Lucene @@ -27,6 +29,18 @@ abstract class CompatKnnFloatVectorQuery extends KnnFloatVectorQuery { /** Returns the collector for searching one segment, given its visit budget. */ abstract KnnCollector newPerLeafCollector(int visitedLimit); + /** + * Returns a manager that is not optimistic. Since Lucene 10.3, {@code rewrite} searches again, + * expecting more hits, every segment whose hits all beat the global top-k when the manager is + * optimistic. {@link #approximateSearch} does not use the manager, so that second search would + * only repeat the first one. + */ + @Override + protected KnnCollectorManager getKnnCollectorManager(int k, IndexSearcher searcher) { + // A method reference does not override isOptimistic(), which defaults to false. + return new TopKnnCollectorManager(k, searcher)::newCollector; + } + @Override protected TopDocs approximateSearch( LeafReaderContext context, diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCompatKnnFloatVectorQuery.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCompatKnnFloatVectorQuery.java new file mode 100644 index 0000000000..2dc5e3b856 --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCompatKnnFloatVectorQuery.java @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.KnnFloatVectorField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.NoMergePolicy; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.search.TopKnnCollector; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.junit.Test; + +/** Tests the per-segment search of {@link CompatKnnFloatVectorQuery}, which needs no GPU. */ +public class TestCompatKnnFloatVectorQuery extends LuceneTestCase { + + private static final String FIELD = "f"; + + /** + * Each segment must be searched once. Since Lucene 10.3, an optimistic collector manager makes + * {@code rewrite} search again every segment whose hits all beat the global top-k, expecting a + * wider second search; the query ignores the manager, so that search would only repeat the first, + * which for the GPU query is a second identical CAGRA search. + */ + @Test + public void testSearchesEachSegmentOnce() throws Exception { + int k = 5; + try (Directory dir = newDirectory()) { + IndexWriterConfig cfg = new IndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE); + try (IndexWriter w = new IndexWriter(dir, cfg)) { + // One segment holds every nearest neighbor of the query, the other only distant vectors, + // so all of the first segment's hits make the global top-k. + addDocs(w, 0f, 20); + w.commit(); + addDocs(w, 1000f, 20); + w.commit(); + } + + try (DirectoryReader reader = DirectoryReader.open(dir)) { + assertEquals(2, reader.leaves().size()); + CountingQuery query = new CountingQuery(new float[] {0f, 0f}, k); + assertEquals(k, new IndexSearcher(reader).search(query, k).scoreDocs.length); + assertEquals("segment searches", reader.leaves().size(), query.searches.get()); + } + } + } + + private static void addDocs(IndexWriter w, float offset, int count) throws Exception { + for (int i = 0; i < count; i++) { + Document doc = new Document(); + doc.add( + new KnnFloatVectorField( + FIELD, new float[] {offset + i, offset + i}, VectorSimilarityFunction.EUCLIDEAN)); + w.addDocument(doc); + } + } + + /** Counts the per-segment searches. */ + private static final class CountingQuery extends CompatKnnFloatVectorQuery { + + final AtomicInteger searches = new AtomicInteger(); + + CountingQuery(float[] target, int k) { + super(FIELD, target, k, null); + } + + @Override + KnnCollector newPerLeafCollector(int visitedLimit) { + searches.incrementAndGet(); + return new TopKnnCollector(k, visitedLimit); + } + } +} From bdd5c09aff1650dfa528c3acda90d90557f4b72f Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 2 Oct 2026 07:08:17 -1000 Subject: [PATCH 06/15] Remove unecessary tests skips --- .../BaseCuVSKnnVectorsFormatTestCase.java | 7 +- .../lucene/TestQuantizedFallbackWriters.java | 12 +-- .../lucene/TestQuantizedVectorsFormats.java | 81 +++++++++++++++++++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java index 2ba0a40634..83c62e4546 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/BaseCuVSKnnVectorsFormatTestCase.java @@ -30,8 +30,11 @@ protected VectorEncoding randomVectorEncoding() { return VectorEncoding.FLOAT32; } - // BaseKnnVectorsFormatTestCase declares this abstract since Lucene 10.5 and not at all before, so - // it cannot be marked @Override. Lucene's own HNSW formats return false as well. + // Whether the format can rebuild float vectors from its quantized copy once the raw vectors are + // gone. Lucene's suite runs its raw-vector fallback tests only when this returns true; subclasses + // that return true also implement simulateEmptyRawVectors(Directory). + // BaseKnnVectorsFormatTestCase + // declares it abstract since Lucene 10.4 and not at all before, so it cannot be marked @Override. protected boolean supportsFloatVectorFallback() { return false; } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java index 4c85214aff..401119bc8c 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedFallbackWriters.java @@ -45,11 +45,13 @@ public class TestQuantizedFallbackWriters extends LuceneTestCase { @BeforeClass public static void beforeClass() { - assumeTrue("cuVS not supported", isSupported()); - // Makes isSupported() false on this thread, like on a host without a GPU. Close the resources - // isSupported() just created first, so that they do not leak. - closeCuVSResourcesInstance(); - setCuVSResourcesInstance(null); + // Where cuVS is unavailable (no GPU, JDK 21) the writers fall back by themselves. Elsewhere, + // make isSupported() false on this thread, like on a host without a GPU, after closing the + // resources isSupported() just created, so that they do not leak. + if (isSupported()) { + closeCuVSResourcesInstance(); + setCuVSResourcesInstance(null); + } } @AfterClass diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java index 2a1be0b20e..9f4347ee34 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestQuantizedVectorsFormats.java @@ -15,7 +15,9 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.CodecUtil; import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.lucene95.OrdToDocDISIReaderConfiguration; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.KnnFloatVectorField; @@ -27,6 +29,9 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.store.ByteBuffersDirectory; import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; import org.apache.lucene.tests.util.LuceneTestCase.SuppressSysoutChecks; import org.apache.lucene.tests.util.TestUtil; @@ -53,6 +58,82 @@ public static List parameters() { }); } + /** + * Only the scalar-quantized format of Lucene 10.4+ keeps a quantized copy it can rebuild float + * vectors from; the binary-quantized format stores full-precision vectors only. Matched by name, + * because the class does not exist on earlier releases. + */ + @Override + protected boolean supportsFloatVectorFallback() { + return "Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat" + .equals(knnVectorsFormat.getName()); + } + + // The format quantizes to 7 bits (ScalarEncoding.SEVEN_BIT). Since Lucene 10.4; not marked + // @Override, so that it compiles against earlier releases. + protected int getQuantizationBits() { + return 7; + } + + /** + * Empties the raw vectors of the scalar-quantized format's flat storage, as Lucene's own + * TestLucene104ScalarQuantizedVectorsFormat does. Since Lucene 10.4; not marked @Override, so + * that it compiles against earlier releases. + */ + protected void simulateEmptyRawVectors(Directory dir) throws Exception { + for (String file : dir.listAll()) { + if (file.endsWith(".vec")) { + replaceWithEmptyVectorFile(dir, file); + } else if (file.endsWith(".vemf")) { + updateVectorMetadataFile(dir, file); + } + } + } + + /** Replaces a raw vector file with an empty one that has a valid header and footer. */ + private static void replaceWithEmptyVectorFile(Directory dir, String fileName) throws Exception { + byte[] indexHeader; + try (IndexInput in = dir.openInput(fileName, IOContext.DEFAULT)) { + indexHeader = CodecUtil.readIndexHeader(in); + } + dir.deleteFile(fileName); + try (IndexOutput out = dir.createOutput(fileName, IOContext.DEFAULT)) { + out.writeBytes(indexHeader, 0, indexHeader.length); + CodecUtil.writeFooter(out); + } + } + + /** Rewrites the flat vectors' metadata to describe no stored vectors. */ + private static void updateVectorMetadataFile(Directory dir, String fileName) throws Exception { + byte[] indexHeader; + int fieldNumber, vectorEncoding, vectorSimilarityFunction, dimension; + long vectorStartPos; + try (IndexInput in = dir.openInput(fileName, IOContext.DEFAULT)) { + indexHeader = CodecUtil.readIndexHeader(in); + fieldNumber = in.readInt(); + vectorEncoding = in.readInt(); + vectorSimilarityFunction = in.readInt(); + vectorStartPos = in.readVLong(); + in.readVLong(); // the original vector length + dimension = in.readVInt(); + } + dir.deleteFile(fileName); + try (IndexOutput out = dir.createOutput(fileName, IOContext.DEFAULT)) { + out.writeBytes(indexHeader, 0, indexHeader.length); + out.writeInt(fieldNumber); + out.writeInt(vectorEncoding); + out.writeInt(vectorSimilarityFunction); + out.writeVLong(vectorStartPos); + out.writeVLong(0); // no vector data + out.writeVInt(dimension); + out.writeInt(0); // no vectors + // Lucene99FlatVectorsFormat.DIRECT_MONOTONIC_BLOCK_SHIFT, which is package-private. + OrdToDocDISIReaderConfiguration.writeStoredMeta(16, out, null, 0, 0, null); + out.writeInt(-1); // end of fields + CodecUtil.writeFooter(out); + } + } + @Override protected Codec getCodec() { log.log(Level.FINE, "Running tests for: " + knnVectorsFormat.getName()); From e976b1654849ec281199378125386db0c78c9593 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 2 Oct 2026 08:10:07 -1000 Subject: [PATCH 07/15] Make upgrade message less confusing. --- ...leratedhnswscalarquantizedvectorsformat.md | 6 +++--- fern/pages/user_guide/lucene.md | 4 +++- java/cuvs-lucene/README.md | 4 +++- .../com/nvidia/cuvs/lucene/LuceneCompat.java | 7 +++++++ .../com/nvidia/cuvs/lucene/LuceneCompat.java | 7 +++++++ .../com/nvidia/cuvs/lucene/LuceneCompat.java | 7 +++++++ .../com/nvidia/cuvs/lucene/LuceneCompat.java | 7 +++++++ ...ratedHNSWScalarQuantizedVectorsFormat.java | 13 ++++++++++++ ...ratedHNSWScalarQuantizedVectorsFormat.java | 18 ++++++++++++++++ .../nvidia/cuvs/lucene/TestBackCompat.java | 21 +++++++++++++++++++ 10 files changed, 89 insertions(+), 5 deletions(-) diff --git a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md index b1b32c479e..6406a48bef 100644 --- a/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md +++ b/fern/pages/lucene_api/lucene-api-com-nvidia-cuvs-lucene-luceneacceleratedhnswscalarquantizedvectorsformat.md @@ -32,7 +32,7 @@ Initializes `LuceneAcceleratedHNSWScalarQuantizedVectorsFormat` with default val | --- | --- | | `LibraryException` | if the native library fails to load | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:25`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:26`_ ### LuceneAcceleratedHNSWScalarQuantizedVectorsFormat @@ -48,6 +48,6 @@ Initializes `LuceneAcceleratedHNSWScalarQuantizedVectorsFormat` with the given t | --- | --- | | `acceleratedHNSWParams` | An instance of `AcceleratedHNSWParams` | -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:34`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:35`_ -_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:18`_ +_Source: `java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java:19`_ diff --git a/fern/pages/user_guide/lucene.md b/fern/pages/user_guide/lucene.md index 7a5a031426..fe0115ea05 100644 --- a/fern/pages/user_guide/lucene.md +++ b/fern/pages/user_guide/lucene.md @@ -51,7 +51,7 @@ Each codec also wraps the default codec of one Lucene release, and Lucene record The quantized codecs follow the same pattern, for example `Lucene104AcceleratedHNSWScalarQuantizedCodec`. `CuVSCodecs` always returns the codecs that write on the Lucene release in use, so code that creates its codecs through it does not change when the application moves to another Lucene release. -To upgrade Lucene, switch to the matching `cuvs-lucene` artifact. Every artifact also includes the codecs of the earlier Lucene releases it supports, so existing segments stay readable. Those codecs are read-only, like Lucene's own backward codecs: new segments, including those produced by merges, are written with the current codec, and writing with an older one throws `UnsupportedOperationException`. Configuration that names a codec, rather than creating it through `CuVSCodecs`, has to be updated to the new name. +To upgrade Lucene, switch to the matching `cuvs-lucene` artifact. Every artifact also includes the codecs of the earlier Lucene releases it supports, so existing segments stay readable. Those codecs are read-only, like Lucene's own backward codecs: new segments, including those produced by merges, are written with the current codec, and writing with an older one throws `UnsupportedOperationException`. Configuration that names a codec, rather than creating it through `CuVSCodecs`, has to be updated to the new name. The same applies to the per-field vectors formats: from Lucene 10.4 on, `Lucene99AcceleratedHNSWScalarQuantizedVectorsFormat` can only read, because Lucene 10.4 can no longer write the `Lucene99ScalarQuantizedVectorsFormat` it stores its vectors with. Configuration that names it, such as a per-field `knnAlgorithm`, has to switch to `Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat`, which `CuVSCodecs.acceleratedHNSWScalarQuantizedFormat()` returns. ## Quickstart @@ -295,6 +295,8 @@ Install the handle on each search thread before that thread runs its first searc **`UnsupportedOperationException: The ... codec can only read indexes`.** The application writes with the codec of an earlier Lucene release, which the current release can only read. Create the codec through `CuVSCodecs`, or switch to the codec for the current release. +**`UnsupportedOperationException: Lucene99AcceleratedHNSWScalarQuantizedVectorsFormat can only read indexes`.** On Lucene 10.4 and later, the application writes with the scalar-quantized vectors format of earlier releases. Use the format the message names, which `CuVSCodecs.acceleratedHNSWScalarQuantizedFormat()` returns; see [Lucene Versions and Upgrades](#lucene-versions-and-upgrades). + **Native libraries do not match.** A missing or mismatched native cuVS library is caught during resource creation and logged at `WARNING` rather than thrown, so it surfaces as one of the two symptoms above rather than as a load error. Make sure the installed native libraries match the `cuvs-lucene` version; see [cuVS Lucene](/installation/java#cuvs-lucene). **Errors about native access, or indexing never uses the GPU.** `cuvs-lucene` is compiled for Java 21, but `cuvs-java` reaches the GPU through the Panama FFI APIs, which need a JDK 22 or newer runtime on Linux (`amd64` or `aarch64`). On JDK 21, cuVS reports that it is not supported: the accelerated HNSW codecs build graphs on the CPU, and the GPU search codec throws `UnsupportedOperationException`. Run the JVM with `--enable-native-access=ALL-UNNAMED` to suppress native-access warnings, or `--enable-native-access=com.nvidia.cuvs` if you put `cuvs-java` on the module path. diff --git a/java/cuvs-lucene/README.md b/java/cuvs-lucene/README.md index dab9c9b463..dbd6c71cd2 100644 --- a/java/cuvs-lucene/README.md +++ b/java/cuvs-lucene/README.md @@ -26,7 +26,9 @@ Each codec wraps the default codec of one Lucene release, so the codec that writ example `Lucene101AcceleratedHNSWCodec` on Lucene 10.2, `Lucene103AcceleratedHNSWCodec` on 10.3, and `Lucene104AcceleratedHNSWCodec` on 10.4 and 10.5. Codecs of earlier releases stay available so that existing indexes can still be read after an upgrade, but they refuse to write. `CuVSCodecs` always returns the codecs -that write on the Lucene release in use. +that write on the Lucene release in use. The same holds for the vectors format +`Lucene99AcceleratedHNSWScalarQuantizedVectorsFormat`, which only reads from Lucene 10.4 on; per-field +configuration that names it has to switch to `Lucene104AcceleratedHNSWScalarQuantizedVectorsFormat`. For guidance on choosing between them, configuring builds, and tuning GPU resources, see the [Lucene Integration](https://docs.nvidia.com/cuvs/user-guide/lucene) guide. diff --git a/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java index 13f2f0db58..ab91a36cf9 100644 --- a/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java +++ b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -49,6 +49,13 @@ static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { return new Lucene99ScalarQuantizedVectorsFormat(); } + /** + * Whether this Lucene release can write {@link #lucene99ScalarQuantizedFlatFormat()}. Lucene 10.2 still writes it. + */ + static boolean canWriteLucene99ScalarQuantized() { + return true; + } + /** * Returns Lucene's HNSW format over {@link #lucene99ScalarQuantizedFlatFormat()}, which builds * the graph on the CPU. diff --git a/java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java index ee692aaccf..620fb2a8a3 100644 --- a/java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java +++ b/java/cuvs-lucene/lucene-10.3/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -55,6 +55,13 @@ static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { return new Lucene99ScalarQuantizedVectorsFormat(); } + /** + * Whether this Lucene release can write {@link #lucene99ScalarQuantizedFlatFormat()}. Lucene 10.3 still writes it. + */ + static boolean canWriteLucene99ScalarQuantized() { + return true; + } + /** * Returns Lucene's HNSW format over {@link #lucene99ScalarQuantizedFlatFormat()}, which builds * the graph on the CPU. diff --git a/java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java index b07263d09d..b26e0af225 100644 --- a/java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java +++ b/java/cuvs-lucene/lucene-10.4/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -66,6 +66,13 @@ static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { return new Lucene99ScalarQuantizedVectorsFormat(); } + /** + * Whether this Lucene release can write {@link #lucene99ScalarQuantizedFlatFormat()}. Lucene 10.4 moved it to its backward codecs, which only read. + */ + static boolean canWriteLucene99ScalarQuantized() { + return false; + } + /** Lucene 10.4 cannot write {@link #lucene99ScalarQuantizedFlatFormat()}, so this throws. */ static KnnVectorsFormat lucene99HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { throw new UnsupportedOperationException("Old codecs may only be used for reading"); diff --git a/java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java b/java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java index 2871ca0f0a..77d8afa2d1 100644 --- a/java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java +++ b/java/cuvs-lucene/lucene-10.5/src/main/java/com/nvidia/cuvs/lucene/LuceneCompat.java @@ -66,6 +66,13 @@ static FlatVectorsFormat lucene99ScalarQuantizedFlatFormat() { return new Lucene99ScalarQuantizedVectorsFormat(); } + /** + * Whether this Lucene release can write {@link #lucene99ScalarQuantizedFlatFormat()}. Lucene 10.5 moved it to its backward codecs, which only read. + */ + static boolean canWriteLucene99ScalarQuantized() { + return false; + } + /** Lucene 10.5 cannot write {@link #lucene99ScalarQuantizedFlatFormat()}, so this throws. */ static KnnVectorsFormat lucene99HnswScalarQuantizedFormat(AcceleratedHNSWParams params) { throw new UnsupportedOperationException("Old codecs may only be used for reading"); diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java index 896137c634..8ec33f3c82 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/BaseAcceleratedHNSWScalarQuantizedVectorsFormat.java @@ -62,6 +62,10 @@ abstract class BaseAcceleratedHNSWScalarQuantizedVectorsFormat extends KnnVector @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { LuceneVersionGuard.ensureCompatible(); + String readOnlyReason = readOnlyReason(); + if (readOnlyReason != null) { + throw new UnsupportedOperationException(readOnlyReason); + } if (isSupported()) { log.fine("cuVS is supported so using the Lucene99AcceleratedHNSWQuantizedVectorsWriter"); return new LuceneAcceleratedHNSWScalarQuantizedVectorsWriter( @@ -78,6 +82,15 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException } } + /** + * Returns why this format cannot write on the running Lucene release, or {@code null} if it can. + * Checked before writing, so that the error names the format and its replacement rather than + * coming from Lucene's backward codecs. + */ + String readOnlyReason() { + return null; + } + /** * Returns a KnnVectorsReader to read the scalar quantized vectors from the index. */ diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java index acb8f71e91..7f7db17271 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java @@ -5,6 +5,7 @@ package com.nvidia.cuvs.lucene; import com.nvidia.cuvs.LibraryException; +import org.apache.lucene.util.Version; /** * cuVS based Scalar Quantized KnnVectorsFormat for indexing on GPU and searching on the CPU. @@ -40,4 +41,21 @@ public LuceneAcceleratedHNSWScalarQuantizedVectorsFormat( () -> LuceneCompat.lucene99ScalarQuantizedFlatFormat(), params -> LuceneCompat.lucene99HnswScalarQuantizedFormat(params)); } + + @Override + String readOnlyReason() { + if (LuceneCompat.canWriteLucene99ScalarQuantized()) { + return null; + } + String replacement = + LuceneCompat.acceleratedHNSWScalarQuantizedFormat( + new AcceleratedHNSWParams.Builder().build()) + .getName(); + return getName() + + " can only read indexes on Lucene " + + Version.LATEST + + ", which no longer writes Lucene99ScalarQuantizedVectorsFormat. Use " + + replacement + + " (CuVSCodecs.acceleratedHNSWScalarQuantizedFormat) to write."; + } } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java index 12bf5dd909..773f369534 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestBackCompat.java @@ -73,6 +73,27 @@ public void testOlderCodecsAreReadOnly() { } } + @Test + public void testOldScalarQuantizedFormatNamesItsReplacement() { + // Looked up by name, as a per-field configuration such as Solr's does. It stores its vectors + // with Lucene99ScalarQuantizedVectorsFormat, which Lucene 10.4 moved to its backward codecs. + BaseAcceleratedHNSWScalarQuantizedVectorsFormat format = + (BaseAcceleratedHNSWScalarQuantizedVectorsFormat) + KnnVectorsFormat.forName("Lucene99AcceleratedHNSWScalarQuantizedVectorsFormat"); + KnnVectorsFormat replacement = + CuVSCodecs.acceleratedHNSWScalarQuantizedFormat( + new AcceleratedHNSWParams.Builder().build()); + if (replacement.getName().equals(format.getName())) { + // Still the format CuVSCodecs writes with, on Lucene 10.2 and 10.3. + assertNull(format.readOnlyReason()); + return; + } + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> format.fieldsWriter(null)); + assertTrue(e.getMessage(), e.getMessage().contains(format.getName())); + assertTrue(e.getMessage(), e.getMessage().contains(replacement.getName())); + } + @Test public void testCallerChosenDelegateIsNeverReadOnly() { // Lucene101Codec is a read-only backward codec from Lucene 10.3 on, but a codec built with a From b96b668efe336cc5a9be7dd970ce5711eb8d0bcb Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 2 Oct 2026 08:27:42 -1000 Subject: [PATCH 08/15] Docs cleanup --- ci/release/update-version.sh | 4 +++- fern/pages/installation/java.md | 2 +- fern/pages/user_guide/lucene.md | 2 +- java/cuvs-lucene/MAINTAINING.md | 11 ++++++----- java/cuvs-lucene/README.md | 4 +++- .../java/com/nvidia/cuvs/lucene/LuceneProvider.java | 2 +- .../java/com/nvidia/cuvs/lucene/TestBackCompat.java | 2 ++ 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index dde2078f6e..82e6f54a11 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -182,6 +182,8 @@ sed_runner "s|/[[:digit:]]\{2\}\.[[:digit:]]\{2\}\.[[:digit:]]\{1,2\}/|/${NEXT_F # This pattern is deliberately narrow: java/cuvs-lucene/README.md also links to a blog post whose # title contains a release number, and that reference must not be rewritten. -sed_runner "s|[[:digit:]]\{2\}\.[[:digit:]]\{2\}\.[[:digit:]]\{1,2\}|${NEXT_FULL_JAVA_TAG}|g" java/cuvs-lucene/README.md +for FILE in java/cuvs-lucene/README.md fern/pages/installation/java.md; do + sed_runner "s|[[:digit:]]\{2\}\.[[:digit:]]\{2\}\.[[:digit:]]\{1,2\}|${NEXT_FULL_JAVA_TAG}|g" "${FILE}" +done sed_runner "s|target/examples-[\.0-9]*-jar|target/examples-${NEXT_FULL_JAVA_TAG}-jar|g" examples/java/cuvs-lucene/README.md diff --git a/fern/pages/installation/java.md b/fern/pages/installation/java.md index 479a2c36a0..9c255c33c2 100644 --- a/fern/pages/installation/java.md +++ b/fern/pages/installation/java.md @@ -94,7 +94,7 @@ For example, for Lucene 10.5, add the following dependency to your `pom.xml`: ``` -An artifact refuses to load against any other Lucene minor release, with an error naming the artifact to use instead. Releases before 26.12 published a single `cuvs-lucene` artifact, built for Lucene 10.2. Its coordinates are relocated to `cuvs-lucene-10.2`, so Maven builds that only bump its version to 26.12 or later keep working and print a warning; switch to the artifact matching your Lucene version. Build tools other than Maven may not follow the relocation; update the artifact name there. +Against any other Lucene minor release, an artifact's codecs fail when they are used, with an error naming the artifact to use instead, and the problem is logged at `SEVERE` the first time Lucene looks up its codecs. Call `CuVSCodecs.checkLuceneVersion()` at startup to fail early instead. Releases before 26.12 published a single `cuvs-lucene` artifact, built for Lucene 10.2. Its coordinates are relocated to `cuvs-lucene-10.2`, so Maven builds that only bump its version to 26.12 or later keep working and print a warning; switch to the artifact matching your Lucene version. Build tools other than Maven may not follow the relocation; update the artifact name there. The native NVIDIA cuVS libraries are not bundled with the artifact. Install a matching version of `libcuvs` and `libcuvs_c` as described above, and make sure the directory containing them is on `LD_LIBRARY_PATH` before starting the JVM. diff --git a/fern/pages/user_guide/lucene.md b/fern/pages/user_guide/lucene.md index fe0115ea05..2250c66017 100644 --- a/fern/pages/user_guide/lucene.md +++ b/fern/pages/user_guide/lucene.md @@ -39,7 +39,7 @@ The GPU search codec has no such fallback. Its on-disk format has no CPU reader, ## Lucene Versions and Upgrades -Lucene changes its codec APIs between minor releases, so `cuvs-lucene` is published as one artifact per Lucene minor release, `cuvs-lucene-10.2` through `cuvs-lucene-10.5`; see [cuVS Lucene](/installation/java#cuvs-lucene). Use the one that matches your application's Lucene version. An artifact refuses to load against another Lucene minor release. +Lucene changes its codec APIs between minor releases, so `cuvs-lucene` is published as one artifact per Lucene minor release, `cuvs-lucene-10.2` through `cuvs-lucene-10.5`; see [cuVS Lucene](/installation/java#cuvs-lucene). Use the one that matches your application's Lucene version. Against another Lucene minor release, its codecs fail when they are used; see [Troubleshooting](#troubleshooting). Each codec also wraps the default codec of one Lucene release, and Lucene records the codec's name in every segment it writes. The codec that writes therefore changes with Lucene: diff --git a/java/cuvs-lucene/MAINTAINING.md b/java/cuvs-lucene/MAINTAINING.md index 30ac2f01d8..9471f9a1c4 100644 --- a/java/cuvs-lucene/MAINTAINING.md +++ b/java/cuvs-lucene/MAINTAINING.md @@ -157,11 +157,12 @@ and `LuceneCompat.acceleratedHNSWScalarQuantizedFormat` use it. mvn -pl lucene-10.6 -am verify ``` -- New abstract methods or new tests in Lucene's `BaseKnnVectorsFormatTestCase` are handled in the shared - test classes without `@Override`, so that they still compile against older releases. See - `supportsFloatVectorFallback()` and `testWriterByteVectorRamEstimate()` in - `src/test/java/.../TestLucene99AcceleratedHNSWVectorsFormat.java` (and the same in - `TestCuVSVectorsFormat` and `TestQuantizedVectorsFormats`). +- New abstract methods or new tests in Lucene's `BaseKnnVectorsFormatTestCase` are handled once, in + `src/test/java/.../BaseCuVSKnnVectorsFormatTestCase.java`, which every format test extends, and without + `@Override`, so that they still compile against older releases. See `supportsFloatVectorFallback()` + (abstract since Lucene 10.4) and `testWriterByteVectorRamEstimate()` (added in 10.5). Hooks that only + some formats implement go in their test: `TestQuantizedVectorsFormats` implements + `getQuantizationBits()` and `simulateEmptyRawVectors()` for the scalar-quantized format of 10.4+. - A test call whose signature changed goes through `TestLuceneCompat`, in a `compat/` variant (step 2). - `TestBackCompat` fails if a codec from `CuVSCodecs` is not registered, or if an older codec can still write. diff --git a/java/cuvs-lucene/README.md b/java/cuvs-lucene/README.md index dbd6c71cd2..f4e4cbf6a0 100644 --- a/java/cuvs-lucene/README.md +++ b/java/cuvs-lucene/README.md @@ -59,7 +59,9 @@ your `pom.xml`: ``` -An artifact fails to load against another Lucene minor release, with an error that names the artifact to use. +Against another Lucene minor release, an artifact's codecs fail when they are used, with an error that names +the artifact to use; the problem is also logged at `SEVERE` the first time Lucene looks up its codecs. Call +`CuVSCodecs.checkLuceneVersion()` at startup to fail early instead. ### Building from source diff --git a/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java index c02091a3ce..d172aed84d 100644 --- a/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java +++ b/java/cuvs-lucene/lucene-10.2/src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java @@ -26,7 +26,7 @@ /** * Dynamically loads Lucene format, reader, and writer classes with a fallback mechanism. * - *

Ramains unchanged, for source compatibility, only in {@code cuvs-lucene-10.2}; the artifacts for + *

Remains unchanged, for source compatibility, only in {@code cuvs-lucene-10.2}; the artifacts for * later Lucene releases do not have it. To migrate off it: * *