kNN / HNSW vector search
|
This section documents the current Apache Lucene 10.x line — Lucene 10 requires Java 21 — as published
at the Apache Lucene documentation and Javadoc, which is the reference these
pages are written and verified against. No specific patch version is pinned; examples target This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Lucene iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Lucene stores a dense embedding in a vector field and searches it approximately with an HNSW graph:
a layered proximity graph that a query walks greedily from a sparse top layer down to the full
bottom layer. This is the engine under Elasticsearch’s
dense_vector and Solr’s
DenseVectorField; this page is the core-Lucene API.
Vector fields
KnnFloatVectorField holds a float[]; KnnByteVectorField holds a byte[] (one byte per
dimension, roughly a quarter of the size, at some recall cost). Every value in a field must have the
same dimension count, and every value is compared with the same VectorSimilarityFunction, fixed
when the field is created.
// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/document/KnnFloatVectorField.html
import org.apache.lucene.document.KnnByteVectorField;
import org.apache.lucene.document.KnnFloatVectorField;
import org.apache.lucene.index.VectorSimilarityFunction;
float[] embedding = model.embed("wireless noise-cancelling headphones"); // length == dims
Document doc = new Document();
doc.add(new KnnFloatVectorField("vec", embedding, VectorSimilarityFunction.COSINE));
byte[] q8 = quantiseToBytes(embedding);
doc.add(new KnnByteVectorField("vec_b", q8, VectorSimilarityFunction.DOT_PRODUCT));
writer.addDocument(doc);
| VectorSimilarityFunction | Use when |
|---|---|
|
Straight L2 distance; the default, always safe |
|
Vectors are unit length — cheapest to compute |
|
Angle only; Lucene normalises internally, so magnitude is ignored |
|
Un-normalised vectors where magnitude carries meaning |
Scores are always returned as a positive, higher-is-better number regardless of the function. Field
reference:
KnnFloatVectorField.
The HNSW graph and its codec formats
Graph parameters live in the KnnVectorsFormat on the codec, not on the field. Lucene99HnswVectorsFormat
takes maxConn (M — edges kept per node, default 16) and beamWidth (candidate list size while
inserting a node, default 100). Higher values raise recall and index size and slow the build; neither
affects query latency directly.
// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsFormat.html
import org.apache.lucene.codecs.KnnVectorsFormat;
import org.apache.lucene.codecs.lucene99.Lucene99HnswScalarQuantizedVectorsFormat;
import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat;
import org.apache.lucene.codecs.lucene100.Lucene100Codec;
KnnVectorsFormat plain = new Lucene99HnswVectorsFormat(16, 100); // full float32
KnnVectorsFormat quantised = new Lucene99HnswScalarQuantizedVectorsFormat(); // int8/int7 on disk
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
iwc.setCodec(new Lucene100Codec() {
@Override
public KnnVectorsFormat getKnnVectorsFormatForField(String field) {
return field.equals("vec") ? quantised : super.getKnnVectorsFormatForField(field);
}
});
Lucene99HnswScalarQuantizedVectorsFormat keeps the float graph in memory but stores vectors
scalar-quantised (7- or 4-bit) on disk; later 10.x lines add int4 and binary (BBQ) quantised formats
in the same codecs.lucene99 / newer codec packages, trading a little recall for a large drop in
disk and memory. Format list:
codecs.lucene99 package.
Querying
KnnFloatVectorQuery (and KnnByteVectorQuery) does approximate search: it returns the k nearest
vectors to a target. In core Lucene k is also the per-segment beam size — engines layered on top
expose that second number separately as num_candidates / topK. A filter Query restricts which
documents the graph walk may return, so k hits still come back even under a selective filter
(pre-filtering, not post-filtering).
// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/search/KnnFloatVectorQuery.html
import org.apache.lucene.search.KnnFloatVectorQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.index.Term;
float[] qv = model.embed("headphones that block background noise");
Query knn = new KnnFloatVectorQuery("vec", qv, 10);
Query filter = new TermQuery(new Term("category", "audio"));
Query filteredKnn = new KnnFloatVectorQuery("vec", qv, 10, filter); // pre-filtered
TopDocs hits = searcher.search(filteredKnn, 10);
Exact kNN
When the candidate set is already small, skip the graph and score every match by brute force with a
DoubleValuesSource over the vector field, wrapped in a
FunctionScoreQuery:
// DoubleValuesSource.similarityToQueryVector() is a per-leaf helper -- wrap it in a
// DoubleValuesSource so it can be combined with a FunctionScoreQuery:
// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/search/DoubleValuesSource.html
import org.apache.lucene.queries.function.FunctionScoreQuery;
import org.apache.lucene.search.*;
import org.apache.lucene.index.LeafReaderContext;
import java.io.IOException;
DoubleValuesSource exact = new DoubleValuesSource() {
@Override
public DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException {
return DoubleValuesSource.similarityToQueryVector(ctx, qv, "vec");
}
@Override public boolean needsScores() { return false; }
@Override public DoubleValuesSource rewrite(IndexSearcher reader) { return this; }
@Override public boolean equals(Object o) { return o != null && getClass() == o.getClass(); }
@Override public int hashCode() { return getClass().hashCode(); }
@Override public String toString() { return "exactVectorSimilarity(vec)"; }
};
Query exactKnn = new FunctionScoreQuery(
new TermQuery(new Term("category", "audio")), exact); // exact over the filtered set
Hybrid lexical + vector search
Combine a text query and a vector query as SHOULD clauses of a BooleanQuery so both contribute to
the score, or run the vector comparison only as a second pass with QueryRescorer.
import org.apache.lucene.search.*;
// (a) one ranking, both signals
Query hybrid = new BooleanQuery.Builder()
.add(textQuery, BooleanClause.Occur.SHOULD)
.add(new KnnFloatVectorQuery("vec", qv, 50), BooleanClause.Occur.SHOULD)
.build();
// (b) lexical first pass, vector rescoring of the top N
// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/search/QueryRescorer.html
TopDocs firstPass = searcher.search(textQuery, 500);
Query vectorScore = new FunctionScoreQuery(new MatchAllDocsQuery(), exact);
Rescorer rescorer = new QueryRescorer(vectorScore) {
@Override
protected float combine(float first, boolean secondMatch, float second) {
return 0.3f * first + 0.7f * second;
}
};
TopDocs reranked = rescorer.rescore(searcher, firstPass, 10);
Because a BM25 score and a similarity score are not on one scale, an unweighted SHOULD blend
favours whichever side produces larger numbers — weight the clauses or use the rescoring form when
the lexical signal should decide the candidate set.
Panama Vector API acceleration
Lucene 10 ships vectorised distance kernels that use the JDK’s incubating Vector API
(jdk.incubator.vector). On a supported JDK the accelerated implementation is selected automatically
from the multi-release lucene-core JAR once the module is added on the launch command:
java --add-modules jdk.incubator.vector -cp app.jar:lucene-core-10.0.0.jar com.example.App
# Lucene logs a line confirming the Panama-optimised vector implementation is in use.
The foreign-memory and Vector API internals are linked, not covered in depth, from the
org.apache.lucene.search package overview,
which also summarises the vector-search API.
Related pages
-
Documents & fields — where vector fields sit among the field types.
-
Core queries —
BooleanQueryand the filter clauses combined here. -
Function scoring, expressions & sorting —
DoubleValuesSourceandFunctionScoreQuery. -
Scoring & similarity — how vector scores blend with BM25.
-
Directories & storage — the codec that carries the HNSW format.
-
Elasticsearch: Vector & semantic search — the same graph, exposed as
dense_vector+ kNN. -
Solr: Dense vector search — the
DenseVectorFieldandknnquery parser.