From 73e3cc0110fa6e05e9ea04f117facd188ff7d270 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Wed, 9 Jul 2025 10:50:44 +0100 Subject: [PATCH 01/15] Add a direct IO option to rescore_vector for bbq_hnsw --- .../test/knn/KnnIndexTester.java | 2 +- .../DirectIOLucene99FlatVectorsFormat.java | 1 - .../ES818BinaryQuantizedVectorsFormat.java | 17 ++-- ...ES818HnswBinaryQuantizedVectorsFormat.java | 28 +++++- .../vectors/DenseVectorFieldMapper.java | 56 ++++++------ ...S818BinaryQuantizedVectorsFormatTests.java | 57 ------------ ...ctIOBinaryQuantizedVectorsFormatTests.java | 85 ++++++++++++++++++ ...HnswBinaryQuantizedVectorsFormatTests.java | 90 +++++++++++++++++++ ...HnswBinaryQuantizedVectorsFormatTests.java | 61 +------------ .../vectors/DenseVectorFieldTypeTests.java | 9 +- .../mapper/SemanticTextFieldMapper.java | 3 +- .../mapper/SemanticTextFieldMapperTests.java | 4 +- 12 files changed, 252 insertions(+), 161 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOBinaryQuantizedVectorsFormatTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java diff --git a/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java b/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java index afa155dfb2d21..3bdb6dea70d86 100644 --- a/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java +++ b/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java @@ -95,7 +95,7 @@ static Codec createCodec(CmdLineArgs args) { if (args.indexType() == IndexType.FLAT) { format = new ES818BinaryQuantizedVectorsFormat(); } else { - format = new ES818HnswBinaryQuantizedVectorsFormat(args.hnswM(), args.hnswEfConstruction(), 1, null); + format = new ES818HnswBinaryQuantizedVectorsFormat(args.hnswM(), args.hnswEfConstruction(), 1, false, null); } } else if (args.quantizeBits() < 32) { if (args.indexType() == IndexType.FLAT) { diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java index 8e328b5c500ad..2a542f6b77864 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java @@ -68,7 +68,6 @@ public FlatVectorsWriter fieldsWriter(SegmentWriteState state) throws IOExceptio } static boolean shouldUseDirectIO(SegmentReadState state) { - assert ES818BinaryQuantizedVectorsFormat.USE_DIRECT_IO; return FsDirectoryFactory.isHybridFs(state.directory); } diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java index 146164b55f00a..4baa18b580173 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java @@ -87,8 +87,6 @@ */ public class ES818BinaryQuantizedVectorsFormat extends FlatVectorsFormat { - public static final boolean USE_DIRECT_IO = Boolean.parseBoolean(System.getProperty("vector.rescoring.directio", "false")); - public static final String BINARIZED_VECTOR_COMPONENT = "BVEC"; public static final String NAME = "ES818BinaryQuantizedVectorsFormat"; @@ -100,17 +98,24 @@ public class ES818BinaryQuantizedVectorsFormat extends FlatVectorsFormat { static final String VECTOR_DATA_EXTENSION = "veb"; static final int DIRECT_MONOTONIC_BLOCK_SHIFT = 16; - private static final FlatVectorsFormat rawVectorFormat = USE_DIRECT_IO - ? new DirectIOLucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()) - : new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()); - private static final ES818BinaryFlatVectorsScorer scorer = new ES818BinaryFlatVectorsScorer( FlatVectorScorerUtil.getLucene99FlatVectorsScorer() ); + private final FlatVectorsFormat rawVectorFormat; + /** Creates a new instance with the default number of vectors per cluster. */ public ES818BinaryQuantizedVectorsFormat() { + this(false); + } + + /** Creates a new instance with the default number of vectors per cluster, + * and whether direct IO should be used to access raw vectors. */ + public ES818BinaryQuantizedVectorsFormat(boolean useDirectIO) { super(NAME); + rawVectorFormat = useDirectIO + ? new DirectIOLucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()) + : new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java index 56942017c3cef..b6c76ced89d56 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java @@ -62,14 +62,14 @@ public class ES818HnswBinaryQuantizedVectorsFormat extends KnnVectorsFormat { private final int beamWidth; /** The format for storing, reading, merging vectors on disk */ - private static final FlatVectorsFormat flatVectorsFormat = new ES818BinaryQuantizedVectorsFormat(); + private final FlatVectorsFormat flatVectorsFormat; private final int numMergeWorkers; private final TaskExecutor mergeExec; /** Constructs a format using default graph construction parameters */ public ES818HnswBinaryQuantizedVectorsFormat() { - this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, null); + this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, false, null); } /** @@ -79,7 +79,18 @@ public ES818HnswBinaryQuantizedVectorsFormat() { * @param beamWidth the size of the queue maintained during graph construction. */ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { - this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, null); + this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, false, null); + } + + /** + * Constructs a format using the given graph construction parameters. + * + * @param maxConn the maximum number of connections to a node in the HNSW graph + * @param beamWidth the size of the queue maintained during graph construction. + * @param useDirectIO whether direct IO should be used to access raw vectors + */ + public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, boolean useDirectIO) { + this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, useDirectIO, null); } /** @@ -92,7 +103,13 @@ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { * @param mergeExec the {@link ExecutorService} that will be used by ALL vector writers that are * generated by this format to do the merge */ - public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) { + public ES818HnswBinaryQuantizedVectorsFormat( + int maxConn, + int beamWidth, + int numMergeWorkers, + boolean useDirectIO, + ExecutorService mergeExec + ) { super(NAME); if (maxConn <= 0 || maxConn > MAXIMUM_MAX_CONN) { throw new IllegalArgumentException( @@ -110,6 +127,9 @@ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int num throw new IllegalArgumentException("No executor service is needed as we'll use single thread to merge"); } this.numMergeWorkers = numMergeWorkers; + + flatVectorsFormat = new ES818BinaryQuantizedVectorsFormat(useDirectIO); + if (mergeExec != null) { this.mergeExec = new TaskExecutor(mergeExec); } else { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 4d1c4fc41526c..91a86c527cd4e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -387,7 +387,7 @@ private DenseVectorIndexOptions defaultIndexOptions(boolean defaultInt8Hnsw, boo return new BBQHnswIndexOptions( Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN, Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH, - new RescoreVector(DEFAULT_OVERSAMPLE) + null ); } else if (defaultInt8Hnsw) { return new Int8HnswIndexOptions( @@ -1632,9 +1632,6 @@ public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map indexOptionsMap, IndexVersion indexVersion) { Object rescoreVectorNode = indexOptionsMap.remove(NAME); @@ -2352,26 +2345,35 @@ static RescoreVector fromIndexOptions(Map indexOptionsMap, IndexVersi return null; } Map mappedNode = XContentMapValues.nodeMapValue(rescoreVectorNode, NAME); + + Float oversampleValue = null; Object oversampleNode = mappedNode.get(OVERSAMPLE); - if (oversampleNode == null) { - throw new IllegalArgumentException("Invalid rescore_vector value. Missing required field " + OVERSAMPLE); - } - float oversampleValue = (float) XContentMapValues.nodeDoubleValue(oversampleNode); - if (oversampleValue == 0 && allowsZeroRescore(indexVersion) == false) { - throw new IllegalArgumentException("oversample must be greater than 1"); - } - if (oversampleValue < 1 && oversampleValue != 0) { - throw new IllegalArgumentException("oversample must be greater than 1 or exactly 0"); - } else if (oversampleValue > 10) { - throw new IllegalArgumentException("oversample must be less than or equal to 10"); + if (oversampleNode != null) { + oversampleValue = (float) XContentMapValues.nodeDoubleValue(oversampleNode); + if (oversampleValue == 0 && allowsZeroRescore(indexVersion) == false) { + throw new IllegalArgumentException("oversample must be greater than 1"); + } + if (oversampleValue < 1 && oversampleValue != 0) { + throw new IllegalArgumentException("oversample must be greater than 1 or exactly 0"); + } else if (oversampleValue > 10) { + throw new IllegalArgumentException("oversample must be less than or equal to 10"); + } } - return new RescoreVector(oversampleValue); + + Boolean directIO = (Boolean) mappedNode.get(DIRECT_IO); + + return new RescoreVector(oversampleValue, directIO); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(NAME); - builder.field(OVERSAMPLE, oversample); + if (oversample != null) { + builder.field(OVERSAMPLE, oversample); + } + if (useDirectIO != null) { + builder.field(DIRECT_IO, useDirectIO); + } builder.endObject(); return builder; } @@ -2710,6 +2712,10 @@ && isNotUnitVector(squaredMagnitude)) { && quantizedIndexOptions.rescoreVector != null) { oversample = quantizedIndexOptions.rescoreVector.oversample; } + if (oversample == null) { + oversample = DEFAULT_OVERSAMPLE; + } + boolean rescore = needsRescore(oversample); if (rescore) { // Will get k * oversample for rescoring, and get the top k diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java index 4a82a7e3f13e6..d0a5ddcf8a12f 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java @@ -38,7 +38,6 @@ import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.index.VectorSimilarityFunction; -import org.apache.lucene.misc.store.DirectIODirectory; import org.apache.lucene.search.FieldExistsQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.KnnFloatVectorQuery; @@ -52,32 +51,19 @@ import org.apache.lucene.search.join.DiversifyingChildrenFloatKnnVectorQuery; import org.apache.lucene.search.join.QueryBitSetProducer; import org.apache.lucene.store.Directory; -import org.apache.lucene.store.FSDirectory; -import org.apache.lucene.store.IOContext; -import org.apache.lucene.store.IndexOutput; import org.apache.lucene.store.MMapDirectory; import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; import org.apache.lucene.tests.store.MockDirectoryWrapper; import org.apache.lucene.tests.util.TestUtil; import org.elasticsearch.common.logging.LogConfigurator; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.IndexModule; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.codec.vectors.BQVectorUtils; import org.elasticsearch.index.codec.vectors.OptimizedScalarQuantizer; -import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.index.shard.ShardPath; -import org.elasticsearch.index.store.FsDirectoryFactory; -import org.elasticsearch.test.IndexSettingsModule; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.OptionalLong; import static java.lang.String.format; import static org.apache.lucene.index.VectorSimilarityFunction.DOT_PRODUCT; @@ -268,14 +254,6 @@ public void testSimpleOffHeapSize() throws IOException { } } - public void testSimpleOffHeapSizeFSDir() throws IOException { - checkDirectIOSupported(); - var config = newIndexWriterConfig().setUseCompoundFile(false); // avoid compound files to allow directIO - try (Directory dir = newFSDirectory()) { - testSimpleOffHeapSizeImpl(dir, config, false); - } - } - public void testSimpleOffHeapSizeMMapDir() throws IOException { try (Directory dir = newMMapDirectory()) { testSimpleOffHeapSizeImpl(dir, newIndexWriterConfig(), true); @@ -315,39 +293,4 @@ static Directory newMMapDirectory() throws IOException { } return dir; } - - private Directory newFSDirectory() throws IOException { - Settings settings = Settings.builder() - .put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), IndexModule.Type.HYBRIDFS.name().toLowerCase(Locale.ROOT)) - .build(); - IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("foo", settings); - Path tempDir = createTempDir().resolve(idxSettings.getUUID()).resolve("0"); - Files.createDirectories(tempDir); - ShardPath path = new ShardPath(false, tempDir, tempDir, new ShardId(idxSettings.getIndex(), 0)); - Directory dir = (new FsDirectoryFactory()).newDirectory(idxSettings, path); - if (random().nextBoolean()) { - dir = new MockDirectoryWrapper(random(), dir); - } - return dir; - } - - static void checkDirectIOSupported() { - assumeTrue("Direct IO is not enabled", ES818BinaryQuantizedVectorsFormat.USE_DIRECT_IO); - - Path path = createTempDir("directIOProbe"); - try (Directory dir = open(path); IndexOutput out = dir.createOutput("out", IOContext.DEFAULT)) { - out.writeString("test"); - } catch (IOException e) { - assumeNoException("test requires a filesystem that supports Direct IO", e); - } - } - - static DirectIODirectory open(Path path) throws IOException { - return new DirectIODirectory(FSDirectory.open(path)) { - @Override - protected boolean useDirectIO(String name, IOContext context, OptionalLong fileLength) { - return true; - } - }; - } } diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOBinaryQuantizedVectorsFormatTests.java new file mode 100644 index 0000000000000..4f27815a576df --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOBinaryQuantizedVectorsFormatTests.java @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.misc.store.DirectIODirectory; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.tests.store.MockDirectoryWrapper; +import org.apache.lucene.tests.util.TestUtil; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexModule; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.shard.ShardPath; +import org.elasticsearch.index.store.FsDirectoryFactory; +import org.elasticsearch.test.IndexSettingsModule; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.OptionalLong; + +public class ES818DirectIOBinaryQuantizedVectorsFormatTests extends ES818BinaryQuantizedVectorsFormatTests { + + static final Codec codec = TestUtil.alwaysKnnVectorsFormat(new ES818BinaryQuantizedVectorsFormat(true)); + + @Override + protected Codec getCodec() { + return codec; + } + + @BeforeClass + public static void checkDirectIOSupport() { + Path path = createTempDir("directIOProbe"); + try (Directory dir = open(path); IndexOutput out = dir.createOutput("out", IOContext.DEFAULT)) { + out.writeString("test"); + } catch (IOException e) { + assumeNoException("test requires a filesystem that supports Direct IO", e); + } + } + + static DirectIODirectory open(Path path) throws IOException { + return new DirectIODirectory(FSDirectory.open(path)) { + @Override + protected boolean useDirectIO(String name, IOContext context, OptionalLong fileLength) { + return true; + } + }; + } + + @Override + public void testSimpleOffHeapSize() throws IOException { + var config = newIndexWriterConfig().setUseCompoundFile(false); // avoid compound files to allow directIO + try (Directory dir = newFSDirectory()) { + testSimpleOffHeapSizeImpl(dir, config, false); + } + } + + private Directory newFSDirectory() throws IOException { + Settings settings = Settings.builder() + .put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), IndexModule.Type.HYBRIDFS.name().toLowerCase(Locale.ROOT)) + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("foo", settings); + Path tempDir = createTempDir().resolve(idxSettings.getUUID()).resolve("0"); + Files.createDirectories(tempDir); + ShardPath path = new ShardPath(false, tempDir, tempDir, new ShardId(idxSettings.getIndex(), 0)); + Directory dir = (new FsDirectoryFactory()).newDirectory(idxSettings, path); + if (random().nextBoolean()) { + dir = new MockDirectoryWrapper(random(), dir); + } + return dir; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java new file mode 100644 index 0000000000000..7140947049bb8 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.misc.store.DirectIODirectory; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.tests.store.MockDirectoryWrapper; +import org.apache.lucene.tests.util.TestUtil; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexModule; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.shard.ShardPath; +import org.elasticsearch.index.store.FsDirectoryFactory; +import org.elasticsearch.test.IndexSettingsModule; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.OptionalLong; + +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; + +public class ES818DirectIOHnswBinaryQuantizedVectorsFormatTests extends ES818HnswBinaryQuantizedVectorsFormatTests { + + static final Codec codec = TestUtil.alwaysKnnVectorsFormat( + new ES818HnswBinaryQuantizedVectorsFormat(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, true) + ); + + @Override + protected Codec getCodec() { + return codec; + } + + @BeforeClass + public static void checkDirectIOSupport() { + Path path = createTempDir("directIOProbe"); + try (Directory dir = open(path); IndexOutput out = dir.createOutput("out", IOContext.DEFAULT)) { + out.writeString("test"); + } catch (IOException e) { + assumeNoException("test requires a filesystem that supports Direct IO", e); + } + } + + static DirectIODirectory open(Path path) throws IOException { + return new DirectIODirectory(FSDirectory.open(path)) { + @Override + protected boolean useDirectIO(String name, IOContext context, OptionalLong fileLength) { + return true; + } + }; + } + + @Override + public void testSimpleOffHeapSize() throws IOException { + var config = newIndexWriterConfig().setUseCompoundFile(false); // avoid compound files to allow directIO + try (Directory dir = newFSDirectory()) { + testSimpleOffHeapSizeImpl(dir, config, false); + } + } + + private Directory newFSDirectory() throws IOException { + Settings settings = Settings.builder() + .put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), IndexModule.Type.HYBRIDFS.name().toLowerCase(Locale.ROOT)) + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("foo", settings); + Path tempDir = createTempDir().resolve(idxSettings.getUUID()).resolve("0"); + Files.createDirectories(tempDir); + ShardPath path = new ShardPath(false, tempDir, tempDir, new ShardId(idxSettings.getIndex(), 0)); + Directory dir = (new FsDirectoryFactory()).newDirectory(idxSettings, path); + if (random().nextBoolean()) { + dir = new MockDirectoryWrapper(random(), dir); + } + return dir; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java index 35bac97013487..8197423b742c4 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java @@ -36,32 +36,18 @@ import org.apache.lucene.index.KnnVectorValues; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.VectorSimilarityFunction; -import org.apache.lucene.misc.store.DirectIODirectory; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; -import org.apache.lucene.store.FSDirectory; -import org.apache.lucene.store.IOContext; -import org.apache.lucene.store.IndexOutput; import org.apache.lucene.store.MMapDirectory; import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; import org.apache.lucene.tests.store.MockDirectoryWrapper; import org.apache.lucene.tests.util.TestUtil; import org.apache.lucene.util.SameThreadExecutorService; import org.elasticsearch.common.logging.LogConfigurator; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.IndexModule; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.index.shard.ShardPath; -import org.elasticsearch.index.store.FsDirectoryFactory; -import org.elasticsearch.test.IndexSettingsModule; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.Locale; -import java.util.OptionalLong; import static java.lang.String.format; import static org.apache.lucene.index.VectorSimilarityFunction.DOT_PRODUCT; @@ -87,7 +73,7 @@ public void testToString() { FilterCodec customCodec = new FilterCodec("foo", Codec.getDefault()) { @Override public KnnVectorsFormat knnVectorsFormat() { - return new ES818HnswBinaryQuantizedVectorsFormat(10, 20, 1, null); + return new ES818HnswBinaryQuantizedVectorsFormat(10, 20, 1, false, null); } }; String expectedPattern = @@ -137,7 +123,7 @@ public void testLimits() { expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 3201)); expectThrows( IllegalArgumentException.class, - () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 100, 1, new SameThreadExecutorService()) + () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 100, 1, false, new SameThreadExecutorService()) ); } @@ -155,14 +141,6 @@ public void testSimpleOffHeapSize() throws IOException { } } - public void testSimpleOffHeapSizeFSDir() throws IOException { - checkDirectIOSupported(); - var config = newIndexWriterConfig().setUseCompoundFile(false); // avoid compound files to allow directIO - try (Directory dir = newFSDirectory()) { - testSimpleOffHeapSizeImpl(dir, config, false); - } - } - public void testSimpleOffHeapSizeMMapDir() throws IOException { try (Directory dir = newMMapDirectory()) { testSimpleOffHeapSizeImpl(dir, newIndexWriterConfig(), true); @@ -203,39 +181,4 @@ static Directory newMMapDirectory() throws IOException { } return dir; } - - private Directory newFSDirectory() throws IOException { - Settings settings = Settings.builder() - .put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), IndexModule.Type.HYBRIDFS.name().toLowerCase(Locale.ROOT)) - .build(); - IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("foo", settings); - Path tempDir = createTempDir().resolve(idxSettings.getUUID()).resolve("0"); - Files.createDirectories(tempDir); - ShardPath path = new ShardPath(false, tempDir, tempDir, new ShardId(idxSettings.getIndex(), 0)); - Directory dir = (new FsDirectoryFactory()).newDirectory(idxSettings, path); - if (random().nextBoolean()) { - dir = new MockDirectoryWrapper(random(), dir); - } - return dir; - } - - static void checkDirectIOSupported() { - assumeTrue("Direct IO is not enabled", ES818BinaryQuantizedVectorsFormat.USE_DIRECT_IO); - - Path path = createTempDir("directIOProbe"); - try (Directory dir = open(path); IndexOutput out = dir.createOutput("out", IOContext.DEFAULT)) { - out.writeString("test"); - } catch (IOException e) { - assumeNoException("test requires a filesystem that supports Direct IO", e); - } - } - - static DirectIODirectory open(Path path) throws IOException { - return new DirectIODirectory(FSDirectory.open(path)) { - @Override - protected boolean useDirectIO(String name, IOContext context, OptionalLong fileLength) { - return true; - } - }; - } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java index 2524422ed8f90..8e67b7f414023 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java @@ -55,7 +55,10 @@ public DenseVectorFieldTypeTests() { } private static DenseVectorFieldMapper.RescoreVector randomRescoreVector() { - return new DenseVectorFieldMapper.RescoreVector(randomBoolean() ? 0 : randomFloatBetween(1.0F, 10.0F, false)); + return new DenseVectorFieldMapper.RescoreVector( + randomBoolean() ? 0 : randomFloatBetween(1.0F, 10.0F, false), + randomOptionalBoolean() + ); } private DenseVectorFieldMapper.DenseVectorIndexOptions randomIndexOptionsNonQuantized() { @@ -663,7 +666,7 @@ public void testRescoreOversampleQueryOverrides() { 3, true, VectorSimilarity.COSINE, - randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(randomFloatBetween(1.1f, 9.9f, false))), + randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(randomFloatBetween(1.1f, 9.9f, false), null)), Collections.emptyMap(), false ); @@ -692,7 +695,7 @@ public void testRescoreOversampleQueryOverrides() { 3, true, VectorSimilarity.COSINE, - randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(0)), + randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(0f, null)), Collections.emptyMap(), false ); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java index 9972fa9e5ae0b..4cf784edd260e 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java @@ -1254,8 +1254,7 @@ static boolean indexVersionDefaultsToBbqHnsw(IndexVersion indexVersion) { public static DenseVectorFieldMapper.DenseVectorIndexOptions defaultBbqHnswDenseVectorIndexOptions() { int m = Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; int efConstruction = Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; - DenseVectorFieldMapper.RescoreVector rescoreVector = new DenseVectorFieldMapper.RescoreVector(DEFAULT_RESCORE_OVERSAMPLE); - return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, rescoreVector); + return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, null); } static SemanticTextIndexOptions defaultIndexOptions(IndexVersion indexVersionCreated, MinimalServiceSettings modelSettings) { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java index cc87edf59e9d3..a573bcc863de3 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java @@ -105,7 +105,6 @@ import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getChunksFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_ELSER_2_INFERENCE_ID; -import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_RESCORE_OVERSAMPLE; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.INDEX_OPTIONS_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettings; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettingsOtherThan; @@ -1185,8 +1184,7 @@ private static SemanticTextIndexOptions defaultDenseVectorSemanticIndexOptions() private static DenseVectorFieldMapper.DenseVectorIndexOptions defaultBbqHnswDenseVectorIndexOptions() { int m = Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; int efConstruction = Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; - DenseVectorFieldMapper.RescoreVector rescoreVector = new DenseVectorFieldMapper.RescoreVector(DEFAULT_RESCORE_OVERSAMPLE); - return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, rescoreVector); + return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, null); } private static SemanticTextIndexOptions defaultBbqHnswSemanticTextIndexOptions() { From 3776a01e46692c10e7fea3360e1e721b66b23d9c Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Mon, 14 Jul 2025 14:24:45 +0100 Subject: [PATCH 02/15] Use a separate option --- .../ES818BinaryQuantizedVectorsFormat.java | 1 - .../vectors/DenseVectorFieldMapper.java | 79 +++++++++++-------- .../vectors/DenseVectorFieldTypeTests.java | 19 +++-- .../mapper/SemanticTextFieldMapper.java | 3 +- .../mapper/SemanticTextFieldMapperTests.java | 4 +- 5 files changed, 60 insertions(+), 46 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java index cb68772ae1f9e..4baa18b580173 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java @@ -26,7 +26,6 @@ import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat; import org.apache.lucene.index.SegmentReadState; import org.apache.lucene.index.SegmentWriteState; -import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.index.codec.vectors.OptimizedScalarQuantizer; import java.io.IOException; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 4f1373866485a..2c7c31857f13f 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -388,7 +388,8 @@ private DenseVectorIndexOptions defaultIndexOptions(boolean defaultInt8Hnsw, boo return new BBQHnswIndexOptions( Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN, Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH, - null + new RescoreVector(DEFAULT_OVERSAMPLE), + false ); } else if (defaultInt8Hnsw) { return new Int8HnswIndexOptions( @@ -1622,6 +1623,8 @@ public boolean supportsDimension(int dims) { public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map indexOptionsMap, IndexVersion indexVersion) { Object mNode = indexOptionsMap.remove("m"); Object efConstructionNode = indexOptionsMap.remove("ef_construction"); + Object useDirectIONode = indexOptionsMap.remove("use_direct_io"); + if (mNode == null) { mNode = Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; } @@ -1630,12 +1633,19 @@ public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map indexOptionsMap, IndexVersion indexVersion) { Object rescoreVectorNode = indexOptionsMap.remove(NAME); @@ -2346,35 +2368,26 @@ static RescoreVector fromIndexOptions(Map indexOptionsMap, IndexVersi return null; } Map mappedNode = XContentMapValues.nodeMapValue(rescoreVectorNode, NAME); - - Float oversampleValue = null; Object oversampleNode = mappedNode.get(OVERSAMPLE); - if (oversampleNode != null) { - oversampleValue = (float) XContentMapValues.nodeDoubleValue(oversampleNode); - if (oversampleValue == 0 && allowsZeroRescore(indexVersion) == false) { - throw new IllegalArgumentException("oversample must be greater than 1"); - } - if (oversampleValue < 1 && oversampleValue != 0) { - throw new IllegalArgumentException("oversample must be greater than 1 or exactly 0"); - } else if (oversampleValue > 10) { - throw new IllegalArgumentException("oversample must be less than or equal to 10"); - } + if (oversampleNode == null) { + throw new IllegalArgumentException("Invalid rescore_vector value. Missing required field " + OVERSAMPLE); } - - Boolean directIO = (Boolean) mappedNode.get(DIRECT_IO); - - return new RescoreVector(oversampleValue, directIO); + float oversampleValue = (float) XContentMapValues.nodeDoubleValue(oversampleNode); + if (oversampleValue == 0 && allowsZeroRescore(indexVersion) == false) { + throw new IllegalArgumentException("oversample must be greater than 1"); + } + if (oversampleValue < 1 && oversampleValue != 0) { + throw new IllegalArgumentException("oversample must be greater than 1 or exactly 0"); + } else if (oversampleValue > 10) { + throw new IllegalArgumentException("oversample must be less than or equal to 10"); + } + return new RescoreVector(oversampleValue); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(NAME); - if (oversample != null) { - builder.field(OVERSAMPLE, oversample); - } - if (useDirectIO != null) { - builder.field(DIRECT_IO, useDirectIO); - } + builder.field(OVERSAMPLE, oversample); builder.endObject(); return builder; } @@ -2716,10 +2729,6 @@ && isNotUnitVector(squaredMagnitude)) { && quantizedIndexOptions.rescoreVector != null) { oversample = quantizedIndexOptions.rescoreVector.oversample; } - if (oversample == null) { - oversample = DEFAULT_OVERSAMPLE; - } - boolean rescore = needsRescore(oversample); if (rescore) { // Will get k * oversample for rescoring, and get the top k diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java index 8e67b7f414023..f0753203a4da4 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java @@ -55,10 +55,7 @@ public DenseVectorFieldTypeTests() { } private static DenseVectorFieldMapper.RescoreVector randomRescoreVector() { - return new DenseVectorFieldMapper.RescoreVector( - randomBoolean() ? 0 : randomFloatBetween(1.0F, 10.0F, false), - randomOptionalBoolean() - ); + return new DenseVectorFieldMapper.RescoreVector(randomBoolean() ? 0 : randomFloatBetween(1.0F, 10.0F, false)); } private DenseVectorFieldMapper.DenseVectorIndexOptions randomIndexOptionsNonQuantized() { @@ -95,7 +92,8 @@ public static DenseVectorFieldMapper.DenseVectorIndexOptions randomIndexOptionsA new DenseVectorFieldMapper.BBQHnswIndexOptions( randomIntBetween(1, 100), randomIntBetween(1, 10_000), - randomFrom((DenseVectorFieldMapper.RescoreVector) null, randomRescoreVector()) + randomFrom((DenseVectorFieldMapper.RescoreVector) null, randomRescoreVector()), + randomBoolean() ), new DenseVectorFieldMapper.BBQFlatIndexOptions(randomFrom((DenseVectorFieldMapper.RescoreVector) null, randomRescoreVector())) ); @@ -121,7 +119,12 @@ private DenseVectorFieldMapper.DenseVectorIndexOptions randomIndexOptionsHnswQua randomFrom((Float) null, 0f, (float) randomDoubleBetween(0.9, 1.0, true)), rescoreVector ), - new DenseVectorFieldMapper.BBQHnswIndexOptions(randomIntBetween(1, 100), randomIntBetween(1, 10_000), rescoreVector) + new DenseVectorFieldMapper.BBQHnswIndexOptions( + randomIntBetween(1, 100), + randomIntBetween(1, 10_000), + rescoreVector, + randomBoolean() + ) ); } @@ -666,7 +669,7 @@ public void testRescoreOversampleQueryOverrides() { 3, true, VectorSimilarity.COSINE, - randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(randomFloatBetween(1.1f, 9.9f, false), null)), + randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(randomFloatBetween(1.1f, 9.9f, false))), Collections.emptyMap(), false ); @@ -695,7 +698,7 @@ public void testRescoreOversampleQueryOverrides() { 3, true, VectorSimilarity.COSINE, - randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(0f, null)), + randomIndexOptionsHnswQuantized(new DenseVectorFieldMapper.RescoreVector(0)), Collections.emptyMap(), false ); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java index 4cf784edd260e..6f3b36d25f31f 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java @@ -1254,7 +1254,8 @@ static boolean indexVersionDefaultsToBbqHnsw(IndexVersion indexVersion) { public static DenseVectorFieldMapper.DenseVectorIndexOptions defaultBbqHnswDenseVectorIndexOptions() { int m = Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; int efConstruction = Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; - return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, null); + DenseVectorFieldMapper.RescoreVector rescoreVector = new DenseVectorFieldMapper.RescoreVector(DEFAULT_RESCORE_OVERSAMPLE); + return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, rescoreVector, false); } static SemanticTextIndexOptions defaultIndexOptions(IndexVersion indexVersionCreated, MinimalServiceSettings modelSettings) { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java index a573bcc863de3..3c7a62ad62411 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java @@ -105,6 +105,7 @@ import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getChunksFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_ELSER_2_INFERENCE_ID; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_RESCORE_OVERSAMPLE; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.INDEX_OPTIONS_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettings; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettingsOtherThan; @@ -1184,7 +1185,8 @@ private static SemanticTextIndexOptions defaultDenseVectorSemanticIndexOptions() private static DenseVectorFieldMapper.DenseVectorIndexOptions defaultBbqHnswDenseVectorIndexOptions() { int m = Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; int efConstruction = Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; - return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, null); + DenseVectorFieldMapper.RescoreVector rescoreVector = new DenseVectorFieldMapper.RescoreVector(DEFAULT_RESCORE_OVERSAMPLE); + return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, rescoreVector, false); } private static SemanticTextIndexOptions defaultBbqHnswSemanticTextIndexOptions() { From 31acb0148a6596a58fd192677f1a78bd8b23d117 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Mon, 14 Jul 2025 17:11:34 +0100 Subject: [PATCH 03/15] Rename option, add basic tests --- .../upgrades/VectorSearchIT.java | 59 +++++++++++++++++ .../search.vectors/41_knn_search_bbq_hnsw.yml | 63 +++++++++++++++++++ .../vectors/DenseVectorFieldMapper.java | 22 +++---- .../elasticsearch/search/SearchFeatures.java | 4 +- 4 files changed, 136 insertions(+), 12 deletions(-) diff --git a/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java b/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java index afee17cd82e2d..e79b8eb0eda65 100644 --- a/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java +++ b/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java @@ -16,6 +16,7 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.search.SearchFeatures; import java.io.IOException; import java.util.List; @@ -35,6 +36,7 @@ public VectorSearchIT(@Name("upgradedNodes") int upgradedNodes) { private static final String BYTE_INDEX_NAME = "byte_vector_index"; private static final String QUANTIZED_INDEX_NAME = "quantized_vector_index"; private static final String BBQ_INDEX_NAME = "bbq_vector_index"; + private static final String BBQ_INDEX_NAME_RESCORE = "bbq_vector_index_rescore"; private static final String FLAT_QUANTIZED_INDEX_NAME = "flat_quantized_vector_index"; private static final String FLAT_BBQ_INDEX_NAME = "flat_bbq_vector_index"; @@ -507,6 +509,63 @@ public void testBBQVectorSearch() throws Exception { ); } + public void testBBQVectorSearchOffheapRescoring() throws Exception { + assumeTrue("Disabling off-heap rescoring is not supported", oldClusterHasFeature(SearchFeatures.BBQ_OFFHEAP_RESCORING)); + if (isOldCluster()) { + String mapping = """ + { + "properties": { + "vector": { + "type": "dense_vector", + "dims": 64, + "index": true, + "similarity": "cosine", + "index_options": { + "type": "bbq_hnsw", + "ef_construction": 100, + "m": 16, + "disable_offheap_cache_rescoring": true + } + } + } + } + """; + // create index and index 10 random floating point vectors + createIndex( + BBQ_INDEX_NAME_RESCORE, + Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1).put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0).build(), + mapping + ); + index64DimVectors(BBQ_INDEX_NAME_RESCORE); + // force merge the index + client().performRequest(new Request("POST", "/" + BBQ_INDEX_NAME_RESCORE + "/_forcemerge?max_num_segments=1")); + } + Request searchRequest = new Request("POST", "/" + BBQ_INDEX_NAME_RESCORE + "/_search"); + searchRequest.setJsonEntity(""" + { + "knn": { + "field": "vector", + "query_vector": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6], + "k": 2, + "num_candidates": 5, + "rescore_vector": { + "oversample": 2.0 + } + } + } + """); + Map response = search(searchRequest); + assertThat(extractValue(response, "hits.total.value"), equalTo(2)); + List> hits = extractValue(response, "hits.hits"); + assertThat("expected: 0 received" + hits.get(0).get("_id") + " hits: " + response, hits.get(0).get("_id"), equalTo("0")); + assertThat( + "expected_near: 0.99 received" + hits.get(0).get("_score") + "hits: " + response, + (double) hits.get(0).get("_score"), + closeTo(0.9934857, 0.005) + ); + } + public void testFlatBBQVectorSearch() throws Exception { assumeTrue("Quantized vector search is not supported on this version", oldClusterHasFeature(BBQ_VECTOR_SEARCH_TEST_FEATURE)); if (isOldCluster()) { diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml index e3c1155ed2000..7ccdf3131158d 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml @@ -338,6 +338,69 @@ setup: - match: { hits.hits.1._score: $rescore_score1 } - match: { hits.hits.2._score: $rescore_score2 } --- +"Test index configured rescore vector with no off-heap scoring": + - requires: + cluster_features: ["search.vectors.bbq_offheap_rescoring"] + reason: Needs bbq_offheap_rescoring feature + - skip: + features: "headers" + - do: + indices.create: + index: bbq_rescore_hnsw + body: + settings: + index: + number_of_shards: 1 + mappings: + properties: + vector: + type: dense_vector + dims: 64 + index: true + similarity: max_inner_product + index_options: + type: bbq_hnsw + disable_offheap_cache_rescoring: true + rescore_vector: + oversample: 1.5 + + - do: + bulk: + index: bbq_rescore_hnsw + refresh: true + body: | + { "index": {"_id": "1"}} + { "vector": [0.077, 0.32 , -0.205, 0.63 , 0.032, 0.201, 0.167, -0.313, 0.176, 0.531, -0.375, 0.334, -0.046, 0.078, -0.349, 0.272, 0.307, -0.083, 0.504, 0.255, -0.404, 0.289, -0.226, -0.132, -0.216, 0.49 , 0.039, 0.507, -0.307, 0.107, 0.09 , -0.265, -0.285, 0.336, -0.272, 0.369, -0.282, 0.086, -0.132, 0.475, -0.224, 0.203, 0.439, 0.064, 0.246, -0.396, 0.297, 0.242, -0.028, 0.321, -0.022, -0.009, -0.001 , 0.031, -0.533, 0.45, -0.683, 1.331, 0.194, -0.157, -0.1 , -0.279, -0.098, -0.176] } + { "index": {"_id": "2"}} + { "vector": [0.196, 0.514, 0.039, 0.555, -0.042, 0.242, 0.463, -0.348, -0.08 , 0.442, -0.067, -0.05 , -0.001, 0.298, -0.377, 0.048, 0.307, 0.159, 0.278, 0.119, -0.057, 0.333, -0.289, -0.438, -0.014, 0.361, -0.169, 0.292, -0.229, 0.123, 0.031, -0.138, -0.139, 0.315, -0.216, 0.322, -0.445, -0.059, 0.071, 0.429, -0.602, -0.142, 0.11 , 0.192, 0.259, -0.241, 0.181, -0.166, 0.082, 0.107, -0.05 , 0.155, 0.011, 0.161, -0.486, 0.569, -0.489, 0.901, 0.208, 0.011, -0.209, -0.153, -0.27 , -0.013] } + { "index": {"_id": "3"}} + { "vector": [0.196, 0.514, 0.039, 0.555, -0.042, 0.242, 0.463, -0.348, -0.08 , 0.442, -0.067, -0.05 , -0.001, 0.298, -0.377, 0.048, 0.307, 0.159, 0.278, 0.119, -0.057, 0.333, -0.289, -0.438, -0.014, 0.361, -0.169, 0.292, -0.229, 0.123, 0.031, -0.138, -0.139, 0.315, -0.216, 0.322, -0.445, -0.059, 0.071, 0.429, -0.602, -0.142, 0.11 , 0.192, 0.259, -0.241, 0.181, -0.166, 0.082, 0.107, -0.05 , 0.155, 0.011, 0.161, -0.486, 0.569, -0.489, 0.901, 0.208, 0.011, -0.209, -0.153, -0.27 , -0.013] } + + - do: + headers: + Content-Type: application/json + search: + rest_total_hits_as_int: true + index: bbq_rescore_hnsw + body: + knn: + field: vector + query_vector: [0.128, 0.067, -0.08 , 0.395, -0.11 , -0.259, 0.473, -0.393, + 0.292, 0.571, -0.491, 0.444, -0.288, 0.198, -0.343, 0.015, + 0.232, 0.088, 0.228, 0.151, -0.136, 0.236, -0.273, -0.259, + -0.217, 0.359, -0.207, 0.352, -0.142, 0.192, -0.061, -0.17 , + -0.343, 0.189, -0.221, 0.32 , -0.301, -0.1 , 0.005, 0.232, + -0.344, 0.136, 0.252, 0.157, -0.13 , -0.244, 0.193, -0.034, + -0.12 , -0.193, -0.102, 0.252, -0.185, -0.167, -0.575, 0.582, + -0.426, 0.983, 0.212, 0.204, 0.03 , -0.276, -0.425, -0.158] + k: 3 + num_candidates: 3 + + - match: { hits.total: 3 } + - set: { hits.hits.0._score: rescore_score0 } + - set: { hits.hits.1._score: rescore_score1 } + - set: { hits.hits.2._score: rescore_score2 } +--- "Test index configured rescore vector updateable and settable to 0": - requires: cluster_features: ["mapper.dense_vector.rescore_zero_vector"] diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 2c7c31857f13f..6ad4bc50be242 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -1623,7 +1623,7 @@ public boolean supportsDimension(int dims) { public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map indexOptionsMap, IndexVersion indexVersion) { Object mNode = indexOptionsMap.remove("m"); Object efConstructionNode = indexOptionsMap.remove("ef_construction"); - Object useDirectIONode = indexOptionsMap.remove("use_direct_io"); + Object disableOffheapCacheRescoringNode = indexOptionsMap.remove("disable_offheap_cache_rescoring"); if (mNode == null) { mNode = Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; @@ -1642,10 +1642,10 @@ public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map getFeatures() { static final NodeFeature MULTI_MATCH_CHECKS_POSITIONS = new NodeFeature("search.multi.match.checks.positions"); public static final NodeFeature BBQ_HNSW_DEFAULT_INDEXING = new NodeFeature("search.vectors.mappers.default_bbq_hnsw"); public static final NodeFeature SEARCH_WITH_NO_DIMENSIONS_BUGFIX = new NodeFeature("search.vectors.no_dimensions_bugfix"); + public static final NodeFeature BBQ_OFFHEAP_RESCORING = new NodeFeature("search.vectors.bbq_offheap_rescoring"); @Override public Set getTestFeatures() { @@ -43,7 +44,8 @@ public Set getTestFeatures() { INT_SORT_FOR_INT_SHORT_BYTE_FIELDS, MULTI_MATCH_CHECKS_POSITIONS, BBQ_HNSW_DEFAULT_INDEXING, - SEARCH_WITH_NO_DIMENSIONS_BUGFIX + SEARCH_WITH_NO_DIMENSIONS_BUGFIX, + BBQ_OFFHEAP_RESCORING ); } } From 1369293108c7701b664c1ee55b80f2fe6c380427 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Fri, 18 Jul 2025 15:32:08 +0100 Subject: [PATCH 04/15] Add more test --- .../java/org/elasticsearch/index/store/DirectIOIT.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java b/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java index 600555320dc02..c1d5eb985e4c1 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java @@ -47,8 +47,6 @@ public class DirectIOIT extends ESIntegTestCase { @BeforeClass public static void checkSupported() { - assumeTrue("Direct IO is not enabled", ES818BinaryQuantizedVectorsFormat.USE_DIRECT_IO); - Path path = createTempDir("directIOProbe"); try (Directory dir = open(path); IndexOutput out = dir.createOutput("out", IOContext.DEFAULT)) { out.writeString("test"); @@ -73,7 +71,7 @@ protected Collection> nodePlugins() { } private void indexVectors() { - String type = randomFrom("bbq_flat", "bbq_hnsw"); + String type = randomFrom(/*"bbq_flat", */"bbq_hnsw"); assertAcked( prepareCreate("foo-vectors").setSettings(Settings.builder().put(InternalSettingsPlugin.USE_COMPOUND_FILE.getKey(), false)) .setMapping(""" @@ -86,7 +84,8 @@ private void indexVectors() { "index": true, "similarity": "l2_norm", "index_options": { - "type": "%type%" + "type": "%type%", + "disable_offheap_cache_rescoring": true } } } From a5a23e5edc06977e1329b62228d86f1fa92ce7d0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 12 Aug 2025 14:02:42 +0000 Subject: [PATCH 05/15] [CI] Auto commit changes from spotless --- .../java/org/elasticsearch/index/store/DirectIOIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java b/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java index 817a5bc8f479a..5db87c13529e2 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java @@ -17,7 +17,6 @@ import org.apache.lucene.store.IndexOutput; import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.vectors.KnnSearchBuilder; import org.elasticsearch.search.vectors.VectorData; From 9b02a481b1f908421270bb15919af5c3330cff34 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 19 Aug 2025 10:40:42 +0100 Subject: [PATCH 06/15] Change to a mapper feature rather than search feature --- .../test/search.vectors/41_knn_search_bbq_hnsw.yml | 2 +- .../java/org/elasticsearch/index/mapper/MapperFeatures.java | 4 +++- .../main/java/org/elasticsearch/search/SearchFeatures.java | 4 +--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml index 7ccdf3131158d..e4b89d669d91c 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml @@ -340,7 +340,7 @@ setup: --- "Test index configured rescore vector with no off-heap scoring": - requires: - cluster_features: ["search.vectors.bbq_offheap_rescoring"] + cluster_features: ["mapper.vectors.bbq_offheap_rescoring"] reason: Needs bbq_offheap_rescoring feature - skip: features: "headers" diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java index 7ba2dfb9a69f5..5a6b89e086bb3 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java @@ -47,6 +47,7 @@ public class MapperFeatures implements FeatureSpecification { static final NodeFeature BBQ_DISK_SUPPORT = new NodeFeature("mapper.bbq_disk_support"); static final NodeFeature SEARCH_LOAD_PER_SHARD = new NodeFeature("mapper.search_load_per_shard"); static final NodeFeature PATTERNED_TEXT = new NodeFeature("mapper.patterned_text"); + static final NodeFeature BBQ_OFFHEAP_RESCORING = new NodeFeature("mapper.vectors.bbq_offheap_rescoring"); @Override public Set getTestFeatures() { @@ -80,7 +81,8 @@ public Set getTestFeatures() { BBQ_DISK_SUPPORT, SEARCH_LOAD_PER_SHARD, SPARSE_VECTOR_INDEX_OPTIONS_FEATURE, - PATTERNED_TEXT + PATTERNED_TEXT, + BBQ_OFFHEAP_RESCORING ); } } diff --git a/server/src/main/java/org/elasticsearch/search/SearchFeatures.java b/server/src/main/java/org/elasticsearch/search/SearchFeatures.java index 0a30bc6044b49..80ccd4c188538 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchFeatures.java +++ b/server/src/main/java/org/elasticsearch/search/SearchFeatures.java @@ -33,7 +33,6 @@ public Set getFeatures() { static final NodeFeature MULTI_MATCH_CHECKS_POSITIONS = new NodeFeature("search.multi.match.checks.positions"); public static final NodeFeature BBQ_HNSW_DEFAULT_INDEXING = new NodeFeature("search.vectors.mappers.default_bbq_hnsw"); public static final NodeFeature SEARCH_WITH_NO_DIMENSIONS_BUGFIX = new NodeFeature("search.vectors.no_dimensions_bugfix"); - public static final NodeFeature BBQ_OFFHEAP_RESCORING = new NodeFeature("search.vectors.bbq_offheap_rescoring"); @Override public Set getTestFeatures() { @@ -44,8 +43,7 @@ public Set getTestFeatures() { INT_SORT_FOR_INT_SHORT_BYTE_FIELDS, MULTI_MATCH_CHECKS_POSITIONS, BBQ_HNSW_DEFAULT_INDEXING, - SEARCH_WITH_NO_DIMENSIONS_BUGFIX, - BBQ_OFFHEAP_RESCORING + SEARCH_WITH_NO_DIMENSIONS_BUGFIX ); } } From 5b8d4993fed0eac56c874cfa50b8f26b78dcba58 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 19 Aug 2025 12:13:31 +0100 Subject: [PATCH 07/15] Create new formats for direct IO access --- .../test/knn/KnnIndexTester.java | 2 +- server/src/main/java/module-info.java | 2 + ...ctIOES818BinaryQuantizedVectorsFormat.java | 24 +++++++++ ...ES818HnswBinaryQuantizedVectorsFormat.java | 50 +++++++++++++++++++ .../ES818BinaryQuantizedVectorsFormat.java | 16 +++--- ...ES818HnswBinaryQuantizedVectorsFormat.java | 33 ++++++------ .../vectors/DenseVectorFieldMapper.java | 5 +- .../org.apache.lucene.codecs.KnnVectorsFormat | 2 + ...818BinaryQuantizedVectorsFormatTests.java} | 4 +- ...nswBinaryQuantizedVectorsFormatTests.java} | 4 +- ...HnswBinaryQuantizedVectorsFormatTests.java | 4 +- 11 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java rename server/src/test/java/org/elasticsearch/index/codec/vectors/es818/{ES818DirectIOBinaryQuantizedVectorsFormatTests.java => DirectIOES818BinaryQuantizedVectorsFormatTests.java} (96%) rename server/src/test/java/org/elasticsearch/index/codec/vectors/es818/{ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java => DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java} (95%) diff --git a/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java b/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java index 5913cd2923855..7215ae76120cf 100644 --- a/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java +++ b/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java @@ -112,7 +112,7 @@ static Codec createCodec(CmdLineArgs args) { if (args.indexType() == IndexType.FLAT) { format = new ES818BinaryQuantizedVectorsFormat(); } else { - format = new ES818HnswBinaryQuantizedVectorsFormat(args.hnswM(), args.hnswEfConstruction(), 1, false, null); + format = new ES818HnswBinaryQuantizedVectorsFormat(args.hnswM(), args.hnswEfConstruction(), 1, null); } } else if (args.quantizeBits() < 32) { if (args.indexType() == IndexType.FLAT) { diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index ebe7548a88ba6..50bd250b43317 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -460,6 +460,8 @@ org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat, org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat, org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat, + org.elasticsearch.index.codec.vectors.es818.DirectIOES818BinaryQuantizedVectorsFormat, + org.elasticsearch.index.codec.vectors.es818.DirectIOES818HnswBinaryQuantizedVectorsFormat, org.elasticsearch.index.codec.vectors.IVFVectorsFormat; provides org.apache.lucene.codecs.Codec diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java new file mode 100644 index 0000000000000..568fe7fc11460 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.hnsw.FlatVectorScorerUtil; + +/** + * A variant of + */ +public class DirectIOES818BinaryQuantizedVectorsFormat extends ES818BinaryQuantizedVectorsFormat { + + public static final String NAME = "DirectIOES818BinaryQuantizedVectorsFormat"; + + public DirectIOES818BinaryQuantizedVectorsFormat() { + super(NAME, new DirectIOLucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer())); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java new file mode 100644 index 0000000000000..1bb26019fc138 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.codec.vectors.es818; + +import java.util.concurrent.ExecutorService; + +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_NUM_MERGE_WORKER; + +public class DirectIOES818HnswBinaryQuantizedVectorsFormat extends ES818HnswBinaryQuantizedVectorsFormat { + + public static final String NAME = "DirectIOES818HnswBinaryQuantizedVectorsFormat"; + + /** Constructs a format using default graph construction parameters */ + public DirectIOES818HnswBinaryQuantizedVectorsFormat() { + this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, null); + } + + /** + * Constructs a format using the given graph construction parameters. + * + * @param maxConn the maximum number of connections to a node in the HNSW graph + * @param beamWidth the size of the queue maintained during graph construction. + */ + public DirectIOES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { + this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, null); + } + + /** + * Constructs a format using the given graph construction parameters and scalar quantization. + * + * @param maxConn the maximum number of connections to a node in the HNSW graph + * @param beamWidth the size of the queue maintained during graph construction. + * @param numMergeWorkers number of workers (threads) that will be used when doing merge. If + * larger than 1, a non-null {@link ExecutorService} must be passed as mergeExec + * @param mergeExec the {@link ExecutorService} that will be used by ALL vector writers that are + * generated by this format to do the merge + */ + public DirectIOES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) { + super(NAME, maxConn, beamWidth, numMergeWorkers, new DirectIOES818BinaryQuantizedVectorsFormat(), mergeExec); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java index 4baa18b580173..7431a38a03c36 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java @@ -98,7 +98,7 @@ public class ES818BinaryQuantizedVectorsFormat extends FlatVectorsFormat { static final String VECTOR_DATA_EXTENSION = "veb"; static final int DIRECT_MONOTONIC_BLOCK_SHIFT = 16; - private static final ES818BinaryFlatVectorsScorer scorer = new ES818BinaryFlatVectorsScorer( + static final ES818BinaryFlatVectorsScorer scorer = new ES818BinaryFlatVectorsScorer( FlatVectorScorerUtil.getLucene99FlatVectorsScorer() ); @@ -106,16 +106,12 @@ public class ES818BinaryQuantizedVectorsFormat extends FlatVectorsFormat { /** Creates a new instance with the default number of vectors per cluster. */ public ES818BinaryQuantizedVectorsFormat() { - this(false); + this(NAME, new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer())); } - /** Creates a new instance with the default number of vectors per cluster, - * and whether direct IO should be used to access raw vectors. */ - public ES818BinaryQuantizedVectorsFormat(boolean useDirectIO) { - super(NAME); - rawVectorFormat = useDirectIO - ? new DirectIOLucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()) - : new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()); + ES818BinaryQuantizedVectorsFormat(String name, FlatVectorsFormat rawVectorFormat) { + super(name); + this.rawVectorFormat = rawVectorFormat; } @Override @@ -135,6 +131,6 @@ public int getMaxDimensions(String fieldName) { @Override public String toString() { - return "ES818BinaryQuantizedVectorsFormat(name=" + NAME + ", flatVectorScorer=" + scorer + ")"; + return getName() + "(name=" + getName() + ", flatVectorScorer=" + scorer + ")"; } } diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java index b6c76ced89d56..60729fecc1dda 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java @@ -69,7 +69,7 @@ public class ES818HnswBinaryQuantizedVectorsFormat extends KnnVectorsFormat { /** Constructs a format using default graph construction parameters */ public ES818HnswBinaryQuantizedVectorsFormat() { - this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, false, null); + this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, null); } /** @@ -79,18 +79,7 @@ public ES818HnswBinaryQuantizedVectorsFormat() { * @param beamWidth the size of the queue maintained during graph construction. */ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { - this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, false, null); - } - - /** - * Constructs a format using the given graph construction parameters. - * - * @param maxConn the maximum number of connections to a node in the HNSW graph - * @param beamWidth the size of the queue maintained during graph construction. - * @param useDirectIO whether direct IO should be used to access raw vectors - */ - public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, boolean useDirectIO) { - this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, useDirectIO, null); + this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, null); } /** @@ -103,14 +92,19 @@ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, boolean * @param mergeExec the {@link ExecutorService} that will be used by ALL vector writers that are * generated by this format to do the merge */ - public ES818HnswBinaryQuantizedVectorsFormat( + public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) { + this(NAME, maxConn, beamWidth, numMergeWorkers, new ES818BinaryQuantizedVectorsFormat(), mergeExec); + } + + ES818HnswBinaryQuantizedVectorsFormat( + String name, int maxConn, int beamWidth, int numMergeWorkers, - boolean useDirectIO, + FlatVectorsFormat flatVectorsFormat, ExecutorService mergeExec ) { - super(NAME); + super(name); if (maxConn <= 0 || maxConn > MAXIMUM_MAX_CONN) { throw new IllegalArgumentException( "maxConn must be positive and less than or equal to " + MAXIMUM_MAX_CONN + "; maxConn=" + maxConn @@ -128,7 +122,7 @@ public ES818HnswBinaryQuantizedVectorsFormat( } this.numMergeWorkers = numMergeWorkers; - flatVectorsFormat = new ES818BinaryQuantizedVectorsFormat(useDirectIO); + this.flatVectorsFormat = flatVectorsFormat; if (mergeExec != null) { this.mergeExec = new TaskExecutor(mergeExec); @@ -154,7 +148,10 @@ public int getMaxDimensions(String fieldName) { @Override public String toString() { - return "ES818HnswBinaryQuantizedVectorsFormat(name=ES818HnswBinaryQuantizedVectorsFormat, maxConn=" + return getName() + + "(name=" + + getName() + + ", maxConn=" + maxConn + ", beamWidth=" + beamWidth diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index d218bf3ad320f..602074fc3ad11 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -56,6 +56,7 @@ import org.elasticsearch.index.codec.vectors.ES815BitFlatVectorFormat; import org.elasticsearch.index.codec.vectors.ES815HnswBitVectorsFormat; import org.elasticsearch.index.codec.vectors.IVFVectorsFormat; +import org.elasticsearch.index.codec.vectors.es818.DirectIOES818HnswBinaryQuantizedVectorsFormat; import org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat; import org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat; import org.elasticsearch.index.fielddata.FieldDataContext; @@ -2199,7 +2200,9 @@ public BBQHnswIndexOptions(int m, int efConstruction, RescoreVector rescoreVecto @Override KnnVectorsFormat getVectorsFormat(ElementType elementType) { assert elementType == ElementType.FLOAT; - return new ES818HnswBinaryQuantizedVectorsFormat(m, efConstruction, disableOffheapCacheRescoring); + return disableOffheapCacheRescoring + ? new DirectIOES818HnswBinaryQuantizedVectorsFormat(m, efConstruction) + : new ES818HnswBinaryQuantizedVectorsFormat(m, efConstruction); } @Override diff --git a/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat b/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat index 14e68029abc3b..9ad678c723a23 100644 --- a/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat +++ b/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat @@ -7,4 +7,6 @@ org.elasticsearch.index.codec.vectors.es816.ES816BinaryQuantizedVectorsFormat org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat +org.elasticsearch.index.codec.vectors.es818.DirectIOES818BinaryQuantizedVectorsFormat +org.elasticsearch.index.codec.vectors.es818.DirectIOES818HnswBinaryQuantizedVectorsFormat org.elasticsearch.index.codec.vectors.IVFVectorsFormat diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormatTests.java similarity index 96% rename from server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOBinaryQuantizedVectorsFormatTests.java rename to server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormatTests.java index 4f27815a576df..77aa78b17b203 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOBinaryQuantizedVectorsFormatTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormatTests.java @@ -32,9 +32,9 @@ import java.util.Locale; import java.util.OptionalLong; -public class ES818DirectIOBinaryQuantizedVectorsFormatTests extends ES818BinaryQuantizedVectorsFormatTests { +public class DirectIOES818BinaryQuantizedVectorsFormatTests extends ES818BinaryQuantizedVectorsFormatTests { - static final Codec codec = TestUtil.alwaysKnnVectorsFormat(new ES818BinaryQuantizedVectorsFormat(true)); + static final Codec codec = TestUtil.alwaysKnnVectorsFormat(new DirectIOES818BinaryQuantizedVectorsFormat()); @Override protected Codec getCodec() { diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java similarity index 95% rename from server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java rename to server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java index 7140947049bb8..4655f9bd15447 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818DirectIOHnswBinaryQuantizedVectorsFormatTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java @@ -35,10 +35,10 @@ import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; -public class ES818DirectIOHnswBinaryQuantizedVectorsFormatTests extends ES818HnswBinaryQuantizedVectorsFormatTests { +public class DirectIOES818HnswBinaryQuantizedVectorsFormatTests extends ES818HnswBinaryQuantizedVectorsFormatTests { static final Codec codec = TestUtil.alwaysKnnVectorsFormat( - new ES818HnswBinaryQuantizedVectorsFormat(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, true) + new DirectIOES818HnswBinaryQuantizedVectorsFormat(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH) ); @Override diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java index 8197423b742c4..762873b4d48dc 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java @@ -73,7 +73,7 @@ public void testToString() { FilterCodec customCodec = new FilterCodec("foo", Codec.getDefault()) { @Override public KnnVectorsFormat knnVectorsFormat() { - return new ES818HnswBinaryQuantizedVectorsFormat(10, 20, 1, false, null); + return new ES818HnswBinaryQuantizedVectorsFormat(10, 20, 1, null); } }; String expectedPattern = @@ -123,7 +123,7 @@ public void testLimits() { expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 3201)); expectThrows( IllegalArgumentException.class, - () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 100, 1, false, new SameThreadExecutorService()) + () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 100, 1, new SameThreadExecutorService()) ); } From 5749e5740f60555c14c850dcf90ba2b2eb56db42 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 19 Aug 2025 12:24:46 +0100 Subject: [PATCH 08/15] Update reference --- .../java/org/elasticsearch/upgrades/VectorSearchIT.java | 4 ++-- .../java/org/elasticsearch/index/mapper/MapperFeatures.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java b/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java index e79b8eb0eda65..ca060c9790cda 100644 --- a/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java +++ b/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/VectorSearchIT.java @@ -16,7 +16,7 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.support.XContentMapValues; -import org.elasticsearch.search.SearchFeatures; +import org.elasticsearch.index.mapper.MapperFeatures; import java.io.IOException; import java.util.List; @@ -510,7 +510,7 @@ public void testBBQVectorSearch() throws Exception { } public void testBBQVectorSearchOffheapRescoring() throws Exception { - assumeTrue("Disabling off-heap rescoring is not supported", oldClusterHasFeature(SearchFeatures.BBQ_OFFHEAP_RESCORING)); + assumeTrue("Disabling off-heap rescoring is not supported", oldClusterHasFeature(MapperFeatures.BBQ_OFFHEAP_RESCORING)); if (isOldCluster()) { String mapping = """ { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java index 5a6b89e086bb3..1671e7265a173 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java @@ -47,7 +47,7 @@ public class MapperFeatures implements FeatureSpecification { static final NodeFeature BBQ_DISK_SUPPORT = new NodeFeature("mapper.bbq_disk_support"); static final NodeFeature SEARCH_LOAD_PER_SHARD = new NodeFeature("mapper.search_load_per_shard"); static final NodeFeature PATTERNED_TEXT = new NodeFeature("mapper.patterned_text"); - static final NodeFeature BBQ_OFFHEAP_RESCORING = new NodeFeature("mapper.vectors.bbq_offheap_rescoring"); + public static final NodeFeature BBQ_OFFHEAP_RESCORING = new NodeFeature("mapper.vectors.bbq_offheap_rescoring"); @Override public Set getTestFeatures() { From 6215f035960bce5dfab7aba1f2b099b20c0c9826 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 19 Aug 2025 15:02:17 +0100 Subject: [PATCH 09/15] Check setting in tests --- .../java/org/elasticsearch/index/store/DirectIOIT.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java b/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java index 5db87c13529e2..3c4af6c7e6a46 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/index/store/DirectIOIT.java @@ -72,7 +72,7 @@ protected Collection> nodePlugins() { } private void indexVectors() { - String type = randomFrom(/*"bbq_flat", */"bbq_hnsw"); + String type = "bbq_hnsw"; assertAcked( prepareCreate("foo-vectors").setSettings(Settings.builder().put(InternalSettingsPlugin.USE_COMPOUND_FILE.getKey(), false)) .setMapping(""" @@ -102,11 +102,12 @@ private void indexVectors() { assertBBQIndexType(type); // test assertion to ensure that the correct index type is being used } - @SuppressWarnings("unchecked") static void assertBBQIndexType(String type) { var response = indicesAdmin().prepareGetFieldMappings("foo-vectors").setFields("fooVector").get(); - var map = (Map) response.fieldMappings("foo-vectors", "fooVector").sourceAsMap().get("fooVector"); - assertThat((String) ((Map) map.get("index_options")).get("type"), is(equalTo(type))); + var map = (Map) response.fieldMappings("foo-vectors", "fooVector").sourceAsMap().get("fooVector"); + var options = (Map) map.get("index_options"); + assertThat(options.get("type"), is(equalTo(type))); + assertThat(options.get("disable_offheap_cache_rescoring"), is(true)); } @TestLogging(value = "org.elasticsearch.index.store.FsDirectoryFactory:DEBUG", reason = "to capture trace logging for direct IO") From 7c4b8affcee97f96072c3430afe671317f9df723 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Wed, 27 Aug 2025 15:59:49 +0100 Subject: [PATCH 10/15] Remove JVM option --- .../index/codec/vectors/AbstractFlatVectorsFormat.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/AbstractFlatVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/AbstractFlatVectorsFormat.java index 4bfdbe4c9273a..d5d34df4a0e40 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/AbstractFlatVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/AbstractFlatVectorsFormat.java @@ -11,21 +11,11 @@ import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; -import org.elasticsearch.core.SuppressForbidden; import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.MAX_DIMS_COUNT; public abstract class AbstractFlatVectorsFormat extends FlatVectorsFormat { - public static final boolean USE_DIRECT_IO = getUseDirectIO(); - - @SuppressForbidden( - 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")); - } - protected AbstractFlatVectorsFormat(String name) { super(name); } From cc3a5fff90807269ed33542383a589f55bf5022b Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 23 Sep 2025 13:24:33 +0100 Subject: [PATCH 11/15] Add direct IO to diskBBQ --- .../test/knn/KnnIndexTester.java | 6 ++++- .../diskbbq/ES920DiskBBQVectorsFormat.java | 22 ++++++++++++++----- .../DirectIOLucene99FlatVectorsFormat.java | 3 +-- .../vectors/DenseVectorFieldMapper.java | 14 +++++++++--- .../ES920DiskBBQVectorsFormatTests.java | 16 ++++++++------ .../vectors/DenseVectorFieldTypeTests.java | 3 ++- ...yingChildrenIVFKnnVectorQueryTestCase.java | 3 ++- .../AbstractIVFKnnVectorQueryTestCase.java | 2 +- 8 files changed, 48 insertions(+), 21 deletions(-) diff --git a/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java b/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java index 421375f038475..d78f906923272 100644 --- a/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java +++ b/qa/vector/src/main/java/org/elasticsearch/test/knn/KnnIndexTester.java @@ -106,7 +106,11 @@ private static String formatIndexPath(CmdLineArgs args) { static Codec createCodec(CmdLineArgs args) { final KnnVectorsFormat format; if (args.indexType() == IndexType.IVF) { - format = new ES920DiskBBQVectorsFormat(args.ivfClusterSize(), ES920DiskBBQVectorsFormat.DEFAULT_CENTROIDS_PER_PARENT_CLUSTER); + format = new ES920DiskBBQVectorsFormat( + args.ivfClusterSize(), + ES920DiskBBQVectorsFormat.DEFAULT_CENTROIDS_PER_PARENT_CLUSTER, + false + ); } else { if (args.quantizeBits() == 1) { if (args.indexType() == IndexType.FLAT) { diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/diskbbq/ES920DiskBBQVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/diskbbq/ES920DiskBBQVectorsFormat.java index 72f7f620d211a..af41d1148ee73 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/diskbbq/ES920DiskBBQVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/diskbbq/ES920DiskBBQVectorsFormat.java @@ -20,6 +20,7 @@ import org.apache.lucene.index.SegmentWriteState; import org.elasticsearch.common.util.Maps; import org.elasticsearch.index.codec.vectors.OptimizedScalarQuantizer; +import org.elasticsearch.index.codec.vectors.es818.DirectIOLucene99FlatVectorsFormat; import java.io.IOException; import java.util.Collections; @@ -63,7 +64,15 @@ public class ES920DiskBBQVectorsFormat extends KnnVectorsFormat { private static final FlatVectorsFormat rawVectorFormat = new Lucene99FlatVectorsFormat( FlatVectorScorerUtil.getLucene99FlatVectorsScorer() ); - private static final Map supportedFormats = Map.of(rawVectorFormat.getName(), rawVectorFormat); + private static final FlatVectorsFormat directIORawVectorFormat = new DirectIOLucene99FlatVectorsFormat( + FlatVectorScorerUtil.getLucene99FlatVectorsScorer() + ); + private static final Map supportedFormats = Map.of( + rawVectorFormat.getName(), + rawVectorFormat, + directIORawVectorFormat.getName(), + directIORawVectorFormat + ); // This dynamically sets the cluster probe based on the `k` requested and the number of clusters. // useful when searching with 'efSearch' type parameters instead of requiring a specific ratio. @@ -77,8 +86,9 @@ public class ES920DiskBBQVectorsFormat extends KnnVectorsFormat { private final int vectorPerCluster; private final int centroidsPerParentCluster; + private final boolean directRawDiskReads; - public ES920DiskBBQVectorsFormat(int vectorPerCluster, int centroidsPerParentCluster) { + public ES920DiskBBQVectorsFormat(int vectorPerCluster, int centroidsPerParentCluster, boolean directRawDiskReads) { super(NAME); if (vectorPerCluster < MIN_VECTORS_PER_CLUSTER || vectorPerCluster > MAX_VECTORS_PER_CLUSTER) { throw new IllegalArgumentException( @@ -102,19 +112,21 @@ public ES920DiskBBQVectorsFormat(int vectorPerCluster, int centroidsPerParentClu } this.vectorPerCluster = vectorPerCluster; this.centroidsPerParentCluster = centroidsPerParentCluster; + this.directRawDiskReads = directRawDiskReads; } /** Constructs a format using the given graph construction parameters and scalar quantization. */ public ES920DiskBBQVectorsFormat() { - this(DEFAULT_VECTORS_PER_CLUSTER, DEFAULT_CENTROIDS_PER_PARENT_CLUSTER); + this(DEFAULT_VECTORS_PER_CLUSTER, DEFAULT_CENTROIDS_PER_PARENT_CLUSTER, false); } @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + FlatVectorsFormat rawFormat = directRawDiskReads ? directIORawVectorFormat : rawVectorFormat; return new ES920DiskBBQVectorsWriter( - rawVectorFormat.getName(), + rawFormat.getName(), state, - rawVectorFormat.fieldsWriter(state), + rawFormat.fieldsWriter(state), vectorPerCluster, centroidsPerParentCluster ); diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java index d8802f451e88d..f7b2f8613c42c 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOLucene99FlatVectorsFormat.java @@ -41,11 +41,10 @@ * Copied from Lucene99FlatVectorsFormat in Lucene 10.1 * * This is copied to change the implementation of {@link #fieldsReader} only. - * The codec format itself is not changed, so we keep the original {@link #NAME} */ public class DirectIOLucene99FlatVectorsFormat extends AbstractFlatVectorsFormat { - static final String NAME = "Lucene99FlatVectorsFormat"; + static final String NAME = "DirectIOLucene99FlatVectorsFormat"; public static final int VERSION_START = 0; public static final int VERSION_CURRENT = VERSION_START; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 9186197dae1f5..b90034e5d6a95 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -1544,8 +1544,10 @@ public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map new ES920DiskBBQVectorsFormat(MIN_VECTORS_PER_CLUSTER - 1, 16)); - expectThrows(IllegalArgumentException.class, () -> new ES920DiskBBQVectorsFormat(MAX_VECTORS_PER_CLUSTER + 1, 16)); - expectThrows(IllegalArgumentException.class, () -> new ES920DiskBBQVectorsFormat(128, MIN_CENTROIDS_PER_PARENT_CLUSTER - 1)); - expectThrows(IllegalArgumentException.class, () -> new ES920DiskBBQVectorsFormat(128, MAX_CENTROIDS_PER_PARENT_CLUSTER + 1)); + expectThrows(IllegalArgumentException.class, () -> new ES920DiskBBQVectorsFormat(MIN_VECTORS_PER_CLUSTER - 1, 16, false)); + expectThrows(IllegalArgumentException.class, () -> new ES920DiskBBQVectorsFormat(MAX_VECTORS_PER_CLUSTER + 1, 16, false)); + expectThrows(IllegalArgumentException.class, () -> new ES920DiskBBQVectorsFormat(128, MIN_CENTROIDS_PER_PARENT_CLUSTER - 1, false)); + expectThrows(IllegalArgumentException.class, () -> new ES920DiskBBQVectorsFormat(128, MAX_CENTROIDS_PER_PARENT_CLUSTER + 1, false)); } public void testSimpleOffHeapSize() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java index 9b69fe3bfc251..ea78028e1d47a 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java @@ -114,7 +114,8 @@ public static DenseVectorFieldMapper.DenseVectorIndexOptions randomIndexOptionsA new DenseVectorFieldMapper.BBQIVFIndexOptions( randomIntBetween(MIN_VECTORS_PER_CLUSTER, MAX_VECTORS_PER_CLUSTER), randomFloatBetween(0.0f, 100.0f, true), - randomFrom((DenseVectorFieldMapper.RescoreVector) null, randomRescoreVector()) + randomFrom((DenseVectorFieldMapper.RescoreVector) null, randomRescoreVector()), + randomBoolean() ) ); } diff --git a/server/src/test/java/org/elasticsearch/search/vectors/AbstractDiversifyingChildrenIVFKnnVectorQueryTestCase.java b/server/src/test/java/org/elasticsearch/search/vectors/AbstractDiversifyingChildrenIVFKnnVectorQueryTestCase.java index 8e27cca564a40..0446b6485e099 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/AbstractDiversifyingChildrenIVFKnnVectorQueryTestCase.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/AbstractDiversifyingChildrenIVFKnnVectorQueryTestCase.java @@ -98,7 +98,8 @@ public void setUp() throws Exception { random().nextInt( ES920DiskBBQVectorsFormat.MIN_CENTROIDS_PER_PARENT_CLUSTER, ES920DiskBBQVectorsFormat.MAX_CENTROIDS_PER_PARENT_CLUSTER - ) + ), + random().nextBoolean() ); } diff --git a/server/src/test/java/org/elasticsearch/search/vectors/AbstractIVFKnnVectorQueryTestCase.java b/server/src/test/java/org/elasticsearch/search/vectors/AbstractIVFKnnVectorQueryTestCase.java index e94f0dc915802..1aabd772a5e42 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/AbstractIVFKnnVectorQueryTestCase.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/AbstractIVFKnnVectorQueryTestCase.java @@ -98,7 +98,7 @@ abstract class AbstractIVFKnnVectorQueryTestCase extends LuceneTestCase { @Before public void setUp() throws Exception { super.setUp(); - format = new ES920DiskBBQVectorsFormat(128, 4); + format = new ES920DiskBBQVectorsFormat(128, 4, false); } abstract AbstractIVFKnnVectorQuery getKnnVectorQuery(String field, float[] query, int k, Query queryFilter, float visitRatio); From 7d1edd33641cb81fe0164336e3ba6567a4943943 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 23 Sep 2025 14:19:51 +0100 Subject: [PATCH 12/15] Remove previous implementation versions --- server/src/main/java/module-info.java | 5 +- ...ctIOES818BinaryQuantizedVectorsFormat.java | 24 ------ ...ES818HnswBinaryQuantizedVectorsFormat.java | 50 ----------- ...ES818HnswBinaryQuantizedVectorsFormat.java | 24 +----- .../vectors/DenseVectorFieldMapper.java | 23 +++-- .../org.apache.lucene.codecs.KnnVectorsFormat | 2 - ...S818BinaryQuantizedVectorsFormatTests.java | 85 ------------------- 7 files changed, 15 insertions(+), 198 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java delete mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java delete mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormatTests.java diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index a2ee0a2dca21a..6021871586961 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import org.elasticsearch.index.codec.vectors.diskbbq.ES920DiskBBQVectorsFormat; import org.elasticsearch.plugins.internal.RestExtension; import org.elasticsearch.reservedstate.ReservedStateHandlerProvider; @@ -462,9 +461,7 @@ org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat, org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat, org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat, - org.elasticsearch.index.codec.vectors.es818.DirectIOES818BinaryQuantizedVectorsFormat, - org.elasticsearch.index.codec.vectors.es818.DirectIOES818HnswBinaryQuantizedVectorsFormat, - ES920DiskBBQVectorsFormat; + org.elasticsearch.index.codec.vectors.diskbbq.ES920DiskBBQVectorsFormat; provides org.apache.lucene.codecs.Codec with diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java deleted file mode 100644 index 568fe7fc11460..0000000000000 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818BinaryQuantizedVectorsFormat.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.index.codec.vectors.es818; - -import org.apache.lucene.codecs.hnsw.FlatVectorScorerUtil; - -/** - * A variant of - */ -public class DirectIOES818BinaryQuantizedVectorsFormat extends ES818BinaryQuantizedVectorsFormat { - - public static final String NAME = "DirectIOES818BinaryQuantizedVectorsFormat"; - - public DirectIOES818BinaryQuantizedVectorsFormat() { - super(NAME, new DirectIOLucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer())); - } -} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java deleted file mode 100644 index 1bb26019fc138..0000000000000 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormat.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.index.codec.vectors.es818; - -import java.util.concurrent.ExecutorService; - -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_NUM_MERGE_WORKER; - -public class DirectIOES818HnswBinaryQuantizedVectorsFormat extends ES818HnswBinaryQuantizedVectorsFormat { - - public static final String NAME = "DirectIOES818HnswBinaryQuantizedVectorsFormat"; - - /** Constructs a format using default graph construction parameters */ - public DirectIOES818HnswBinaryQuantizedVectorsFormat() { - this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, null); - } - - /** - * Constructs a format using the given graph construction parameters. - * - * @param maxConn the maximum number of connections to a node in the HNSW graph - * @param beamWidth the size of the queue maintained during graph construction. - */ - public DirectIOES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { - this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, null); - } - - /** - * Constructs a format using the given graph construction parameters and scalar quantization. - * - * @param maxConn the maximum number of connections to a node in the HNSW graph - * @param beamWidth the size of the queue maintained during graph construction. - * @param numMergeWorkers number of workers (threads) that will be used when doing merge. If - * larger than 1, a non-null {@link ExecutorService} must be passed as mergeExec - * @param mergeExec the {@link ExecutorService} that will be used by ALL vector writers that are - * generated by this format to do the merge - */ - public DirectIOES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) { - super(NAME, maxConn, beamWidth, numMergeWorkers, new DirectIOES818BinaryQuantizedVectorsFormat(), mergeExec); - } -} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java index d63ed1fcbbb95..0148604467a90 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java @@ -31,10 +31,6 @@ import java.io.IOException; import java.util.concurrent.ExecutorService; -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_NUM_MERGE_WORKER; - /** * Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10 */ @@ -43,11 +39,11 @@ public class ES818HnswBinaryQuantizedVectorsFormat extends AbstractHnswVectorsFo public static final String NAME = "ES818HnswBinaryQuantizedVectorsFormat"; /** The format for storing, reading, merging vectors on disk */ - private final FlatVectorsFormat flatVectorsFormat; + private static final FlatVectorsFormat flatVectorsFormat = new ES818BinaryQuantizedVectorsFormat(); /** Constructs a format using default graph construction parameters */ public ES818HnswBinaryQuantizedVectorsFormat() { - this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, null); + super(NAME); } /** @@ -57,7 +53,7 @@ public ES818HnswBinaryQuantizedVectorsFormat() { * @param beamWidth the size of the queue maintained during graph construction. */ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { - this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, null); + super(NAME, maxConn, beamWidth); } /** @@ -71,19 +67,7 @@ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { * generated by this format to do the merge */ public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) { - this(NAME, maxConn, beamWidth, numMergeWorkers, new ES818BinaryQuantizedVectorsFormat(), mergeExec); - } - - ES818HnswBinaryQuantizedVectorsFormat( - String name, - int maxConn, - int beamWidth, - int numMergeWorkers, - FlatVectorsFormat flatVectorsFormat, - ExecutorService mergeExec - ) { - super(name, maxConn, beamWidth, numMergeWorkers, mergeExec); - this.flatVectorsFormat = flatVectorsFormat; + super(NAME, maxConn, beamWidth, numMergeWorkers, mergeExec); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index b90034e5d6a95..934e58e5f8365 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -56,7 +56,6 @@ import org.elasticsearch.index.codec.vectors.ES815BitFlatVectorFormat; import org.elasticsearch.index.codec.vectors.ES815HnswBitVectorsFormat; import org.elasticsearch.index.codec.vectors.diskbbq.ES920DiskBBQVectorsFormat; -import org.elasticsearch.index.codec.vectors.es818.DirectIOES818HnswBinaryQuantizedVectorsFormat; import org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat; import org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat; import org.elasticsearch.index.fielddata.FieldDataContext; @@ -1449,7 +1448,7 @@ public boolean supportsDimension(int dims) { public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map indexOptionsMap, IndexVersion indexVersion) { Object mNode = indexOptionsMap.remove("m"); Object efConstructionNode = indexOptionsMap.remove("ef_construction"); - Object disableOffheapCacheRescoringNode = indexOptionsMap.remove("disable_offheap_cache_rescoring"); + Object directRawVectorReadsNode = indexOptionsMap.remove("direct_raw_vector_reads"); if (mNode == null) { mNode = Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; @@ -1468,10 +1467,10 @@ public DenseVectorIndexOptions parseIndexOptions(String fieldName, Map Date: Tue, 23 Sep 2025 14:25:40 +0100 Subject: [PATCH 13/15] Update implementations --- .../vectors/DenseVectorFieldMapper.java | 18 ++-- ...HnswBinaryQuantizedVectorsFormatTests.java | 90 ------------------- 2 files changed, 11 insertions(+), 97 deletions(-) delete mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 934e58e5f8365..96cc33127472a 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -2065,7 +2065,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("m", m); builder.field("ef_construction", efConstruction); if (directRawVectorReads) { - builder.field("disable_offheap_cache_rescoring", true); + builder.field("direct_raw_vector_reads", true); } if (rescoreVector != null) { rescoreVector.toXContent(builder, params); @@ -2148,13 +2148,13 @@ public boolean validateDimension(int dim, boolean throwOnError) { static class BBQIVFIndexOptions extends QuantizedIndexOptions { final int clusterSize; final double defaultVisitPercentage; - final boolean directRawDiskReads; + final boolean directRawVectorReads; - BBQIVFIndexOptions(int clusterSize, double defaultVisitPercentage, RescoreVector rescoreVector, boolean directRawDiskReads) { + BBQIVFIndexOptions(int clusterSize, double defaultVisitPercentage, RescoreVector rescoreVector, boolean directRawVectorReads) { super(VectorIndexType.BBQ_DISK, rescoreVector); this.clusterSize = clusterSize; this.defaultVisitPercentage = defaultVisitPercentage; - this.directRawDiskReads = directRawDiskReads; + this.directRawVectorReads = directRawVectorReads; } @Override @@ -2163,7 +2163,7 @@ KnnVectorsFormat getVectorsFormat(ElementType elementType) { return new ES920DiskBBQVectorsFormat( clusterSize, ES920DiskBBQVectorsFormat.DEFAULT_CENTROIDS_PER_PARENT_CLUSTER, - directRawDiskReads + directRawVectorReads ); } @@ -2177,12 +2177,13 @@ boolean doEquals(DenseVectorIndexOptions other) { BBQIVFIndexOptions that = (BBQIVFIndexOptions) other; return clusterSize == that.clusterSize && defaultVisitPercentage == that.defaultVisitPercentage - && Objects.equals(rescoreVector, that.rescoreVector); + && Objects.equals(rescoreVector, that.rescoreVector) + && directRawVectorReads == that.directRawVectorReads; } @Override int doHashCode() { - return Objects.hash(clusterSize, defaultVisitPercentage, rescoreVector); + return Objects.hash(clusterSize, defaultVisitPercentage, rescoreVector, directRawVectorReads); } @Override @@ -2196,6 +2197,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("type", type); builder.field("cluster_size", clusterSize); builder.field("default_visit_percentage", defaultVisitPercentage); + if (directRawVectorReads) { + builder.field("direct_raw_vector_reads", true); + } if (rescoreVector != null) { rescoreVector.toXContent(builder, params); } diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java deleted file mode 100644 index 4655f9bd15447..0000000000000 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/DirectIOES818HnswBinaryQuantizedVectorsFormatTests.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.index.codec.vectors.es818; - -import org.apache.lucene.codecs.Codec; -import org.apache.lucene.misc.store.DirectIODirectory; -import org.apache.lucene.store.Directory; -import org.apache.lucene.store.FSDirectory; -import org.apache.lucene.store.IOContext; -import org.apache.lucene.store.IndexOutput; -import org.apache.lucene.tests.store.MockDirectoryWrapper; -import org.apache.lucene.tests.util.TestUtil; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.IndexModule; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.index.shard.ShardPath; -import org.elasticsearch.index.store.FsDirectoryFactory; -import org.elasticsearch.test.IndexSettingsModule; -import org.junit.BeforeClass; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Locale; -import java.util.OptionalLong; - -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; -import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; - -public class DirectIOES818HnswBinaryQuantizedVectorsFormatTests extends ES818HnswBinaryQuantizedVectorsFormatTests { - - static final Codec codec = TestUtil.alwaysKnnVectorsFormat( - new DirectIOES818HnswBinaryQuantizedVectorsFormat(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH) - ); - - @Override - protected Codec getCodec() { - return codec; - } - - @BeforeClass - public static void checkDirectIOSupport() { - Path path = createTempDir("directIOProbe"); - try (Directory dir = open(path); IndexOutput out = dir.createOutput("out", IOContext.DEFAULT)) { - out.writeString("test"); - } catch (IOException e) { - assumeNoException("test requires a filesystem that supports Direct IO", e); - } - } - - static DirectIODirectory open(Path path) throws IOException { - return new DirectIODirectory(FSDirectory.open(path)) { - @Override - protected boolean useDirectIO(String name, IOContext context, OptionalLong fileLength) { - return true; - } - }; - } - - @Override - public void testSimpleOffHeapSize() throws IOException { - var config = newIndexWriterConfig().setUseCompoundFile(false); // avoid compound files to allow directIO - try (Directory dir = newFSDirectory()) { - testSimpleOffHeapSizeImpl(dir, config, false); - } - } - - private Directory newFSDirectory() throws IOException { - Settings settings = Settings.builder() - .put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), IndexModule.Type.HYBRIDFS.name().toLowerCase(Locale.ROOT)) - .build(); - IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("foo", settings); - Path tempDir = createTempDir().resolve(idxSettings.getUUID()).resolve("0"); - Files.createDirectories(tempDir); - ShardPath path = new ShardPath(false, tempDir, tempDir, new ShardId(idxSettings.getIndex(), 0)); - Directory dir = (new FsDirectoryFactory()).newDirectory(idxSettings, path); - if (random().nextBoolean()) { - dir = new MockDirectoryWrapper(random(), dir); - } - return dir; - } -} From 6f92506bb1d215609bf3c7e67691957fffc23f03 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 2 Oct 2025 08:25:10 +0100 Subject: [PATCH 14/15] Update docs/changelog/130893.yaml --- docs/changelog/130893.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/130893.yaml diff --git a/docs/changelog/130893.yaml b/docs/changelog/130893.yaml new file mode 100644 index 0000000000000..3829641dd8455 --- /dev/null +++ b/docs/changelog/130893.yaml @@ -0,0 +1,5 @@ +pr: 130893 +summary: Add a direct IO option for rescoring to `bbq_hnsw` +area: Vector Search +type: feature +issues: [] From 0f020c75d3884e9ab8e4899d45e0a2d292350359 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 2 Oct 2025 07:33:56 +0000 Subject: [PATCH 15/15] [CI] Update transport version definitions --- server/src/main/resources/transport/upper_bounds/8.18.csv | 2 +- server/src/main/resources/transport/upper_bounds/8.19.csv | 2 +- server/src/main/resources/transport/upper_bounds/9.0.csv | 2 +- server/src/main/resources/transport/upper_bounds/9.1.csv | 2 +- server/src/main/resources/transport/upper_bounds/9.2.csv | 2 +- server/src/main/resources/transport/upper_bounds/9.3.csv | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 server/src/main/resources/transport/upper_bounds/9.3.csv diff --git a/server/src/main/resources/transport/upper_bounds/8.18.csv b/server/src/main/resources/transport/upper_bounds/8.18.csv index ffc592e1809ee..266bfbbd3bf78 100644 --- a/server/src/main/resources/transport/upper_bounds/8.18.csv +++ b/server/src/main/resources/transport/upper_bounds/8.18.csv @@ -1 +1 @@ -initial_elasticsearch_8_18_8,8840010 +transform_check_for_dangling_tasks,8840011 diff --git a/server/src/main/resources/transport/upper_bounds/8.19.csv b/server/src/main/resources/transport/upper_bounds/8.19.csv index 3cc6f439c5ea5..3600b3f8c633a 100644 --- a/server/src/main/resources/transport/upper_bounds/8.19.csv +++ b/server/src/main/resources/transport/upper_bounds/8.19.csv @@ -1 +1 @@ -initial_elasticsearch_8_19_5,8841069 +transform_check_for_dangling_tasks,8841070 diff --git a/server/src/main/resources/transport/upper_bounds/9.0.csv b/server/src/main/resources/transport/upper_bounds/9.0.csv index 8ad2ed1a4cacf..c11e6837bb813 100644 --- a/server/src/main/resources/transport/upper_bounds/9.0.csv +++ b/server/src/main/resources/transport/upper_bounds/9.0.csv @@ -1 +1 @@ -initial_elasticsearch_9_0_8,9000017 +transform_check_for_dangling_tasks,9000018 diff --git a/server/src/main/resources/transport/upper_bounds/9.1.csv b/server/src/main/resources/transport/upper_bounds/9.1.csv index 1cea5dc4d929b..80b97d85f7511 100644 --- a/server/src/main/resources/transport/upper_bounds/9.1.csv +++ b/server/src/main/resources/transport/upper_bounds/9.1.csv @@ -1 +1 @@ -initial_elasticsearch_9_1_5,9112008 +transform_check_for_dangling_tasks,9112009 diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv index 6e7d51d3d3020..2147eab66c207 100644 --- a/server/src/main/resources/transport/upper_bounds/9.2.csv +++ b/server/src/main/resources/transport/upper_bounds/9.2.csv @@ -1 +1 @@ -security_stats_endpoint,9168000 +initial_9.2.0,9185000 diff --git a/server/src/main/resources/transport/upper_bounds/9.3.csv b/server/src/main/resources/transport/upper_bounds/9.3.csv new file mode 100644 index 0000000000000..2147eab66c207 --- /dev/null +++ b/server/src/main/resources/transport/upper_bounds/9.3.csv @@ -0,0 +1 @@ +initial_9.2.0,9185000