diff --git a/java/cuvs-lucene/README.md b/java/cuvs-lucene/README.md index 8e3881f23d..6c789dd689 100644 --- a/java/cuvs-lucene/README.md +++ b/java/cuvs-lucene/README.md @@ -12,14 +12,14 @@ This is a project for using [cuVS](https://github.com/rapidsai/cuvs), NVIDIA's G ## 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 and gracefully fall back to the default CPU codec when no GPU is present. +`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. The accelerated-HNSW codecs can fall back to Lucene's CPU HNSW writer when cuVS is unavailable; the GPU-search codec requires cuVS. This development line is compiled and tested against the Lucene 10.2.0 runtime ABI. Four codecs are currently provided: -- `Lucene101AcceleratedHNSWCodec` — GPU-accelerated HNSW build with CPU HNSW search. The on-disk format is standard Lucene HNSW, so indexes built on the GPU can be read by any stock Lucene 10.x reader. +- `Lucene101AcceleratedHNSWCodec` — GPU-accelerated HNSW build with CPU HNSW search. Its vector data uses Lucene's standard HNSW format and stock HNSW reader; applications still need a compatible `cuvs-lucene` codec provider to resolve the segment codec. - `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 +- `CuVS2510GPUSearchCodec` — GPU CAGRA build and GPU CAGRA search ## Installing cuvs-lucene diff --git a/java/cuvs-lucene/pom.xml b/java/cuvs-lucene/pom.xml index 48dc6d7889..9a12d5c52c 100644 --- a/java/cuvs-lucene/pom.xml +++ b/java/cuvs-lucene/pom.xml @@ -196,6 +196,24 @@ SPDX-License-Identifier: Apache-2.0 + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.4 + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}.jar + + + org.apache.maven.plugins maven-source-plugin 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..a925befd42 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 @@ -52,9 +52,9 @@ * single multi-partition search to cuVS, passing one Lucene segment per cuVS partition. cuVS * runs the per-partition CAGRA searches, applies distance post-processing, and performs the * cross-partition top-k merge internally; the returned arrays are mapped to Lucene doc IDs on - * the host. The effective CAGRA algorithm (SINGLE_CTA or MULTI_KERNEL) is selected by cuVS - * based on {@code searchAlgo} and {@code itopk_size}, with MULTI_KERNEL handling k beyond - * SINGLE_CTA's per-partition cap. + * the host. For a multi-partition search, cuVS resolves {@code AUTO} to {@code SINGLE_CTA} or + * {@code MULTI_CTA} from the search parameters and query/partition topology; {@code MULTI_KERNEL} + * is not supported by the multi-partition API. * *

If the query has an explicit {@code filter}, or if any segment carries live-document deletes, * the acceptance mask (filter ∩ liveDocs) is packed into one {@link FilterBitsetHandle} per segment @@ -481,10 +481,9 @@ private static CuVS2510GPUVectorsReader unwrapGpuReader(LeafReaderContext ctx, S /** * Builds a {@link Query} that matches exactly the given pre-scored documents. * - *

Partitions {@code scoreDocs} by segment (using {@link ScoreDoc#shardIndex} as the segment - * offset relative to {@link LeafReaderContext#docBase}), then returns a {@link Scorer} per - * segment that iterates those docs in ascending doc-ID order and replays their pre-computed - * scores. + *

Partitions {@code scoreDocs} by each global doc ID's membership in a leaf's {@link + * LeafReaderContext#docBase} range, then returns a {@link Scorer} per segment that iterates those + * docs in ascending doc-ID order and replays their pre-computed scores. */ private static Query docAndScoreQuery(ScoreDoc[] scoreDocs) { return new Query() { 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..af98cabad9 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 @@ -52,7 +52,12 @@ public Lucene101AcceleratedHNSWCodec(String name, Codec delegate) { */ public Lucene101AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams) throws Exception { - this(NAME, LuceneProvider.getCodec("101")); + this(NAME, LuceneProvider.getCodec("101"), acceleratedHNSWParams); + } + + private Lucene101AcceleratedHNSWCodec( + String name, Codec delegate, AcceleratedHNSWParams acceleratedHNSWParams) { + super(name, delegate); initializeFormat(acceleratedHNSWParams); } 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..fcf5a50aa8 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 @@ -35,7 +35,7 @@ public LuceneAcceleratedHNSWBinaryQuantizedCodec(String name, Codec delegate) { public LuceneAcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) throws Exception { - this(NAME, LuceneProvider.getCodec("101")); + super(NAME, LuceneProvider.getCodec("101")); initializeFormat(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..9542cbac5e 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 @@ -8,12 +8,13 @@ import com.nvidia.cuvs.LibraryException; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.Callable; import java.util.logging.Level; 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.DefaultFlatVectorScorer; import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; import org.apache.lucene.index.SegmentReadState; import org.apache.lucene.index.SegmentWriteState; @@ -27,22 +28,86 @@ public class LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat extends KnnVector 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 int MAX_DIMENSIONS = 4096; + private static volatile FlatVectorsFormat cachedFlatVectorsFormat; private final AcceleratedHNSWParams acceleratedHNSWParams; + private volatile KnnVectorsFormat cachedFallbackFormat; - static { + private static LuceneProvider getLucene99Provider() throws IOException { try { - LUCENE99_PROVIDER = LuceneProvider.getInstance("99"); - LUCENE102_PROVIDER = LuceneProvider.getInstance("102"); - FLAT_VECTORS_FORMAT = - LUCENE102_PROVIDER.getLuceneFlatVectorsFormatInstance(DefaultFlatVectorScorer.INSTANCE); + return LuceneProvider.getInstance(LuceneProvider.LUCENE_99_FORMAT_VERSION); + } catch (ClassNotFoundException e) { + throw new IOException("Lucene99 vector formats are not available in this runtime", e); + } + } + + private static RuntimeException handleConstructionFailure(String formatName, Throwable failure) + throws IOException { + if (failure instanceof IOException + || failure instanceof RuntimeException + || failure instanceof Error) { + return Utils.handleThrowable(failure); + } + return new IllegalStateException("Unable to construct " + formatName, failure); + } + + static T constructLucene102Format(String formatName, Callable constructor) + throws IOException { + try { + return constructor.call(); + } catch (ClassNotFoundException e) { + throw new UnsupportedOperationException( + formatName + " is not available in this Lucene runtime", e); + } catch (InvocationTargetException e) { + throw handleConstructionFailure(formatName, e.getTargetException()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to construct " + formatName, e); + } catch (IOException | RuntimeException | Error e) { + throw Utils.handleThrowable(e); } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); + throw new IllegalStateException("Unable to construct " + formatName, e); + } + } + + private static FlatVectorsFormat getOrCreateFlatVectorsFormat() throws IOException { + FlatVectorsFormat format = cachedFlatVectorsFormat; + if (format == null) { + synchronized (LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.class) { + format = cachedFlatVectorsFormat; + if (format == null) { + format = + constructLucene102Format( + "Lucene102BinaryQuantizedVectorsFormat", + () -> + LuceneProvider.getInstance(LuceneProvider.LUCENE_102_BINARY_FORMAT_VERSION) + .getLuceneBinaryQuantizedVectorsFormatInstance()); + cachedFlatVectorsFormat = format; + } + } } + return format; + } + + private KnnVectorsFormat getOrCreateFallbackFormat() throws IOException { + KnnVectorsFormat format = cachedFallbackFormat; + if (format == null) { + synchronized (this) { + format = cachedFallbackFormat; + if (format == null) { + format = + constructLucene102Format( + "Lucene102HnswBinaryQuantizedVectorsFormat", + () -> + LuceneProvider.getInstance(LuceneProvider.LUCENE_102_BINARY_FORMAT_VERSION) + .getLuceneHnswBinaryQuantizedKnnVectorsFormatInstance( + acceleratedHNSWParams.getMaxConn(), + acceleratedHNSWParams.getBeamWidth())); + cachedFallbackFormat = format; + } + } + } + return format; } /** @@ -70,27 +135,20 @@ public LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat( */ @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { - var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state); if (isSupported()) { + var flatWriter = getOrCreateFlatVectorsFormat().fieldsWriter(state); log.log( Level.FINE, "cuVS is supported so using the Lucene99AcceleratedHNSWBinaryQuantizedVectorsWriter"); return new LuceneAcceleratedHNSWBinaryQuantizedVectorsWriter( state, acceleratedHNSWParams, flatWriter); } 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); - } + // Fallback to Lucene's Lucene102HnswBinaryQuantizedVectorsFormat format + log.log( + Level.WARNING, + "GPU based indexing not supported, falling back to using the" + + " Lucene102HnswBinaryQuantizedVectorsFormat"); + return getOrCreateFallbackFormat().fieldsWriter(state); } } @@ -100,8 +158,9 @@ 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)); + return getLucene99Provider() + .getLuceneHnswVectorsReaderInstance( + state, getOrCreateFlatVectorsFormat().fieldsReader(state)); } catch (Exception e) { throw Utils.handleThrowable(e); } 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..e81e6f950e 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 @@ -35,7 +35,7 @@ public LuceneAcceleratedHNSWScalarQuantizedCodec(String name, Codec delegate) { public LuceneAcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams) throws Exception { - this(NAME, LuceneProvider.getCodec("101")); + super(NAME, LuceneProvider.getCodec("101")); initializeFormat(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..56759bd866 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 @@ -6,7 +6,6 @@ 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; @@ -25,26 +24,60 @@ public class LuceneAcceleratedHNSWScalarQuantizedVectorsFormat extends KnnVector 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 static volatile FlatVectorsFormat cachedFlatVectorsFormat; private final AcceleratedHNSWParams acceleratedHNSWParams; + private volatile KnnVectorsFormat cachedFallbackFormat; - static { + private static LuceneProvider getLuceneProvider() throws IOException { try { - LUCENE_PROVIDER = LuceneProvider.getInstance("99"); - FLAT_VECTORS_FORMAT = LUCENE_PROVIDER.getLuceneScalarQuantizedVectorsFormatInstance(); - } catch (Exception e) { - throw new ExceptionInInitializerError(e.getMessage()); + return LuceneProvider.getInstance(LuceneProvider.LUCENE_99_FORMAT_VERSION); + } catch (ClassNotFoundException e) { + throw new IOException("Lucene99 vector formats are not available in this runtime", e); } } - /** - * Initializes {@link LuceneAcceleratedHNSWScalarQuantizedVectorsFormat} with default values. - * - * @throws LibraryException if the native library fails to load - */ + private static FlatVectorsFormat getOrCreateFlatVectorsFormat() throws IOException { + FlatVectorsFormat format = cachedFlatVectorsFormat; + if (format == null) { + synchronized (LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.class) { + format = cachedFlatVectorsFormat; + if (format == null) { + try { + format = getLuceneProvider().getLuceneScalarQuantizedVectorsFormatInstance(); + cachedFlatVectorsFormat = format; + } catch (Exception e) { + throw Utils.handleThrowable(e); + } + } + } + } + return format; + } + + private KnnVectorsFormat getOrCreateFallbackFormat() throws IOException { + KnnVectorsFormat format = cachedFallbackFormat; + if (format == null) { + synchronized (this) { + format = cachedFallbackFormat; + if (format == null) { + try { + format = + getLuceneProvider() + .getLuceneHnswScalarQuantizedKnnVectorsFormatInstance( + acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth()); + cachedFallbackFormat = format; + } catch (Exception e) { + throw Utils.handleThrowable(e); + } + } + } + } + return format; + } + + /** Initializes {@link LuceneAcceleratedHNSWScalarQuantizedVectorsFormat} with default values. */ public LuceneAcceleratedHNSWScalarQuantizedVectorsFormat() { this(new AcceleratedHNSWParams.Builder().build()); } @@ -65,8 +98,8 @@ public LuceneAcceleratedHNSWScalarQuantizedVectorsFormat( */ @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { - var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state); if (isSupported()) { + var flatWriter = getOrCreateFlatVectorsFormat().fieldsWriter(state); log.info("cuVS is supported so using the Lucene99AcceleratedHNSWQuantizedVectorsWriter"); return new LuceneAcceleratedHNSWScalarQuantizedVectorsWriter( state, acceleratedHNSWParams, flatWriter); @@ -76,10 +109,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException 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); + return getOrCreateFallbackFormat().fieldsWriter(state); } catch (Exception e) { throw Utils.handleThrowable(e); } @@ -92,8 +122,9 @@ 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)); + return getLuceneProvider() + .getLuceneHnswVectorsReaderInstance( + state, getOrCreateFlatVectorsFormat().fieldsReader(state)); } catch (Exception e) { throw Utils.handleThrowable(e); } 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 index 9caa2a6aa3..f9f518e3a8 100644 --- 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 @@ -8,10 +8,13 @@ import java.lang.invoke.VarHandle; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.lucene.codecs.Codec; +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; @@ -31,6 +34,8 @@ public class LuceneProvider { static final Logger log = Logger.getLogger(LuceneProvider.class.getName()); + static final String LUCENE_99_FORMAT_VERSION = "99"; + static final String LUCENE_102_BINARY_FORMAT_VERSION = "102"; private static final String BASE = "org.apache.lucene."; private static String codecs = "codecs.lucene."; @@ -79,7 +84,7 @@ public class LuceneProvider { private static String luceneCodec = BASE + codecs + "LuceneCodec"; private static String luceneCodecFallback = BASE + fallbackCodecs + "LuceneCodec"; - private static LuceneProvider instance; + private static final Map INSTANCES = new HashMap<>(); private static MethodHandles.Lookup lookup = MethodHandles.lookup(); @@ -92,14 +97,30 @@ public class LuceneProvider { private Class scalarQuantizedVectorsFormat; private Class hnswScalarQuantizedVectorsFormat; - public static LuceneProvider getInstance(String version) throws ClassNotFoundException { + public static synchronized LuceneProvider getInstance(String version) + throws ClassNotFoundException { + LuceneProvider instance = INSTANCES.get(version); if (instance == null) { instance = new LuceneProvider(version); + INSTANCES.put(version, instance); } return instance; } private LuceneProvider(String version) throws ClassNotFoundException { + // TODO: Find a better way if possible, but as a separate initiative. + if (LUCENE_102_BINARY_FORMAT_VERSION.equals(version)) { + binaryQuantizedVectorsFormat = + loadClass( + setVersion(luceneBinaryQuantizedVectorsFormat, version), + setVersion(luceneBinaryQuantizedVectorsFormatFallback, version)); + hnswBinaryQuantizedVectorsFormat = + loadClass( + setVersion(luceneHnswBinaryQuantizedVectorsFormat, version), + setVersion(luceneHnswBinaryQuantizedVectorsFormatFallback, version)); + return; + } + flatVectorsFormat = loadClass( setVersion(luceneFlatVectorsFormat, version), @@ -125,18 +146,6 @@ private LuceneProvider(String version) throws ClassNotFoundException { 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) { @@ -147,14 +156,20 @@ private static Class loadClass(String defaultClassName, String fallbackClassN throws ClassNotFoundException { try { return Class.forName(defaultClassName); - } catch (ClassNotFoundException e) { + } catch (ClassNotFoundException defaultException) { // 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; + } catch (ClassNotFoundException fallbackException) { + ClassNotFoundException missing = + new ClassNotFoundException( + "Unable to load Lucene class. Tried " + + defaultClassName + + " and " + + fallbackClassName); + missing.addSuppressed(defaultException); + missing.addSuppressed(fallbackException); + throw missing; } } } @@ -245,7 +260,8 @@ public List getSimilarityFunctions() } } - public FlatVectorsFormat getluceneBinaryQuantizedVectorsFormatInstance() throws Exception { + /** Returns the Lucene 10.2 flat binary-quantized vectors format. */ + public FlatVectorsFormat getLuceneBinaryQuantizedVectorsFormatInstance() throws Exception { try { Constructor luceneBinaryQuantizedVectorsFormatConstructor = binaryQuantizedVectorsFormat.getConstructor(); @@ -258,21 +274,50 @@ public FlatVectorsFormat getluceneBinaryQuantizedVectorsFormatInstance() throws } } - public FlatVectorsFormat getLuceneHnswBinaryQuantizedVectorsFormatInstance( + /** + * Retains the original public spelling for source and binary compatibility. + * + * @deprecated Use {@link #getLuceneBinaryQuantizedVectorsFormatInstance()}. + */ + @Deprecated(since = "26.10", forRemoval = false) + public FlatVectorsFormat getluceneBinaryQuantizedVectorsFormatInstance() throws Exception { + return getLuceneBinaryQuantizedVectorsFormatInstance(); + } + + /** Returns the Lucene 10.2 HNSW binary-quantized vectors format. */ + public KnnVectorsFormat getLuceneHnswBinaryQuantizedKnnVectorsFormatInstance( int maxConn, int beamWidth) throws Exception { try { Constructor luceneHnswBinaryQuantizedVectorsFormatConstructor = - hnswBinaryQuantizedVectorsFormat.getConstructor(Integer.TYPE, Integer.TYPE); - return (FlatVectorsFormat) + hnswBinaryQuantizedVectorsFormat.getConstructor(int.class, int.class); + return (KnnVectorsFormat) luceneHnswBinaryQuantizedVectorsFormatConstructor.newInstance(maxConn, beamWidth); } catch (Exception e) { log.log( Level.SEVERE, - "Unable to initialize LuceneBinaryQuantizedVectorsFormat: " + e.getMessage()); + "Unable to initialize LuceneHnswBinaryQuantizedVectorsFormat: " + e.getMessage()); throw e; } } + /** + * Retains the original JVM method descriptor for binary compatibility. + * + *

The legacy API declared {@link FlatVectorsFormat} as its return type, but Lucene's HNSW + * binary-quantized format extends {@link KnnVectorsFormat} directly. When construction returned + * the expected Lucene implementation, the legacy cast failed. Use {@link + * #getLuceneHnswBinaryQuantizedKnnVectorsFormatInstance(int, int)}. + * + * @deprecated The legacy return type cannot represent Lucene's HNSW format. + */ + @Deprecated(since = "26.10", forRemoval = false) + public FlatVectorsFormat getLuceneHnswBinaryQuantizedVectorsFormatInstance( + int maxConn, int beamWidth) throws Exception { + throw new UnsupportedOperationException( + "Lucene HNSW binary-quantized vectors require KnnVectorsFormat; use " + + "getLuceneHnswBinaryQuantizedKnnVectorsFormatInstance(int, int)"); + } + public FlatVectorsFormat getLuceneScalarQuantizedVectorsFormatInstance() throws Exception { try { Constructor luceneScalarQuantizedVectorsFormatConstructor = @@ -286,13 +331,21 @@ public FlatVectorsFormat getLuceneScalarQuantizedVectorsFormatInstance() throws } } - public FlatVectorsFormat getLuceneHnswScalarQuantizedVectorsFormatInstance( - int beamWidth, int maxConn) throws Exception { + /** + * Returns Lucene's HNSW scalar-quantized vectors format. + * + * @param maxConn maximum number of connections per graph node + * @param beamWidth number of candidate neighbors tracked while building the graph + * @return the configured scalar-quantized HNSW format + * @throws Exception if the Lucene format cannot be constructed + */ + public KnnVectorsFormat getLuceneHnswScalarQuantizedKnnVectorsFormatInstance( + int maxConn, int beamWidth) throws Exception { try { Constructor luceneHnswScalarQuantizedVectorsFormatConstructor = hnswScalarQuantizedVectorsFormat.getConstructor(Integer.TYPE, Integer.TYPE); - return (FlatVectorsFormat) - luceneHnswScalarQuantizedVectorsFormatConstructor.newInstance(beamWidth, maxConn); + return (KnnVectorsFormat) + luceneHnswScalarQuantizedVectorsFormatConstructor.newInstance(maxConn, beamWidth); } catch (Exception e) { log.log( Level.SEVERE, @@ -300,4 +353,22 @@ public FlatVectorsFormat getLuceneHnswScalarQuantizedVectorsFormatInstance( throw e; } } + + /** + * Retains the original JVM method descriptor for binary compatibility. + * + *

The legacy API declared {@link FlatVectorsFormat} as its return type, but Lucene's HNSW + * scalar-quantized format extends {@link KnnVectorsFormat} directly. When construction returned + * the expected Lucene implementation, the legacy cast failed. Use {@link + * #getLuceneHnswScalarQuantizedKnnVectorsFormatInstance(int, int)}. + * + * @deprecated The legacy return type cannot represent Lucene's HNSW format. + */ + @Deprecated(since = "26.10", forRemoval = false) + public FlatVectorsFormat getLuceneHnswScalarQuantizedVectorsFormatInstance( + int beamWidth, int maxConn) throws Exception { + throw new UnsupportedOperationException( + "Lucene HNSW scalar-quantized vectors require KnnVectorsFormat; use " + + "getLuceneHnswScalarQuantizedKnnVectorsFormatInstance(int, int)"); + } } diff --git a/java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat b/java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat index 6625ac72a4..1f9ceeda67 100644 --- a/java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat +++ b/java/cuvs-lucene/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat @@ -1,8 +1,6 @@ # 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 com.nvidia.cuvs.lucene.CuVS2510GPUVectorsFormat com.nvidia.cuvs.lucene.Lucene99AcceleratedHNSWVectorsFormat com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/SPIColdStartProbe.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/SPIColdStartProbe.java new file mode 100644 index 0000000000..f37acf3724 --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/SPIColdStartProbe.java @@ -0,0 +1,105 @@ +/* + * 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.Set; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; + +/** Exercises Lucene SPI discovery before another test can initialize its static holders. */ +public final class SPIColdStartProbe { + + private static final Set CODEC_NAMES = + Set.of( + "Lucene101AcceleratedHNSWCodec", + "CuVS2510GPUSearchCodec", + "Lucene101AcceleratedHNSWBinaryQuantizedCodec", + "Lucene101AcceleratedHNSWScalarQuantizedCodec"); + + private static final Set VECTOR_FORMAT_NAMES = + Set.of( + "CuVS2510GPUVectorsFormat", + "Lucene99AcceleratedHNSWVectorsFormat", + "Lucene99AcceleratedHNSWBinaryQuantizedVectorsFormat", + "Lucene99AcceleratedHNSWScalarQuantizedVectorsFormat"); + + private SPIColdStartProbe() {} + + public static void main(String[] args) { + if (args.length != 1) { + throw new IllegalArgumentException("Expected one probe mode"); + } + switch (args[0]) { + case "codec" -> probeCodecs(); + case "knn" -> probeVectorFormats(); + case "scalar-constructor" -> probeScalarConstructor(); + case "binary-constructor" -> probeBinaryConstructor(); + default -> throw new IllegalArgumentException("Unknown probe mode: " + args[0]); + } + } + + private static void probeCodecs() { + Set available = Codec.availableCodecs(); + requireAll("codecs", available, CODEC_NAMES); + for (String name : CODEC_NAMES) { + requireName(name, Codec.forName(name).getName()); + } + } + + private static void probeVectorFormats() { + Set available = KnnVectorsFormat.availableKnnVectorsFormats(); + requireAll("vector formats", available, VECTOR_FORMAT_NAMES); + for (String name : VECTOR_FORMAT_NAMES) { + requireName(name, KnnVectorsFormat.forName(name).getName()); + } + } + + private static void probeScalarConstructor() { + new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(); + requireProviderCacheEmpty("Scalar"); + } + + private static void probeBinaryConstructor() { + new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat(); + requireProviderCacheEmpty("Binary"); + } + + private static void requireProviderCacheEmpty(String formatName) { + try { + java.lang.reflect.Field instancesField = LuceneProvider.class.getDeclaredField("INSTANCES"); + instancesField.setAccessible(true); + @SuppressWarnings("unchecked") + java.util.Map instances = + (java.util.Map) instancesField.get(null); + if (!instances.isEmpty()) { + throw new AssertionError( + formatName + + " format construction initialized Lucene providers: " + + instances.keySet()); + } + } catch (ReflectiveOperationException e) { + throw new AssertionError("Unable to inspect Lucene provider cache", e); + } + } + + private static void requireAll(String kind, Set available, Set expected) { + if (!available.containsAll(expected)) { + throw new AssertionError( + "Missing " + kind + ": " + difference(expected, available) + "; available=" + available); + } + } + + private static Set difference(Set expected, Set available) { + java.util.HashSet missing = new java.util.HashSet<>(expected); + missing.removeAll(available); + return missing; + } + + private static void requireName(String expected, String actual) { + if (!expected.equals(actual)) { + throw new AssertionError("Expected " + expected + " but resolved " + actual); + } + } +} 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..3508edf2b5 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 @@ -6,9 +6,16 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; import org.junit.Test; @@ -33,9 +40,129 @@ public void testNonexistentCodec() throws Exception { @Test public void testExistingComponents() throws Exception { - LuceneProvider provider = LuceneProvider.getInstance("99"); + LuceneProvider provider = LuceneProvider.getInstance(LuceneProvider.LUCENE_99_FORMAT_VERSION); assertTrue(provider.getLuceneFlatVectorsFormatInstance(null) instanceof FlatVectorsFormat); assertEquals(provider.getStaticIntParam("VERSION_CURRENT"), 0); assertNotEquals(provider.getSimilarityFunctions().size(), 0); } + + @Test + public void testProviderCachesSupportedVersion() throws Exception { + LuceneProvider lucene99Provider = + LuceneProvider.getInstance(LuceneProvider.LUCENE_99_FORMAT_VERSION); + assertSame( + lucene99Provider, LuceneProvider.getInstance(LuceneProvider.LUCENE_99_FORMAT_VERSION)); + } + + @Test + @SuppressWarnings("deprecation") + public void testProviderSupportsLucene102BinaryFormats() throws Exception { + LuceneProvider lucene102BinaryFormatProvider = + LuceneProvider.getInstance(LuceneProvider.LUCENE_102_BINARY_FORMAT_VERSION); + assertNotNull(lucene102BinaryFormatProvider.getLuceneBinaryQuantizedVectorsFormatInstance()); + assertNotNull(lucene102BinaryFormatProvider.getluceneBinaryQuantizedVectorsFormatInstance()); + assertNotNull( + lucene102BinaryFormatProvider.getLuceneHnswBinaryQuantizedKnnVectorsFormatInstance( + 16, 100)); + } + + @Test + public void testProviderSupportsLucene99ScalarFormats() throws Exception { + LuceneProvider lucene99Provider = + LuceneProvider.getInstance(LuceneProvider.LUCENE_99_FORMAT_VERSION); + assertNotNull(lucene99Provider.getLuceneScalarQuantizedVectorsFormatInstance()); + KnnVectorsFormat hnswScalarFormat = + lucene99Provider.getLuceneHnswScalarQuantizedKnnVectorsFormatInstance(16, 100); + assertTrue(hnswScalarFormat.toString().contains("maxConn=16, beamWidth=100")); + } + + @Test(expected = UnsupportedOperationException.class) + @SuppressWarnings("deprecation") + public void testLegacyHnswBinaryFormatDescriptorIsRetained() throws Exception { + LuceneProvider.getInstance(LuceneProvider.LUCENE_102_BINARY_FORMAT_VERSION) + .getLuceneHnswBinaryQuantizedVectorsFormatInstance(16, 100); + } + + @Test(expected = UnsupportedOperationException.class) + @SuppressWarnings("deprecation") + public void testLegacyHnswScalarFormatDescriptorIsRetained() throws Exception { + LuceneProvider.getInstance(LuceneProvider.LUCENE_99_FORMAT_VERSION) + .getLuceneHnswScalarQuantizedVectorsFormatInstance(100, 16); + } + + @Test + public void testLucene101DelegateCodec() throws Exception { + Codec delegate = LuceneProvider.getCodec("101"); + assertEquals("Lucene101", delegate.getName()); + assertEquals( + "org.apache.lucene.codecs.lucene101.Lucene101Codec", delegate.getClass().getName()); + } + + @Test + public void testServiceLoadedCodecsCanBeInstantiated() { + String[] codecNames = { + "Lucene101AcceleratedHNSWCodec", + "CuVS2510GPUSearchCodec", + "Lucene101AcceleratedHNSWBinaryQuantizedCodec", + "Lucene101AcceleratedHNSWScalarQuantizedCodec" + }; + for (String codecName : codecNames) { + assertTrue(Codec.availableCodecs().contains(codecName)); + assertEquals(codecName, Codec.forName(codecName).getName()); + } + } + + @Test + public void testCodecSPIColdStart() throws Exception { + runColdStartProbe("codec"); + } + + @Test + public void testKnnVectorsFormatSPIColdStart() throws Exception { + runColdStartProbe("knn"); + } + + @Test + public void testScalarFormatConstructionIsLazy() throws Exception { + runColdStartProbe("scalar-constructor"); + } + + @Test + public void testBinaryFormatConstructionIsLazy() throws Exception { + runColdStartProbe("binary-constructor"); + } + + private static void runColdStartProbe(String mode) throws Exception { + String javaExecutable = + Path.of(System.getProperty("java.home"), "bin", "java").toAbsolutePath().toString(); + String testClassPath = + System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")); + Path outputFile = Files.createTempFile("cuvs-lucene-spi-" + mode + "-", ".log"); + try { + Process process = + new ProcessBuilder( + javaExecutable, + "--add-modules=jdk.incubator.vector", + "--enable-native-access=ALL-UNNAMED", + "-cp", + testClassPath, + SPIColdStartProbe.class.getName(), + mode) + .redirectErrorStream(true) + .redirectOutput(outputFile.toFile()) + .start(); + + boolean completed = process.waitFor(30, TimeUnit.SECONDS); + if (!completed) { + process.destroyForcibly(); + process.waitFor(5, TimeUnit.SECONDS); + throw new AssertionError("Timed out waiting for " + mode + " SPI cold-start probe"); + } + + String output = Files.readString(outputFile, StandardCharsets.UTF_8); + assertEquals("Cold-start probe output:\n" + output, 0, process.exitValue()); + } finally { + Files.deleteIfExists(outputFile); + } + } } diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestLucene102FormatConstruction.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestLucene102FormatConstruction.java new file mode 100644 index 0000000000..720cef0cc3 --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestLucene102FormatConstruction.java @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import org.junit.Test; + +public class TestLucene102FormatConstruction { + + private static final String FORMAT_NAME = "Lucene102BinaryQuantizedVectorsFormat"; + + @Test + public void testMissingProviderCapabilityIsUnsupported() { + ClassNotFoundException missing = new ClassNotFoundException("missing Lucene102 provider"); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> + LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.constructLucene102Format( + FORMAT_NAME, + () -> { + throw missing; + })); + + assertSame(missing, thrown.getCause()); + assertTrue(thrown.getMessage().contains(FORMAT_NAME)); + } + + @Test + public void testConstructorTargetIOExceptionIsRethrown() { + IOException failure = new IOException("constructor I/O failure"); + + IOException thrown = + assertThrows( + IOException.class, + () -> + LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.constructLucene102Format( + FORMAT_NAME, () -> invokeFailingConstructor(failure))); + + assertSame(failure, thrown); + } + + @Test + public void testConstructorTargetRuntimeExceptionIsRethrown() { + IllegalArgumentException failure = new IllegalArgumentException("invalid arguments"); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> + LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.constructLucene102Format( + FORMAT_NAME, () -> invokeFailingConstructor(failure))); + + assertSame(failure, thrown); + } + + @Test + public void testConstructorTargetErrorIsRethrown() { + AssertionError failure = new AssertionError("constructor error"); + + AssertionError thrown = + assertThrows( + AssertionError.class, + () -> + LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.constructLucene102Format( + FORMAT_NAME, () -> invokeFailingConstructor(failure))); + + assertSame(failure, thrown); + } + + @Test + public void testConstructorTargetClassNotFoundIsConstructionFailure() { + ClassNotFoundException failure = new ClassNotFoundException("failure inside constructor"); + + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, + () -> + LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.constructLucene102Format( + FORMAT_NAME, () -> invokeFailingConstructor(failure))); + + assertSame(failure, thrown.getCause()); + assertTrue(thrown.getMessage().contains("Unable to construct " + FORMAT_NAME)); + } + + @Test + public void testCheckedReflectionFailureHasConstructionContext() { + NoSuchMethodException failure = new NoSuchMethodException("missing constructor"); + + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, + () -> + LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.constructLucene102Format( + FORMAT_NAME, + () -> { + throw failure; + })); + + assertSame(failure, thrown.getCause()); + assertTrue(thrown.getMessage().contains("Unable to construct " + FORMAT_NAME)); + } + + private static Object invokeFailingConstructor(Throwable failure) throws Exception { + return FailingConstructor.class.getDeclaredConstructor(Throwable.class).newInstance(failure); + } + + private static final class FailingConstructor { + private FailingConstructor(Throwable failure) throws Exception { + if (failure instanceof Exception exception) { + throw exception; + } + if (failure instanceof Error error) { + throw error; + } + throw new AssertionError("Unexpected throwable", failure); + } + } +} diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/ThinJarContentsIT.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/ThinJarContentsIT.java new file mode 100644 index 0000000000..68d1328234 --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/ThinJarContentsIT.java @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import org.junit.Test; + +public class ThinJarContentsIT { + + private static final String THIN_JAR_PROPERTY = "cuvs.lucene.thinJar"; + private static final String CODEC_SERVICE = "META-INF/services/org.apache.lucene.codecs.Codec"; + private static final String FORMAT_SERVICE = + "META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat"; + private static final Map> EXPECTED_SERVICES = + Map.of( + CODEC_SERVICE, + Set.of( + "com.nvidia.cuvs.lucene.CuVS2510GPUSearchCodec", + "com.nvidia.cuvs.lucene.Lucene101AcceleratedHNSWCodec", + "com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedCodec", + "com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedCodec"), + FORMAT_SERVICE, + Set.of( + "com.nvidia.cuvs.lucene.CuVS2510GPUVectorsFormat", + "com.nvidia.cuvs.lucene.Lucene99AcceleratedHNSWVectorsFormat", + "com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat", + "com.nvidia.cuvs.lucene.LuceneAcceleratedHNSWScalarQuantizedVectorsFormat")); + + @Test + public void testStandardThinJarContents() throws Exception { + String configuredJar = System.getProperty(THIN_JAR_PROPERTY); + assertNotNull("Missing system property " + THIN_JAR_PROPERTY, configuredJar); + Path thinJar = Path.of(configuredJar); + assertTrue("Thin JAR does not exist: " + thinJar, Files.isRegularFile(thinJar)); + + try (JarFile jar = new JarFile(thinJar.toFile())) { + Set entries = + jar.stream() + .filter(entry -> !entry.isDirectory()) + .map(JarEntry::getName) + .collect(Collectors.toUnmodifiableSet()); + + Set serviceDescriptors = + entries.stream() + .filter(name -> name.startsWith("META-INF/services/org.apache.lucene.")) + .collect(Collectors.toUnmodifiableSet()); + assertEquals(EXPECTED_SERVICES.keySet(), serviceDescriptors); + + for (Map.Entry> expectedService : EXPECTED_SERVICES.entrySet()) { + List providers = readProviders(jar, expectedService.getKey()); + assertEquals( + "Duplicate providers in " + expectedService.getKey(), + providers.size(), + Set.copyOf(providers).size()); + assertEquals(expectedService.getValue(), Set.copyOf(providers)); + for (String provider : providers) { + assertTrue( + "Missing provider class " + provider, + entries.contains(provider.replace('.', '/') + ".class")); + } + } + + for (String entry : entries) { + assertFalse( + "Thin JAR bundles a Lucene class: " + entry, + entry.endsWith(".class") + && (entry.startsWith("org/apache/lucene/") + || entry.contains("/org/apache/lucene/"))); + assertFalse( + "Thin JAR bundles a base cuvs-java class: " + entry, + entry.startsWith("com/nvidia/cuvs/") + && entry.endsWith(".class") + && !entry.startsWith("com/nvidia/cuvs/lucene/")); + assertFalse( + "Thin JAR bundles a multi-release cuvs-java payload: " + entry, + entry.startsWith("META-INF/versions/") && entry.contains("/com/nvidia/cuvs/")); + assertFalse( + "Thin JAR contains PyLucene test support: " + entry, + entry.contains("PyLuceneTestSupport")); + } + } + } + + private static List readProviders(JarFile jar, String descriptor) throws IOException { + JarEntry entry = jar.getJarEntry(descriptor); + assertNotNull("Missing service descriptor " + descriptor, entry); + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader(jar.getInputStream(entry), StandardCharsets.UTF_8))) { + return reader + .lines() + .map(ThinJarContentsIT::stripComment) + .filter(line -> !line.isEmpty()) + .toList(); + } + } + + private static String stripComment(String line) { + int commentStart = line.indexOf('#'); + return (commentStart < 0 ? line : line.substring(0, commentStart)).trim(); + } +}