Skip to content

Conversation

thecoop
Copy link
Member

@thecoop thecoop commented Sep 24, 2025

Adds classes to form a generic layer to hnsw and disk bbq flat vector storage, allowing the flat vector format to be swapped out without changing the HNSW/DiskBBQ format. The format to use for flat vectors is stored in the top-level metadata, and loaded from the top-level format via a string key (as the relevant formats are not necessarily registered with SPI).

The previous DirectIO JVM option is removed, pending an on_disk_rescore index option being added

This does not change the actual formats that are used by ES, but this infrastructure will be built on in later PRs. Until then, direct IO is not available

@thecoop thecoop force-pushed the generic-hnsw-format branch from be1f204 to 64d858c Compare September 24, 2025 12:04
@thecoop thecoop changed the title Use a generic HNSW format for some HNSW vector types Use a generic HNSW format for hnsw and disk BBQ Sep 24, 2025
@thecoop thecoop force-pushed the generic-hnsw-format branch from ad1841b to 4f2154a Compare September 24, 2025 14:37
reason = "TODO Deprecate any lenient usage of Boolean#parseBoolean https://github.com/elastic/elasticsearch/issues/128993"
)
private static boolean getUseDirectIO() {
return Boolean.parseBoolean(System.getProperty("vector.rescoring.directio", "false"));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: update docs

@thecoop thecoop marked this pull request as ready for review September 24, 2025 15:38
@thecoop thecoop requested a review from benwtrent September 25, 2025 13:50
@thecoop thecoop force-pushed the generic-hnsw-format branch from a03e752 to 940be42 Compare September 25, 2025 16:02
@thecoop thecoop removed the WIP label Sep 25, 2025
@elasticsearchmachine elasticsearchmachine added the Team:Search Relevance Meta label for the Search Relevance team in Elasticsearch label Sep 25, 2025
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-search-relevance (Team:Search Relevance)

@elasticsearchmachine
Copy link
Collaborator

Hi @thecoop, I've created a changelog YAML for you.

Comment on lines 85 to 93
@Override
public final KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
var readFormats = supportedReadFlatVectorsFormats();
return new ES93GenericHnswVectorsReader(state, f -> {
var format = readFormats.get(f);
if (format == null) return null;
return format.fieldsReader(state);
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, instead of doing this, why don't we actually wrap the reader/writers. Having this new "meta format" write information to its own meta file vfi(vector format information). And then utilize that information to construct the appropriate readers.

Example (with many TODOs):


    private static final class FieldsWriter extends KnnVectorsWriter {

        private final IndexOutput metaOut;
        private final SegmentWriteState state;
        private final KnnVectorsWriter rawVectorWriter;

        FieldsWriter(SegmentWriteState state, KnnVectorsWriter rawWriter) throws IOException {
            this.rawVectorWriter = rawWriter;
            this.state = state;
            final String metaFileName = IndexFileNames.segmentFileName(
                state.segmentInfo.name,
                state.segmentSuffix,
                VECTOR_FORMAT_INFO_EXTENSION
            );
            try {
                this.metaOut = state.directory.createOutput(metaFileName, state.context);
                // TODO write meta information about the writer
                CodecUtil.writeHeader(metaOut, META_CODEC_NAME, VERSION_CURRENT);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public KnnFieldVectorsWriter<?> addField(FieldInfo fieldInfo) throws IOException {
            return rawVectorWriter.addField(fieldInfo);
        }

        @Override
        public void flush(int maxDoc, Sorter.DocMap sortMap) throws IOException {
            rawVectorWriter.flush(maxDoc, sortMap);
        }

        @Override
        public void finish() throws IOException {
            rawVectorWriter.finish();
        }

        @Override
        public void close() throws IOException {
            rawVectorWriter.close();
        }

        @Override
        public long ramBytesUsed() {
            return rawVectorWriter.ramBytesUsed();
        }

        @Override
        public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException {
            rawVectorWriter.mergeOneField(fieldInfo, mergeState);
        }
    }

    private static final class FieldsReader extends KnnVectorsReader {
        KnnVectorsReader rawVectorReader;
        FieldsReader(SegmentReadState state) throws IOException {
            // read in the meta information
            final String metaFileName = IndexFileNames.segmentFileName(
                state.segmentInfo.name,
                state.segmentSuffix,
                VECTOR_FORMAT_INFO_EXTENSION
            );
            int versionMeta = -1;
            try (var metaIn = state.directory.openChecksumInput(metaFileName)) {
                Throwable priorE = null;
                Map<String, FlatVectorsReader> readers = null;
                try {
                    versionMeta = CodecUtil.checkIndexHeader(
                        metaIn,
                        META_CODEC_NAME,
                        VERSION_START,
                        VERSION_CURRENT,
                        state.segmentInfo.getId(),
                        state.segmentSuffix
                    );
                    String innerFormatName = metaIn.readString();
                    //TODO load format
                    FlatVectorsFormat format = new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer());
                    rawVectorReader = new Lucene99HnswVectorsReader(state, format.fieldsReader(state));
                } catch (Throwable exception) {
                    priorE = exception;
                } finally {
                    CodecUtil.checkFooter(metaIn, priorE);
                }
            }
        }

        @Override
        public void checkIntegrity() throws IOException {
            rawVectorReader.checkIntegrity();
        }

        @Override
        public FloatVectorValues getFloatVectorValues(String field) throws IOException {
            return rawVectorReader.getFloatVectorValues(field);
        }

        @Override
        public ByteVectorValues getByteVectorValues(String field) throws IOException {
            return rawVectorReader.getByteVectorValues(field);
        }

        @Override
        public void search(String field, float[] target, KnnCollector knnCollector, AcceptDocs acceptDocs) throws IOException {
            rawVectorReader.search(field, target, knnCollector, acceptDocs);
        }

        @Override
        public void search(String field, byte[] target, KnnCollector knnCollector, AcceptDocs acceptDocs) throws IOException {
            rawVectorReader.search(field, target, knnCollector, acceptDocs);
        }

        @Override
        public void close() throws IOException {
            rawVectorReader.close();
        }
    }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! I'll see how far that takes me...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it works! One thing I'm not sure about is merging - currently it just records a single flat vector format for the segment. I'm not sure if we need to handle merging segments with different flat formats, or even how we do that?

@thecoop thecoop changed the title Use a generic HNSW format for hnsw and disk BBQ Add base classes for generic HNSW formats Oct 1, 2025
@thecoop thecoop force-pushed the generic-hnsw-format branch from 17a7944 to 56f98e4 Compare October 1, 2025 12:58
@thecoop thecoop requested a review from benwtrent October 1, 2025 13:06
@thecoop thecoop force-pushed the generic-hnsw-format branch from 56f98e4 to f27430e Compare October 1, 2025 13:46
@thecoop thecoop force-pushed the generic-hnsw-format branch from f0bac52 to ddcf44f Compare October 2, 2025 08:56
Copy link
Member

@benwtrent benwtrent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are at a good phase 0 here.

Though I don't think we should allow on_disk_rescore to be set to true on things that don't support it yet.

@thecoop thecoop force-pushed the generic-hnsw-format branch 2 times, most recently from 77f4b14 to 604be7d Compare October 2, 2025 15:38
@thecoop thecoop force-pushed the generic-hnsw-format branch from 604be7d to 46f5531 Compare October 2, 2025 16:07
@thecoop thecoop force-pushed the generic-hnsw-format branch from 9ba6546 to d4231a2 Compare October 3, 2025 08:11
@thecoop thecoop force-pushed the generic-hnsw-format branch from e660793 to 0fe89c5 Compare October 3, 2025 08:28
@thecoop thecoop merged commit 38c05ea into elastic:main Oct 3, 2025
34 checks passed
@thecoop thecoop deleted the generic-hnsw-format branch October 3, 2025 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

>refactoring :Search Relevance/Vectors Vector search Team:Search Relevance Meta label for the Search Relevance team in Elasticsearch v9.3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants