Skip to content
Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
73e3cc0
Add a direct IO option to rescore_vector for bbq_hnsw
thecoop Jul 9, 2025
fb9c1df
Merge remote-tracking branch 'upstream/lucene_snapshot' into direct-i…
thecoop Jul 10, 2025
318a564
Merge branch 'lucene_snapshot' into direct-io-index-option
thecoop Jul 14, 2025
799daaa
Merge remote-tracking branch 'upstream/lucene_snapshot' into direct-i…
thecoop Jul 14, 2025
3776a01
Use a separate option
thecoop Jul 14, 2025
31acb01
Rename option, add basic tests
thecoop Jul 14, 2025
1369293
Add more test
thecoop Jul 18, 2025
c35ca82
Merge branch 'lucene_snapshot' into direct-io-index-option
thecoop Aug 12, 2025
a5a23e5
[CI] Auto commit changes from spotless
Aug 12, 2025
a59d736
Merge branch 'lucene_snapshot' into direct-io-index-option
thecoop Aug 19, 2025
9b02a48
Change to a mapper feature rather than search feature
thecoop Aug 19, 2025
5b8d499
Create new formats for direct IO access
thecoop Aug 19, 2025
5749e57
Update reference
thecoop Aug 19, 2025
7f7372a
Merge branch 'lucene_snapshot' into direct-io-index-option
thecoop Aug 19, 2025
6215f03
Check setting in tests
thecoop Aug 19, 2025
490e505
Merge branch 'lucene_snapshot' into direct-io-index-option
thecoop Aug 27, 2025
7d33963
Merge branch 'lucene_snapshot' into direct-io-index-option
thecoop Aug 27, 2025
7c4b8af
Remove JVM option
thecoop Aug 27, 2025
15f7f4c
Merge branch 'lucene_snapshot' into direct-io-index-option
thecoop Aug 28, 2025
0d0b484
Merge remote-tracking branch 'upstream/lucene_snapshot_10_3' into dir…
thecoop Sep 22, 2025
cc3a5ff
Add direct IO to diskBBQ
thecoop Sep 23, 2025
222ccc1
Merge remote-tracking branch 'upstream/lucene_snapshot_10_3' into dir…
thecoop Sep 23, 2025
7d1edd3
Remove previous implementation versions
thecoop Sep 23, 2025
67fb702
Update implementations
thecoop Sep 23, 2025
6f92506
Update docs/changelog/130893.yaml
thecoop Oct 2, 2025
0f020c7
[CI] Update transport version definitions
Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.index.mapper.MapperFeatures;

import java.io.IOException;
import java.util.List;
Expand All @@ -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";

Expand Down Expand Up @@ -507,6 +509,63 @@ public void testBBQVectorSearch() throws Exception {
);
}

public void testBBQVectorSearchOffheapRescoring() throws Exception {
assumeTrue("Disabling off-heap rescoring is not supported", oldClusterHasFeature(MapperFeatures.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<String, Object> response = search(searchRequest);
assertThat(extractValue(response, "hits.total.value"), equalTo(2));
List<Map<String, Object>> 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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: ["mapper.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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,8 +49,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");
Expand All @@ -76,7 +73,7 @@ protected Collection<Class<? extends Plugin>> 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("""
Expand All @@ -89,7 +86,8 @@ private void indexVectors() {
"index": true,
"similarity": "l2_norm",
"index_options": {
"type": "%type%"
"type": "%type%",
"disable_offheap_cache_rescoring": true
}
}
}
Expand All @@ -105,11 +103,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<String, Object>) response.fieldMappings("foo-vectors", "fooVector").sourceAsMap().get("fooVector");
assertThat((String) ((Map<String, Object>) 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")
Expand Down
3 changes: 1 addition & 2 deletions server/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -462,7 +461,7 @@
org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat,
org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat,
org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat,
ES920DiskBBQVectorsFormat;
org.elasticsearch.index.codec.vectors.diskbbq.ES920DiskBBQVectorsFormat;

provides org.apache.lucene.codecs.Codec
with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,7 +64,15 @@ public class ES920DiskBBQVectorsFormat extends KnnVectorsFormat {
private static final FlatVectorsFormat rawVectorFormat = new Lucene99FlatVectorsFormat(
FlatVectorScorerUtil.getLucene99FlatVectorsScorer()
);
private static final Map<String, FlatVectorsFormat> supportedFormats = Map.of(rawVectorFormat.getName(), rawVectorFormat);
private static final FlatVectorsFormat directIORawVectorFormat = new DirectIOLucene99FlatVectorsFormat(
FlatVectorScorerUtil.getLucene99FlatVectorsScorer()
);
private static final Map<String, FlatVectorsFormat> 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.
Expand All @@ -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(
Expand All @@ -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
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -69,7 +68,6 @@ public FlatVectorsWriter fieldsWriter(SegmentWriteState state) throws IOExceptio
}

static boolean shouldUseDirectIO(SegmentReadState state) {
assert USE_DIRECT_IO;
return FsDirectoryFactory.isHybridFs(state.directory);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,20 @@ public class ES818BinaryQuantizedVectorsFormat extends AbstractFlatVectorsFormat
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(
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() {
super(NAME);
this(NAME, new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()));
}

ES818BinaryQuantizedVectorsFormat(String name, FlatVectorsFormat rawVectorFormat) {
Copy link
Member

Choose a reason for hiding this comment

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

this works now?

super(name);
this.rawVectorFormat = rawVectorFormat;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class MapperFeatures implements FeatureSpecification {
static final NodeFeature PATTERN_TEXT = new NodeFeature("mapper.patterned_text");
static final NodeFeature IGNORED_SOURCE_FIELDS_PER_ENTRY = new NodeFeature("mapper.ignored_source_fields_per_entry");
public static final NodeFeature MULTI_FIELD_UNICODE_OPTIMISATION_FIX = new NodeFeature("mapper.multi_field.unicode_optimisation_fix");
public static final NodeFeature BBQ_OFFHEAP_RESCORING = new NodeFeature("mapper.vectors.bbq_offheap_rescoring");

@Override
public Set<NodeFeature> getTestFeatures() {
Expand Down Expand Up @@ -87,7 +88,8 @@ public Set<NodeFeature> getTestFeatures() {
PATTERN_TEXT,
IGNORED_SOURCE_FIELDS_PER_ENTRY,
MULTI_FIELD_UNICODE_OPTIMISATION_FIX,
MATCH_ONLY_TEXT_BLOCK_LOADER_FIX
MATCH_ONLY_TEXT_BLOCK_LOADER_FIX,
BBQ_OFFHEAP_RESCORING
);
}
}
Loading