Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,40 @@ mvn clean compile package
```
The artifacts would be built and available in the target / folder.

### Using with PyLucene

PyLucene embeds a JVM and starts it with the classpath passed to `lucene.initVM(...)`.
Because PyLucene's generated Python module only exposes the Java classes it was built
to wrap, use Lucene's service provider lookup to load `cuvs-lucene` codecs from
Python instead of importing `com.nvidia.cuvs.lucene` classes directly.

Build the PyLucene sidecar jar:

```sh
mvn clean package -DskipTests
```

Then add the generated PyLucene jar to `lucene.CLASSPATH` before starting the VM:

```python
import os
import lucene

cuvs_lucene_jar = "target/cuvs-lucene-26.06.0-jar-with-pylucene-dependencies.jar"
lucene.initVM(classpath=os.pathsep.join([lucene.CLASSPATH, cuvs_lucene_jar]))

from org.apache.lucene.codecs import Codec

codec = Codec.forName("Lucene101AcceleratedHNSWCodec")
```

Use the returned `codec` with `IndexWriterConfig.setCodec(codec)`. The
`jar-with-pylucene-dependencies` artifact includes `cuvs-lucene` and its non-Lucene
runtime dependencies while leaving Lucene itself to PyLucene's own classpath. This
avoids loading a second copy of Lucene classes into the embedded JVM. The regular
`jar-with-dependencies` artifact also merges `META-INF/services` entries and is
available for non-PyLucene Java applications that want a standalone jar.

### Running Tests
```sh
export LD_LIBRARY_PATH={ PATH TO YOUR LOCAL libcuvs_c.so }:$LD_LIBRARY_PATH && mvn clean test
Expand Down
2 changes: 1 addition & 1 deletion bench/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<groupId>com.nvidia.cuvs.lucene.benchmarks</groupId>
<artifactId>cuvs-lucene-benchmarks</artifactId>
<!--CUVS_JAVA#VERSION_UPDATE_MARKER_START--><version>26.02.0</version><!--CUVS_JAVA#VERSION_UPDATE_MARKER_END-->
<!--CUVS_LUCENE#VERSION_UPDATE_MARKER_START--><version>26.06.0</version><!--CUVS_LUCENE#VERSION_UPDATE_MARKER_END-->
<packaging>jar</packaging>

<name>cuvs-lucene-benchmarks</name>
Expand Down
68 changes: 68 additions & 0 deletions examples/pylucene_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0

import os
from pathlib import Path

import lucene
from lucene import JArray


REPO_ROOT = Path(__file__).resolve().parents[1]
CUVS_LUCENE_JAR = (
REPO_ROOT / "target" / "cuvs-lucene-26.06.0-jar-with-pylucene-dependencies.jar"
)
INDEX_PATH = Path("/tmp/cuvs-lucene-pylucene-index")


def fvec(values):
return JArray("float")([float(value) for value in values])


def main():
lucene.initVM(classpath=os.pathsep.join([lucene.CLASSPATH, str(CUVS_LUCENE_JAR)]))

from java.nio.file import Paths
from org.apache.lucene.codecs import Codec
from org.apache.lucene.document import Document, Field, KnnFloatVectorField, StringField
from org.apache.lucene.index import (
DirectoryReader,
IndexWriter,
IndexWriterConfig,
VectorSimilarityFunction,
)
from org.apache.lucene.search import IndexSearcher, KnnFloatVectorQuery
from org.apache.lucene.store import MMapDirectory

codec = Codec.forName("Lucene101AcceleratedHNSWCodec")

directory = MMapDirectory(Paths.get(str(INDEX_PATH)))
config = IndexWriterConfig()
config.setCodec(codec)
config.setUseCompoundFile(False)

writer = IndexWriter(directory, config)
for doc_id, vector in [("a", [0.1, 0.2, 0.3]), ("b", [0.2, 0.1, 0.4])]:
doc = Document()
doc.add(StringField("id", doc_id, Field.Store.YES))
doc.add(
KnnFloatVectorField(
"vector", fvec(vector), VectorSimilarityFunction.EUCLIDEAN
)
)
writer.addDocument(doc)
writer.close()

reader = DirectoryReader.open(directory)
searcher = IndexSearcher(reader)
query = KnnFloatVectorQuery("vector", fvec([0.1, 0.2, 0.3]), 2)

for hit in searcher.search(query, 2).scoreDocs:
print(searcher.doc(hit.doc).get("id"), hit.score)

reader.close()
directory.close()


if __name__ == "__main__":
main()
7 changes: 4 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<descriptors>
<descriptor>src/main/assembly/jar-with-dependencies.xml</descriptor>
<descriptor>src/main/assembly/jar-with-pylucene-dependencies.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
Expand Down
22 changes: 22 additions & 0 deletions src/main/assembly/jar-with-dependencies.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>jar-with-dependencies</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
</dependencySet>
</dependencySets>
<containerDescriptorHandlers>
<containerDescriptorHandler>
<handlerName>metaInf-services</handlerName>
</containerDescriptorHandler>
</containerDescriptorHandlers>
</assembly>
25 changes: 25 additions & 0 deletions src/main/assembly/jar-with-pylucene-dependencies.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>jar-with-pylucene-dependencies</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
<excludes>
<exclude>org.apache.lucene:*</exclude>
</excludes>
</dependencySet>
</dependencySets>
<containerDescriptorHandlers>
<containerDescriptorHandler>
<handlerName>metaInf-services</handlerName>
</containerDescriptorHandler>
</containerDescriptorHandlers>
</assembly>
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class CuVS2510GPUSearchCodec extends FilterCodec {
* @throws Exception
*/
public CuVS2510GPUSearchCodec() throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
initializeFormat(new GPUSearchParams.Builder().build());
}

