From e3b67f15393b4ccb80a83e0419ff50e0526e3501 Mon Sep 17 00:00:00 2001 From: Shaunak Kapur Date: Thu, 27 Aug 2026 17:40:54 +0000 Subject: [PATCH 1/6] Lucene: add Error propagation regression coverage --- .../java/com/nvidia/cuvs/lucene/Utils.java | 5 ++++- .../lucene/TestUtilsThrowableHandling.java | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java 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 e4a20d2b4d..d402d94f60 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 @@ -28,7 +28,10 @@ public class Utils { static final Logger log = Logger.getLogger(Utils.class.getName()); /** - * A utility method that throws specific types of throwable objects based on types. + * A utility method that rethrows known throwable types without changing their identity. + * + *

In particular, {@link Error} instances must not be converted to a {@link + * RuntimeException}; callers rely on errors retaining their original type and stack trace. * * @param t the throwable object * @throws IOException diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java new file mode 100644 index 0000000000..5bb23c4a6d --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java @@ -0,0 +1,20 @@ +/* + * 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.tests.util.LuceneTestCase; +import org.junit.Test; + +public class TestUtilsThrowableHandling extends LuceneTestCase { + + @Test + public void testHandleThrowableRethrowsErrorUnchanged() { + Error error = new AssertionError("fatal failure"); + + Error thrown = assertThrows(Error.class, () -> Utils.handleThrowable(error)); + + assertSame(error, thrown); + } +} From cc17f7415f37db53e1cd0d14445e57647987f199 Mon Sep 17 00:00:00 2001 From: Shaunak Kapur Date: Thu, 27 Aug 2026 20:09:01 +0000 Subject: [PATCH 2/6] Lucene: preserve throwable types in vector formats --- .../Lucene99AcceleratedHNSWVectorsFormat.java | 6 +- ...ratedHNSWBinaryQuantizedVectorsFormat.java | 6 +- ...ratedHNSWScalarQuantizedVectorsFormat.java | 6 +- ...tedHNSWVectorsFormatThrowableHandling.java | 99 +++++++++++++++++++ .../lucene/TestUtilsThrowableHandling.java | 30 ++++++ 5 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java 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 61c9b41c3a..7262d88242 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 @@ -91,7 +91,8 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getNumMergeWorkers(), new TaskExecutor(acceleratedHNSWParams.getMergeExec())); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + Utils.handleThrowable(e); + throw new AssertionError("handleThrowable always throws"); // unreachable } } } @@ -105,7 +106,8 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + Utils.handleThrowable(e); + throw new AssertionError("handleThrowable always throws"); // unreachable } } 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 d1810bc540..0e45adb8b2 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 @@ -89,7 +89,8 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth()); return fallbackFormat.fieldsWriter(state); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + Utils.handleThrowable(e); + throw new AssertionError("handleThrowable always throws"); // unreachable } } } @@ -103,7 +104,8 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE99_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + Utils.handleThrowable(e); + throw new AssertionError("handleThrowable always throws"); // unreachable } } 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 8d599a54ef..f8d2084c78 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 @@ -81,7 +81,8 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getBeamWidth(), acceleratedHNSWParams.getMaxConn()); return fallbackFormat.fieldsWriter(state); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + Utils.handleThrowable(e); + throw new AssertionError("handleThrowable always throws"); // unreachable } } } @@ -95,7 +96,8 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + Utils.handleThrowable(e); + throw new AssertionError("handleThrowable always throws"); // unreachable } } 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 new file mode 100644 index 0000000000..97fb066bd5 --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java @@ -0,0 +1,99 @@ +/* + * 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.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.SegmentInfo; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FilterDirectory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.apache.lucene.util.StringHelper; +import org.apache.lucene.util.Version; +import org.junit.Test; + +public class TestAcceleratedHNSWVectorsFormatThrowableHandling extends LuceneTestCase { + + @Test + public void testReadersRethrowIOExceptionUnchanged() throws Exception { + assertReaderFormatsRethrowUnchanged(new IOException("reader I/O failure")); + } + + @Test + public void testReadersRethrowRuntimeExceptionUnchanged() throws Exception { + assertReaderFormatsRethrowUnchanged(new IllegalStateException("reader runtime failure")); + } + + @Test + public void testReadersRethrowErrorUnchanged() throws Exception { + assertReaderFormatsRethrowUnchanged(new AssertionError("reader error")); + } + + private void assertReaderFormatsRethrowUnchanged(Throwable failure) throws Exception { + for (KnnVectorsFormat format : readerFormats()) { + try (Directory directory = new ThrowingDirectory(failure)) { + SegmentReadState state = newSegmentReadState(directory); + Throwable thrown = assertThrows(failure.getClass(), () -> format.fieldsReader(state)); + assertSame(format.getName(), failure, thrown); + } + } + } + + private static KnnVectorsFormat[] readerFormats() { + return new KnnVectorsFormat[] { + new Lucene99AcceleratedHNSWVectorsFormat(), + new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(), + new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat() + }; + } + + private static SegmentReadState newSegmentReadState(Directory directory) { + SegmentInfo segmentInfo = + new SegmentInfo( + directory, + Version.LATEST, + Version.LATEST, + "_0", + 0, + false, + false, + Codec.getDefault(), + Map.of(), + StringHelper.randomId(), + Map.of(), + null); + return new SegmentReadState(directory, segmentInfo, FieldInfos.EMPTY, IOContext.DEFAULT); + } + + private static final class ThrowingDirectory extends FilterDirectory { + private final Throwable failure; + + private ThrowingDirectory(Throwable failure) { + super(new ByteBuffersDirectory()); + this.failure = failure; + } + + @Override + public IndexInput openInput(String name, IOContext context) throws IOException { + if (failure instanceof IOException ioe) { + throw ioe; + } + if (failure instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (failure instanceof Error error) { + throw error; + } + throw new AssertionError("unexpected test throwable", failure); + } + } +} diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java index 5bb23c4a6d..6aead1bd8d 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java @@ -4,11 +4,31 @@ */ package com.nvidia.cuvs.lucene; +import java.io.IOException; import org.apache.lucene.tests.util.LuceneTestCase; import org.junit.Test; public class TestUtilsThrowableHandling extends LuceneTestCase { + @Test + public void testHandleThrowableRethrowsIOExceptionUnchanged() { + IOException exception = new IOException("I/O failure"); + + IOException thrown = assertThrows(IOException.class, () -> Utils.handleThrowable(exception)); + + assertSame(exception, thrown); + } + + @Test + public void testHandleThrowableRethrowsRuntimeExceptionUnchanged() { + RuntimeException exception = new IllegalStateException("runtime failure"); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> Utils.handleThrowable(exception)); + + assertSame(exception, thrown); + } + @Test public void testHandleThrowableRethrowsErrorUnchanged() { Error error = new AssertionError("fatal failure"); @@ -17,4 +37,14 @@ public void testHandleThrowableRethrowsErrorUnchanged() { assertSame(error, thrown); } + + @Test + public void testHandleThrowableWrapsCheckedExceptionWithCause() { + Exception exception = new Exception("checked failure"); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> Utils.handleThrowable(exception)); + + assertSame(exception, thrown.getCause()); + } } From 9bff4a841f00acd63ed942da483558f6000d6c1e Mon Sep 17 00:00:00 2001 From: Shaunak Kapur Date: Fri, 28 Aug 2026 14:29:15 +0000 Subject: [PATCH 3/6] Lucene: remove unreachable AssertionError after handleThrowable Utils.handleThrowable now returns RuntimeException so callers can write `throw Utils.handleThrowable(e)`, letting the compiler verify the catch block always completes abruptly. This removes the redundant `throw new AssertionError(...)` line flagged in review, while relying on existing assertThrows coverage to confirm exceptions are not swallowed. Signed-off-by: Shaunak Kapur --- .../cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java | 6 ++---- .../LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java | 6 ++---- .../LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java | 6 ++---- .../src/main/java/com/nvidia/cuvs/lucene/Utils.java | 7 ++++++- 4 files changed, 12 insertions(+), 13 deletions(-) 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 7262d88242..1b1bc3cbd2 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 @@ -91,8 +91,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getNumMergeWorkers(), new TaskExecutor(acceleratedHNSWParams.getMergeExec())); } catch (Exception e) { - Utils.handleThrowable(e); - throw new AssertionError("handleThrowable always throws"); // unreachable + throw Utils.handleThrowable(e); } } } @@ -106,8 +105,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - Utils.handleThrowable(e); - throw new AssertionError("handleThrowable always throws"); // unreachable + throw Utils.handleThrowable(e); } } 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 0e45adb8b2..40a818683d 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 @@ -89,8 +89,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth()); return fallbackFormat.fieldsWriter(state); } catch (Exception e) { - Utils.handleThrowable(e); - throw new AssertionError("handleThrowable always throws"); // unreachable + throw Utils.handleThrowable(e); } } } @@ -104,8 +103,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE99_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - Utils.handleThrowable(e); - throw new AssertionError("handleThrowable always throws"); // unreachable + throw Utils.handleThrowable(e); } } 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 f8d2084c78..ead6daeaad 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 @@ -81,8 +81,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getBeamWidth(), acceleratedHNSWParams.getMaxConn()); return fallbackFormat.fieldsWriter(state); } catch (Exception e) { - Utils.handleThrowable(e); - throw new AssertionError("handleThrowable always throws"); // unreachable + throw Utils.handleThrowable(e); } } } @@ -96,8 +95,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - Utils.handleThrowable(e); - throw new AssertionError("handleThrowable always throws"); // unreachable + throw Utils.handleThrowable(e); } } 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 d402d94f60..034f8aa5d8 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 @@ -33,10 +33,15 @@ public class Utils { *

In particular, {@link Error} instances must not be converted to a {@link * RuntimeException}; callers rely on errors retaining their original type and stack trace. * + *

This method never returns normally; its return type exists solely so callers can write + * {@code throw handleThrowable(t);}, letting the compiler verify that the enclosing statement + * always completes abruptly. + * * @param t the throwable object + * @return never returns; always throws * @throws IOException */ - static void handleThrowable(Throwable t) throws IOException { + static RuntimeException handleThrowable(Throwable t) throws IOException { switch (t) { case IOException ioe -> throw ioe; case Error error -> throw error; From 603b0a93a96ef128285f8f86059206c0e6a39aa5 Mon Sep 17 00:00:00 2001 From: Shaunak Kapur Date: Fri, 28 Aug 2026 16:51:47 +0000 Subject: [PATCH 4/6] Lucene: make filtered gap test deterministic Signed-off-by: Shaunak Kapur --- .../src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java index 31958ef0db..0bca41c204 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java @@ -146,8 +146,10 @@ public void testVectorSearchWithAlternatingDocuments() throws IOException { public void testVectorSearchWithFilterAndAlternatingDocuments() throws IOException { assumeTrue("cuVS not supported", isSupported()); - // Use the first vector (from document 0) as query - float[] queryVector = dataset[0]; + // Use the vector from the only document accepted by the filter. CAGRA is approximate, so an + // unrelated query vector can legitimately miss a single highly selective filtered candidate + // depending on the randomized dataset. + float[] queryVector = dataset[8]; int topK = random.nextInt(5, TOP_K_LIMIT); // Create a filter that only matches documents with ID less than 10 From 5171d314c9137c4458f27e5acc564e027b0208ce Mon Sep 17 00:00:00 2001 From: Shaunak Kapur Date: Fri, 28 Aug 2026 17:09:42 +0000 Subject: [PATCH 5/6] Lucene: preserve exact fallback for single-segment filters Signed-off-by: Shaunak Kapur --- .../com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java | 10 +++++++--- .../test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java | 6 ++---- 2 files changed, 9 insertions(+), 7 deletions(-) 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 f796d88799..a85e0b4758 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 @@ -63,9 +63,10 @@ * upload is cached inside the handle itself across threads. * *

Falls back to the standard per-segment Lucene path when the optimized path cannot be - * applied: mixed segment types, a missing CAGRA index for the field on any segment, or segments - * whose built CAGRA graphs differ in degree (a single multi-partition request requires a uniform - * graph degree, and a small segment can have its degree truncated at build time). + * applied: a single segment (which preserves Lucene's exact search for selective filters), mixed + * segment types, a missing CAGRA index for the field on any segment, or segments whose built CAGRA + * graphs differ in degree (a single multi-partition request requires a uniform graph degree, and a + * small segment can have its degree truncated at build time). * * @since 25.10 */ @@ -135,6 +136,9 @@ public Query rewrite(IndexSearcher indexSearcher) throws IOException { if (leaves.isEmpty()) { return new MatchNoDocsQuery(); } + if (leaves.size() == 1) { + return super.rewrite(indexSearcher); + } // Collect a CuVS2510GPUVectorsReader for every segment; fall back if any segment lacks one, // has no CAGRA index for this field, or has a CAGRA graph whose degree differs from the other diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java index 0bca41c204..31958ef0db 100644 --- a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestCuVSGaps.java @@ -146,10 +146,8 @@ public void testVectorSearchWithAlternatingDocuments() throws IOException { public void testVectorSearchWithFilterAndAlternatingDocuments() throws IOException { assumeTrue("cuVS not supported", isSupported()); - // Use the vector from the only document accepted by the filter. CAGRA is approximate, so an - // unrelated query vector can legitimately miss a single highly selective filtered candidate - // depending on the randomized dataset. - float[] queryVector = dataset[8]; + // Use the first vector (from document 0) as query + float[] queryVector = dataset[0]; int topK = random.nextInt(5, TOP_K_LIMIT); // Create a filter that only matches documents with ID less than 10 From 1a8d38afdeeef6d1f01e0082e9ef9160e3efb7e4 Mon Sep 17 00:00:00 2001 From: Shaunak Kapur Date: Fri, 28 Aug 2026 17:50:42 +0000 Subject: [PATCH 6/6] Revert single-segment exact-fallback fix out of this PR The GPUKnnFloatVectorQuery change is unrelated to this PR's scope (propagating Error subclasses instead of wrapping them). It addresses a separate pre-existing bug on main where the multi-partition GPU rewrite path bypasses Lucene's exact-search fallback for highly selective filters, which surfaces as an intermittent TestCuVSGaps failure on certain randomized seeds. Reverting it here per review feedback; tracked separately in its own issue so it can be reviewed on its own merits. Signed-off-by: Shaunak Kapur --- .../com/nvidia/cuvs/lucene/GPUKnnFloatVectorQuery.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 a85e0b4758..f796d88799 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 @@ -63,10 +63,9 @@ * upload is cached inside the handle itself across threads. * *

Falls back to the standard per-segment Lucene path when the optimized path cannot be - * applied: a single segment (which preserves Lucene's exact search for selective filters), mixed - * segment types, a missing CAGRA index for the field on any segment, or segments whose built CAGRA - * graphs differ in degree (a single multi-partition request requires a uniform graph degree, and a - * small segment can have its degree truncated at build time). + * applied: mixed segment types, a missing CAGRA index for the field on any segment, or segments + * whose built CAGRA graphs differ in degree (a single multi-partition request requires a uniform + * graph degree, and a small segment can have its degree truncated at build time). * * @since 25.10 */ @@ -136,9 +135,6 @@ public Query rewrite(IndexSearcher indexSearcher) throws IOException { if (leaves.isEmpty()) { return new MatchNoDocsQuery(); } - if (leaves.size() == 1) { - return super.rewrite(indexSearcher); - } // Collect a CuVS2510GPUVectorsReader for every segment; fall back if any segment lacks one, // has no CAGRA index for this field, or has a CAGRA graph whose degree differs from the other