KNN vector search with HNSW for semantic retrieval
Lucene’s core library includes native support for approximate nearest-neighbor (ANN) vector search using the HNSW (Hierarchical Navigable Small World) graph algorithm. This enables semantic and hybrid retrieval without any external plugin.
KNN vector search finds the k documents whose stored vectors are most similar to a query vector according to a chosen distance function. Lucene indexes these vectors in an HNSW graph, which provides sub-linear query time at the cost of some recall (approximate, not exact). When a pre-filter is applied, Lucene dynamically switches to exact search when the filtered candidate set is small enough.No extra module dependency is required — KNN vector support is part of lucene-core.
Stores dense float[] vectors. Each dimension is a 32-bit float. Supports up to 1024 dimensions per field. Use for embedding models that output float32 vectors.
KnnByteVectorField
Stores dense byte[] vectors. Each dimension is an 8-bit signed integer. More compact than float; suitable for quantized embeddings.
KnnFloatVectorField stores a float[] vector alongside a similarity function for a named field. Every document can store at most one value per vector field.
import org.apache.lucene.document.KnnFloatVectorField;import org.apache.lucene.index.VectorSimilarityFunction;float[] embedding = computeEmbedding(text); // your embedding model// Constructor: (fieldName, vector, similarityFunction)document.add(new KnnFloatVectorField("embedding", embedding, VectorSimilarityFunction.DOT_PRODUCT));// Or use the default EUCLIDEAN similarity:document.add(new KnnFloatVectorField("embedding", embedding));
If you plan to reuse the same field type across many documents (e.g., in a loop), create it once with createFieldType():
FieldType vectorFieldType = KnnFloatVectorField.createFieldType( 768, VectorSimilarityFunction.DOT_PRODUCT);// For each document:document.add(new KnnFloatVectorField("embedding", embedding, vectorFieldType));
Vectors in a single field must all have the same dimension. Attempting to add a vector with a different dimension than what was first indexed will throw an IllegalArgumentException.
Combining with BooleanQuery for filtered vector search
Pass a filter Query to KnnFloatVectorQuery to restrict the candidate set before the ANN search. Lucene automatically chooses between approximate and exact KNN based on how selective the filter is.
SeededKnnVectorQuery provides a seed query that the HNSW implementation may use to initialize its graph traversal, potentially reducing the number of nodes visited. This is the technique described in “Lexically-Accelerated Dense Retrieval” (SIGIR 2023).
SeededKnnVectorQuery is @lucene.experimental. The underlying codec is free to ignore the seed; behavior may change across releases.
import org.apache.lucene.search.SeededKnnVectorQuery;import org.apache.lucene.search.KnnFloatVectorQuery;KnnFloatVectorQuery knnQuery = new KnnFloatVectorQuery("embedding", queryVector, 10);// Provide a BM25 query as the seed to guide graph traversalQuery textSeed = new TermQuery(new Term("body", "lucene"));Query seededQuery = SeededKnnVectorQuery.fromFloatQuery(knnQuery, textSeed);TopDocs hits = searcher.search(seededQuery, 10);
Lucene does not include a production embedding model. The demo module ships DemoEmbeddings, which tokenizes text, lower-cases it, looks up tokens in a pre-built KnnVectorDict, and sums the token vectors into a unit-length result vector.
import org.apache.lucene.demo.knn.DemoEmbeddings;import org.apache.lucene.demo.knn.KnnVectorDict;KnnVectorDict dict = new KnnVectorDict(dir, "vectors");DemoEmbeddings embedder = new DemoEmbeddings(dict);// Returns a unit-length float[] ready for KnnFloatVectorFieldfloat[] vector = embedder.computeEmbedding("apache lucene search");
In production, replace DemoEmbeddings with your preferred embedding model (e.g., a model served via ONNX Runtime, a REST call to an embedding API, or a Java-native library). The interface is simply float[] computeEmbedding(String input).
For DOT_PRODUCT similarity, vectors must be unit-length. Normalize with VectorUtil.l2normalize(float[]) before indexing and before querying. Passing un-normalized vectors with DOT_PRODUCT produces incorrect rankings without throwing an error.