Expand All @@ -53,7 +53,7 @@ public CuVS2510GPUSearchCodec(String name, Codec delegate) {
* @throws Exception Exception raised when initializing the codec
*/
public CuVS2510GPUSearchCodec(GPUSearchParams params) throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
initializeFormat(params);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class Lucene101AcceleratedHNSWCodec extends FilterCodec {
* @throws Exception
*/
public Lucene101AcceleratedHNSWCodec() throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
}

/**
Expand All @@ -52,7 +52,7 @@ public Lucene101AcceleratedHNSWCodec(String name, Codec delegate) {
*/
public Lucene101AcceleratedHNSWCodec(AcceleratedHNSWParams acceleratedHNSWParams)
throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
initializeFormat(acceleratedHNSWParams);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class LuceneAcceleratedHNSWBinaryQuantizedCodec extends FilterCodec {
private KnnVectorsFormat format;

public LuceneAcceleratedHNSWBinaryQuantizedCodec() throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
}

public LuceneAcceleratedHNSWBinaryQuantizedCodec(String name, Codec delegate) {
Expand All @@ -35,7 +35,7 @@ public LuceneAcceleratedHNSWBinaryQuantizedCodec(String name, Codec delegate) {

public LuceneAcceleratedHNSWBinaryQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams)
throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
initializeFormat(acceleratedHNSWParams);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,26 @@ 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 final AcceleratedHNSWParams acceleratedHNSWParams;

static {
private static LuceneProvider luceneProvider(String version) {
try {
LUCENE99_PROVIDER = LuceneProvider.getInstance("99");
LUCENE102_PROVIDER = LuceneProvider.getInstance("102");
FLAT_VECTORS_FORMAT =
LUCENE102_PROVIDER.getLuceneFlatVectorsFormatInstance(DefaultFlatVectorScorer.INSTANCE);
return LuceneProvider.getInstance(version);
} catch (Exception e) {
throw new ExceptionInInitializerError(e.getMessage());
throw new UnsupportedOperationException(
"Lucene" + version + " vector formats are not available in this runtime", e);
}
}

private static FlatVectorsFormat flatVectorsFormat() {
try {
return luceneProvider("102")
.getLuceneFlatVectorsFormatInstance(DefaultFlatVectorScorer.INSTANCE);
} catch (Exception e) {
throw new UnsupportedOperationException(
"Binary quantized vectors require Lucene102 vector formats", e);
}
}

Expand Down Expand Up @@ -70,7 +75,7 @@ public LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat(
*/
@Override
public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
var flatWriter = FLAT_VECTORS_FORMAT.fieldsWriter(state);
var flatWriter = flatVectorsFormat().fieldsWriter(state);
if (isSupported()) {
log.log(
Level.FINE,
Expand All @@ -85,8 +90,9 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException
"GPU based indexing not supported, falling back to using the"
+ " Lucene102HnswBinaryQuantizedVectorsFormat");
KnnVectorsFormat fallbackFormat =
LUCENE102_PROVIDER.getLuceneHnswBinaryQuantizedVectorsFormatInstance(
acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth());
luceneProvider("102")
.getLuceneHnswBinaryQuantizedVectorsFormatInstance(
acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth());
return fallbackFormat.fieldsWriter(state);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
Expand All @@ -100,8 +106,8 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException
@Override
public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
try {
return LUCENE99_PROVIDER.getLuceneHnswVectorsReaderInstance(
state, FLAT_VECTORS_FORMAT.fieldsReader(state));
return luceneProvider("99")
.getLuceneHnswVectorsReaderInstance(state, flatVectorsFormat().fieldsReader(state));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class LuceneAcceleratedHNSWScalarQuantizedCodec extends FilterCodec {
private KnnVectorsFormat format;

public LuceneAcceleratedHNSWScalarQuantizedCodec() throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
}

public LuceneAcceleratedHNSWScalarQuantizedCodec(String name, Codec delegate) {
Expand All @@ -35,7 +35,7 @@ public LuceneAcceleratedHNSWScalarQuantizedCodec(String name, Codec delegate) {

public LuceneAcceleratedHNSWScalarQuantizedCodec(AcceleratedHNSWParams acceleratedHNSWParams)
throws Exception {
this(NAME, LuceneProvider.getCodec("101"));
this(NAME, LuceneProvider.getDefaultDelegateCodec());
initializeFormat(acceleratedHNSWParams);
}

Expand Down
24 changes: 22 additions & 2 deletions src/main/java/com/nvidia/cuvs/lucene/LuceneProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
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;
Expand Down Expand Up @@ -79,7 +81,7 @@ public class LuceneProvider {
private static String luceneCodec = BASE + codecs + "Lucene<version>Codec";
private static String luceneCodecFallback = BASE + fallbackCodecs + "Lucene<version>Codec";

private static LuceneProvider instance;
private static final Map<String, LuceneProvider> INSTANCES = new HashMap<>();

private static MethodHandles.Lookup lookup = MethodHandles.lookup();

Expand All @@ -92,9 +94,12 @@ 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;
}
Expand Down Expand Up @@ -173,6 +178,21 @@ public static Codec getCodec(String version)
return (Codec) codecClassConstructor.newInstance();
}

public static Codec getDefaultDelegateCodec() {
for (String version : List.of("101", "99")) {
try {
return getCodec(version);
} catch (ReflectiveOperationException
| SecurityException
| IllegalArgumentException
| LinkageError e) {
log.log(Level.FINE, "Unable to load Lucene" + version + "Codec", e);
}
}
log.log(Level.FINE, "Falling back to the runtime default codec");
return Codec.getDefault();
}

public FlatVectorsFormat getLuceneFlatVectorsFormatInstance(FlatVectorsScorer scorer)
throws Exception {
try {
Expand Down
Loading