Propagate getMergeInstance() through the quantized vector readers - #16622
Open
john-mlika wants to merge 1 commit into
Open
john-mlika wants to merge 1 commit into
john-mlika wants to merge 1 commit into
Conversation
john-mlika
force-pushed
the
sq-reader-merge-instance
branch
from
September 2, 2026 16:25
1074784 to
9d04510
Compare
john-mlika
force-pushed
the
sq-reader-merge-instance
branch
from
September 6, 2026 15:25
9d04510 to
559fbdf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MergeStateasks every reader for a merge instance when a merge starts, andLucene99HnswVectorsReaderpasses that on to its flat reader. For quantized fields the flat readeris
Lucene104ScalarQuantizedVectorsReader(or, for older segments, theLucene99andLucene102quantized readers in backward-codecs), and that's where the request dies: these readers wrap a
rawVectorsReaderbut don't overridegetMergeInstance(), so they returnthisand the raw readernever hears about the merge. Same for
finishMerge().What that costs depends on the raw reader. In Lucene it is
Lucene99FlatVectorsReader, whose mergeinstance is the same reader with its
.vecinput switched to sequential read advice, so quantizedfields miss an optimization that plain float fields already get on every merge. A raw reader that
keeps a separate input for merging would hand that back instead, and it never gets the chance
either.
I ran into this in Elasticsearch, where the direct-I/O vector stack has one reader for searches and
one for merges and chooses between them in
getMergeInstance(). Onint8_hnswfields the choicenever happened, so every merge read the whole source
.vecthrough the search reader. With directI/O that is one 8 KiB device round-trip at a time, and twice over the file, once for the integrity
check and once for the copy. Jim Ferenczi fixed the links Elasticsearch owns in
elastic/elasticsearch#153423 and left this one in the commit message: "
Lucene104ScalarQuantizedVectorsReader(
int8_hnsw/int4_hnsw) overrides neither method, so nothing below it is reached for those fieldtypes. It keeps its
rawVectorsReaderprivate with no copy constructor, so the equivalent fix cannotbe made from the Elasticsearch subclass and belongs upstream." So here it is.
The change is small. Each reader gets a copy constructor that shares the original's open state and
takes a different
rawVectorsReader.getMergeInstance()returns a copy built aroundrawVectorsReader.getMergeInstance(), andfinishMerge()forwards to the raw reader. That is howLucene99HnswVectorsReaderalready handles its own flat reader. Thefieldsmap moves out of itsfield initializer into the main constructor, because a final field with an initializer can't be
assigned by the copy constructor. On the two non-final readers the copy constructor is
protected, and thegetMergeInstance()javadoc explains that a subclass with state of its own hasto override it and build its own copy, or it gets a plain base-class merge instance.
Lucene99ScalarQuantizedVectorsReaderis final, so its constructor stays private.A merge instance is a view over the live reader's resources: the merging thread uses it and nobody
closes it, the same as the HNSW readers' merge instances. If the raw reader's
getMergeInstance()returns
this, the copy reads through the same objects and the only effect is whatever the rawreader did along the way. No file format changes.
I included the two backward-codecs readers because they still serve every merge whose sources are
pre-10.4 scalar-quantized or 10.2 binary-quantized segments, which is what an upgrade merge reads
for anyone who had quantized vectors before 10.4. I can split them into a follow-up if you'd rather
keep this to core.
A few limits. This only matters when the merge reads through a pooled reader, so on an index that is
serving searches, or NRT. Without a pooled reader,
IndexWriteropens the sources with a mergecontext that
MMapDirectoryalready maps to sequential advice. Merges that go throughMergePolicy#reorderoraddIndexes(CodecReader...)are wrapped in readers that don't propagatemerge instances at all, before or after this. And while a merge runs, a quantized field's
.vecison sequential advice for concurrent searches that read full-precision vectors, which is already the
case for plain float fields.