Skip to content

Commit dae3d72

Browse files
committed
switch to dynamic index setting
1 parent e1dfb2c commit dae3d72

File tree

17 files changed

+222
-466
lines changed

17 files changed

+222
-466
lines changed

docs/reference/elasticsearch/index-settings/index-modules.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,6 @@ $$$index-esql-stored-fields-sequential-proportion$$$
259259

260260
`index.esql.stored_fields_sequential_proportion`
261261
: Tuning parameter for deciding when {{esql}} will load [Stored fields](/reference/elasticsearch/rest-apis/retrieve-selected-fields.md#stored-fields) using a strategy tuned for loading dense sequence of documents. Allows values between 0.0 and 1.0 and defaults to 0.2. Indices with documents smaller than 10kb may see speed improvements loading `text` fields by setting this lower.
262+
263+
$$$index-dense-vector-hnsw-filter-heuristic$$$ `index.dense_vector.hnsw_filter_heuristic`
264+
: Whether to apply _patience_ based early termination strategy to knn queries over HNSW graphs (see [paper](https://cs.uwaterloo.ca/~jimmylin/publications/Teofili_Lin_ECIR2025.pdf)). This is only applicable to `dense_vector` fields with `hnsw`, `int8_hnsw`, `int4_hnsw` and `bbq_hnsw` index types. Defaults to `false`.

docs/reference/elasticsearch/mapping-reference/dense-vector.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,6 @@ $$$dense-vector-index-options$$$
281281
: In case a knn query specifies a `rescore_vector` parameter, the query `rescore_vector` parameter will be used instead.
282282
: See [oversampling and rescoring quantized vectors](docs-content://solutions/search/vector/knn.md#dense-vector-knn-search-rescoring) for details.
283283
:::::
284-
285-
`early_termination`
286-
: (Optional, boolean) Apply _patience_ based early termination strategy to knn queries over HNSW graphs (see [paper](https://cs.uwaterloo.ca/~jimmylin/publications/Teofili_Lin_ECIR2025.pdf)). This is expected to produce Only applicable to `hnsw`, `int8_hnsw`, `int4_hnsw` and `bbq_hnsw` index types.
287284
::::
288285

289286

rest-api-spec/build.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,4 @@ tasks.named("yamlRestCompatTestTransform").configure ({ task ->
9090
task.skipTest("indices.create/21_synthetic_source_stored/field param - keep root array", "Synthetic source keep arrays now stores leaf arrays natively")
9191
task.skipTest("cluster.info/30_info_thread_pool/Cluster HTTP Info", "The search_throttled thread pool has been removed")
9292
task.skipTest("synonyms/80_synonyms_from_index/Fail loading synonyms from index if synonyms_set doesn't exist", "Synonyms do no longer fail if the synonyms_set doesn't exist")
93-
task.skipTest("search.vectors/80_dense_vector_indexed_by_default/Default index options for dense_vector", "Introduced early_termination option")
94-
task.skipTest("search.vectors/80_dense_vector_indexed_by_default/Indexed by default with specified similarity and index options", "Introduced early_termination option")
9593
})

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/240_knn_search_early_termination.yml

Lines changed: 0 additions & 168 deletions
This file was deleted.

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/80_dense_vector_indexed_by_default.yml

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ setup:
2525
- match: { test.mappings.properties.vector.dims: 5 }
2626
- match: { test.mappings.properties.vector.index: true }
2727
- match: { test.mappings.properties.vector.similarity: cosine }
28-
2928
---
3029
"Indexed by default with specified similarity and index options":
3130
- do:
@@ -49,48 +48,19 @@ setup:
4948
indices.get_mapping:
5049
index: test
5150

52-
- match: { test.mappings.properties.vector.type: dense_vector }
53-
- match: { test.mappings.properties.vector.dims: 5 }
54-
- match: { test.mappings.properties.vector.index: true }
55-
- match: { test.mappings.properties.vector.similarity: dot_product }
56-
- match: { test.mappings.properties.vector.index_options.type: hnsw }
57-
- match: { test.mappings.properties.vector.index_options.m: 32 }
58-
- match: { test.mappings.properties.vector.index_options.ef_construction: 200 }
59-
60-
---
61-
"Indexed by default with specified similarity and index options with early_termination":
62-
- requires:
63-
cluster_features: [ "search.vectors.mappers.expose_hnsw_early_termination" ]
64-
reason: "requires early termination for hnsw to be exposed"
65-
- do:
66-
indices.create:
67-
index: test
68-
body:
69-
mappings:
70-
properties:
71-
vector:
72-
type: dense_vector
73-
dims: 5
74-
similarity: dot_product
75-
index_options:
76-
type: hnsw
77-
m: 32
78-
ef_construction: 200
79-
80-
- match: { acknowledged: true }
81-
82-
- do:
83-
indices.get_mapping:
84-
index: test
85-
86-
- match: { test.mappings.properties.vector.type: dense_vector }
87-
- match: { test.mappings.properties.vector.dims: 5 }
88-
- match: { test.mappings.properties.vector.index: true }
89-
- match: { test.mappings.properties.vector.similarity: dot_product }
90-
- match: { test.mappings.properties.vector.index_options.type: hnsw }
91-
- match: { test.mappings.properties.vector.index_options.m: 32 }
92-
- match: { test.mappings.properties.vector.index_options.ef_construction: 200 }
93-
- match: { test.mappings.properties.vector.index_options.early_termination: false }
51+
- match:
52+
test:
53+
mappings:
54+
properties:
55+
vector:
56+
type: dense_vector
57+
dims: 5
58+
index: true
59+
similarity: dot_product
60+
index_options:
61+
type: hnsw
62+
m: 32
63+
ef_construction: 200
9464

9565
---
9666
"Not indexed vector":
@@ -156,9 +126,6 @@ setup:
156126
- requires:
157127
cluster_features: "gte_v8.14.0"
158128
reason: 'dense_vector indexed as int8_hnsw by default was added in 8.14'
159-
- requires:
160-
cluster_features: [ "search.vectors.mappers.expose_hnsw_early_termination" ]
161-
reason: "requires early termination for hnsw to be exposed"
162129
- do:
163130
indices.create:
164131
index: test_default_index_options
@@ -180,7 +147,6 @@ setup:
180147
- match: { test_default_index_options.mappings.properties.vector.index: true }
181148
- match: { test_default_index_options.mappings.properties.vector.similarity: cosine }
182149
- match: { test_default_index_options.mappings.properties.vector.index_options.type: int8_hnsw }
183-
- match: { test_default_index_options.mappings.properties.vector.index_options.early_termination: false }
184150
---
185151
"Default index options for dense_vector element type byte":
186152
- requires:

server/src/internalClusterTest/java/org/elasticsearch/search/query/VectorIT.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,55 @@ public void testFilteredQueryStrategy() {
127127
});
128128
}
129129

130+
public void testHnswEarlyTerminationQuery() {
131+
float[] vector = new float[16];
132+
randomVector(vector, 25);
133+
int upperLimit = 35;
134+
var query = new KnnSearchBuilder(VECTOR_FIELD, vector, 1, 1, null, null);
135+
assertResponse(client().prepareSearch(INDEX_NAME).setKnnSearch(List.of(query)).setSize(1).setProfile(true), response -> {
136+
assertNotEquals(0, response.getHits().getHits().length);
137+
var profileResults = response.getProfileResults();
138+
long vectorOpsSum = profileResults.values()
139+
.stream()
140+
.mapToLong(
141+
pr -> pr.getQueryPhase()
142+
.getSearchProfileDfsPhaseResult()
143+
.getQueryProfileShardResult()
144+
.stream()
145+
.mapToLong(qpr -> qpr.getVectorOperationsCount().longValue())
146+
.sum()
147+
)
148+
.sum();
149+
client().admin()
150+
.indices()
151+
.prepareUpdateSettings(INDEX_NAME)
152+
.setSettings(Settings.builder().put(DenseVectorFieldMapper.HNSW_EARLY_TERMINATION.getKey(), true))
153+
.get();
154+
assertResponse(
155+
client().prepareSearch(INDEX_NAME).setKnnSearch(List.of(query)).setSize(1).setProfile(true),
156+
earlyTerminationResponse -> {
157+
assertNotEquals(0, earlyTerminationResponse.getHits().getHits().length);
158+
var earlyTerminationResults = earlyTerminationResponse.getProfileResults();
159+
long earlyTerminationVectorOpsSum = earlyTerminationResults.values()
160+
.stream()
161+
.mapToLong(
162+
pr -> pr.getQueryPhase()
163+
.getSearchProfileDfsPhaseResult()
164+
.getQueryProfileShardResult()
165+
.stream()
166+
.mapToLong(qpr -> qpr.getVectorOperationsCount().longValue())
167+
.sum()
168+
)
169+
.sum();
170+
assertTrue(
171+
"earlyTerminationVectorOps [" + earlyTerminationVectorOpsSum + "] is not lt vectorOps [" + vectorOpsSum + "]",
172+
earlyTerminationVectorOpsSum < vectorOpsSum
173+
// if both switch to brute-force due to excessive exploration, they will both equal to upperLimit
174+
|| (earlyTerminationVectorOpsSum == vectorOpsSum && vectorOpsSum == upperLimit + 1)
175+
);
176+
}
177+
);
178+
});
179+
}
180+
130181
}

server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public final class IndexScopedSettings extends AbstractScopedSettings {
159159
IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING,
160160
IndexSettings.INDEX_SEARCH_IDLE_AFTER,
161161
DenseVectorFieldMapper.HNSW_FILTER_HEURISTIC,
162+
DenseVectorFieldMapper.HNSW_EARLY_TERMINATION,
162163
IndexFieldDataService.INDEX_FIELDDATA_CACHE_KEY,
163164
IndexSettings.IGNORE_ABOVE_SETTING,
164165
FieldMapper.IGNORE_MALFORMED_SETTING,

server/src/main/java/org/elasticsearch/index/IndexSettings.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,7 @@ private void setRetentionLeaseMillis(final TimeValue retentionLease) {
916916
private volatile int maxNgramDiff;
917917
private volatile int maxShingleDiff;
918918
private volatile DenseVectorFieldMapper.FilterHeuristic hnswFilterHeuristic;
919+
private volatile boolean earlyTermination;
919920
private volatile TimeValue searchIdleAfter;
920921
private volatile int maxAnalyzedOffset;
921922
private volatile boolean weightMatchesEnabled;
@@ -1113,6 +1114,7 @@ public IndexSettings(final IndexMetadata indexMetadata, final Settings nodeSetti
11131114
skipIgnoredSourceWrite = scopedSettings.get(IgnoredSourceFieldMapper.SKIP_IGNORED_SOURCE_WRITE_SETTING);
11141115
skipIgnoredSourceRead = scopedSettings.get(IgnoredSourceFieldMapper.SKIP_IGNORED_SOURCE_READ_SETTING);
11151116
hnswFilterHeuristic = scopedSettings.get(DenseVectorFieldMapper.HNSW_FILTER_HEURISTIC);
1117+
earlyTermination = scopedSettings.get(DenseVectorFieldMapper.HNSW_EARLY_TERMINATION);
11161118
indexMappingSourceMode = scopedSettings.get(INDEX_MAPPER_SOURCE_MODE_SETTING);
11171119
recoverySourceEnabled = RecoverySettings.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.get(nodeSettings);
11181120
recoverySourceSyntheticEnabled = DiscoveryNode.isStateless(nodeSettings) == false
@@ -1227,6 +1229,7 @@ public IndexSettings(final IndexMetadata indexMetadata, final Settings nodeSetti
12271229
);
12281230
scopedSettings.addSettingsUpdateConsumer(IgnoredSourceFieldMapper.SKIP_IGNORED_SOURCE_READ_SETTING, this::setSkipIgnoredSourceRead);
12291231
scopedSettings.addSettingsUpdateConsumer(DenseVectorFieldMapper.HNSW_FILTER_HEURISTIC, this::setHnswFilterHeuristic);
1232+
scopedSettings.addSettingsUpdateConsumer(DenseVectorFieldMapper.HNSW_EARLY_TERMINATION, this::setHnswEarlyTermination);
12301233
}
12311234

12321235
private void setSearchIdleAfter(TimeValue searchIdleAfter) {
@@ -1858,6 +1861,14 @@ private void setHnswFilterHeuristic(DenseVectorFieldMapper.FilterHeuristic heuri
18581861
this.hnswFilterHeuristic = heuristic;
18591862
}
18601863

1864+
public boolean getHnswEarlyTermination() {
1865+
return this.earlyTermination;
1866+
}
1867+
1868+
private void setHnswEarlyTermination(boolean earlyTermination) {
1869+
this.earlyTermination = earlyTermination;
1870+
}
1871+
18611872
public SeqNoFieldMapper.SeqNoIndexOptions seqNoIndexOptions() {
18621873
return seqNoIndexOptions;
18631874
}

server/src/main/java/org/elasticsearch/index/IndexVersions.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ private static Version parseUnchecked(String version) {
178178
public static final IndexVersion UPGRADE_TO_LUCENE_10_2_2 = def(9_030_0_00, Version.LUCENE_10_2_2);
179179
public static final IndexVersion SPARSE_VECTOR_PRUNING_INDEX_OPTIONS_SUPPORT = def(9_031_0_00, Version.LUCENE_10_2_2);
180180
public static final IndexVersion DEFAULT_DENSE_VECTOR_TO_BBQ_HNSW = def(9_032_0_00, Version.LUCENE_10_2_2);
181-
public static final IndexVersion EXPOSE_EARLY_TERMINATION = def(9_033_0_00, Version.LUCENE_10_2_2);
182181

183182
/*
184183
* STOP! READ THIS FIRST! No, really,

0 commit comments

Comments
 (0)