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();
+ }
+}