Skip to content

Commit 0b24682

Browse files
committed
Make bbq_hnsw the default index option for dense-vector fields with more than 384 dimensions
1 parent c1046b0 commit 0b24682

File tree

4 files changed

+93
-49
lines changed

4 files changed

+93
-49
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ In many cases, a brute-force kNN search is not efficient enough. For this reason
5555

5656
Unmapped array fields of float elements with size between 128 and 4096 are dynamically mapped as `dense_vector` with a default similariy of `cosine`. You can override the default similarity by explicitly mapping the field as `dense_vector` with the desired similarity.
5757

58-
Indexing is enabled by default for dense vector fields and indexed as `int8_hnsw`. When indexing is enabled, you can define the vector similarity to use in kNN search:
58+
Indexing is enabled by default for dense vector fields and indexed as `bbq_hnsw` if dimensions are greater than or equal to 384, otherwise they are indexed as `int8_hnsw`. When indexing is enabled, you can define the vector similarity to use in kNN search:
5959

6060
```console
6161
PUT my-index-2
@@ -105,7 +105,7 @@ The `dense_vector` type supports quantization to reduce the memory footprint req
105105

106106
When using a quantized format, you may want to oversample and rescore the results to improve accuracy. See [oversampling and rescoring](docs-content://solutions/search/vector/knn.md#dense-vector-knn-search-rescoring) for more information.
107107

108-
To use a quantized index, you can set your index type to `int8_hnsw`, `int4_hnsw`, or `bbq_hnsw`. When indexing `float` vectors, the current default index type is `int8_hnsw`.
108+
To use a quantized index, you can set your index type to `int8_hnsw`, `int4_hnsw`, or `bbq_hnsw`. When indexing `float` vectors, the current default index type is `bbq_hnsw` for vectors with greater than or equal to 384 dimensions, otherwise it's `int8_hnsw`.
109109

110110
Quantized vectors can use [oversampling and rescoring](docs-content://solutions/search/vector/knn.md#dense-vector-knn-search-rescoring) to improve accuracy on approximate kNN search results.
111111

@@ -255,9 +255,9 @@ $$$dense-vector-index-options$$$
255255
`type`
256256
: (Required, string) The type of kNN algorithm to use. Can be either any of:
257257
* `hnsw` - This utilizes the [HNSW algorithm](https://arxiv.org/abs/1603.09320) for scalable approximate kNN search. This supports all `element_type` values.
258-
* `int8_hnsw` - The default index type for float vectors. This utilizes the [HNSW algorithm](https://arxiv.org/abs/1603.09320) in addition to automatically scalar quantization for scalable approximate kNN search with `element_type` of `float`. This can reduce the memory footprint by 4x at the cost of some accuracy. See [Automatically quantize vectors for kNN search](#dense-vector-quantization).
258+
* `int8_hnsw` - The default index type for float vectors with less than 384 dimensions. This utilizes the [HNSW algorithm](https://arxiv.org/abs/1603.09320) in addition to automatically scalar quantization for scalable approximate kNN search with `element_type` of `float`. This can reduce the memory footprint by 4x at the cost of some accuracy. See [Automatically quantize vectors for kNN search](#dense-vector-quantization).
259259
* `int4_hnsw` - This utilizes the [HNSW algorithm](https://arxiv.org/abs/1603.09320) in addition to automatically scalar quantization for scalable approximate kNN search with `element_type` of `float`. This can reduce the memory footprint by 8x at the cost of some accuracy. See [Automatically quantize vectors for kNN search](#dense-vector-quantization).
260-
* `bbq_hnsw` - This utilizes the [HNSW algorithm](https://arxiv.org/abs/1603.09320) in addition to automatically binary quantization for scalable approximate kNN search with `element_type` of `float`. This can reduce the memory footprint by 32x at the cost of accuracy. See [Automatically quantize vectors for kNN search](#dense-vector-quantization).
260+
* `bbq_hnsw` - The default index type for float vectors with greater than or equal to 384 dimensions. This utilizes the [HNSW algorithm](https://arxiv.org/abs/1603.09320) in addition to automatically binary quantization for scalable approximate kNN search with `element_type` of `float`. This can reduce the memory footprint by 32x at the cost of accuracy. See [Automatically quantize vectors for kNN search](#dense-vector-quantization).
261261
* `flat` - This utilizes a brute-force search algorithm for exact kNN search. This supports all `element_type` values.
262262
* `int8_flat` - This utilizes a brute-force search algorithm in addition to automatically scalar quantization. Only supports `element_type` of `float`.
263263
* `int4_flat` - This utilizes a brute-force search algorithm in addition to automatically half-byte scalar quantization. Only supports `element_type` of `float`.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ private static Version parseUnchecked(String version) {
173173
public static final IndexVersion SEQ_NO_WITHOUT_POINTS = def(9_027_0_00, Version.LUCENE_10_2_1);
174174
public static final IndexVersion INDEX_INT_SORT_INT_TYPE = def(9_028_0_00, Version.LUCENE_10_2_1);
175175
public static final IndexVersion MAPPER_TEXT_MATCH_ONLY_MULTI_FIELDS_DEFAULT_NOT_STORED = def(9_029_0_00, Version.LUCENE_10_2_1);
176+
public static final IndexVersion DEFAULT_DENSE_VECTOR_TO_BBQ_HNSW = def(9_030_0_00, Version.LUCENE_10_2_1);
176177

177178
/*
178179
* STOP! READ THIS FIRST! No, really,

server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ private static boolean defaultOversampleForBBQ(IndexVersion version) {
193193
public static final IndexVersion INDEXED_BY_DEFAULT_INDEX_VERSION = IndexVersions.FIRST_DETACHED_INDEX_VERSION;
194194
public static final IndexVersion NORMALIZE_COSINE = IndexVersions.NORMALIZED_VECTOR_COSINE;
195195
public static final IndexVersion DEFAULT_TO_INT8 = IndexVersions.DEFAULT_DENSE_VECTOR_TO_INT8_HNSW;
196+
public static final IndexVersion DEFAULT_TO_BBQ = IndexVersions.DEFAULT_DENSE_VECTOR_TO_BBQ_HNSW;
196197
public static final IndexVersion LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION = IndexVersions.V_8_9_0;
197198

198199
public static final NodeFeature RESCORE_VECTOR_QUANTIZED_VECTOR_MAPPING = new NodeFeature("mapper.dense_vector.rescore_vector");
@@ -212,6 +213,7 @@ private static boolean defaultOversampleForBBQ(IndexVersion version) {
212213
public static final int MAGNITUDE_BYTES = 4;
213214
public static final int OVERSAMPLE_LIMIT = 10_000; // Max oversample allowed
214215
public static final float DEFAULT_OVERSAMPLE = 3.0F; // Default oversample value
216+
public static final int BBQ_DIMS_DEFAULT_THRESHOLD = 384; // Lower bound for dimensions for using bbq_hnsw as default index options
215217

216218
private static DenseVectorFieldMapper toType(FieldMapper in) {
217219
return (DenseVectorFieldMapper) in;
@@ -226,34 +228,7 @@ public static class Builder extends FieldMapper.Builder {
226228
}
227229
return elementType;
228230
}, m -> toType(m).fieldType().elementType, XContentBuilder::field, Objects::toString);
229-
230-
// This is defined as updatable because it can be updated once, from [null] to a valid dim size,
231-
// by a dynamic mapping update. Once it has been set, however, the value cannot be changed.
232-
private final Parameter<Integer> dims = new Parameter<>("dims", true, () -> null, (n, c, o) -> {
233-
if (o instanceof Integer == false) {
234-
throw new MapperParsingException("Property [dims] on field [" + n + "] must be an integer but got [" + o + "]");
235-
}
236-
237-
return XContentMapValues.nodeIntegerValue(o);
238-
}, m -> toType(m).fieldType().dims, XContentBuilder::field, Objects::toString).setSerializerCheck((id, ic, v) -> v != null)
239-
.setMergeValidator((previous, current, c) -> previous == null || Objects.equals(previous, current))
240-
.addValidator(dims -> {
241-
if (dims == null) {
242-
return;
243-
}
244-
int maxDims = elementType.getValue() == ElementType.BIT ? MAX_DIMS_COUNT_BIT : MAX_DIMS_COUNT;
245-
int minDims = elementType.getValue() == ElementType.BIT ? Byte.SIZE : 1;
246-
if (dims < minDims || dims > maxDims) {
247-
throw new MapperParsingException(
248-
"The number of dimensions should be in the range [" + minDims + ", " + maxDims + "] but was [" + dims + "]"
249-
);
250-
}
251-
if (elementType.getValue() == ElementType.BIT) {
252-
if (dims % Byte.SIZE != 0) {
253-
throw new MapperParsingException("The number of dimensions for should be a multiple of 8 but was [" + dims + "]");
254-
}
255-
}
256-
});
231+
private final Parameter<Integer> dims;
257232
private final Parameter<VectorSimilarity> similarity;
258233

259234
private final Parameter<DenseVectorIndexOptions> indexOptions;
@@ -266,8 +241,38 @@ public static class Builder extends FieldMapper.Builder {
266241
public Builder(String name, IndexVersion indexVersionCreated) {
267242
super(name);
268243
this.indexVersionCreated = indexVersionCreated;
244+
// This is defined as updatable because it can be updated once, from [null] to a valid dim size,
245+
// by a dynamic mapping update. Once it has been set, however, the value cannot be changed.
246+
this.dims = new Parameter<>("dims", true, () -> null, (n, c, o) -> {
247+
if (o instanceof Integer == false) {
248+
throw new MapperParsingException("Property [dims] on field [" + n + "] must be an integer but got [" + o + "]");
249+
}
250+
251+
return XContentMapValues.nodeIntegerValue(o);
252+
}, m -> toType(m).fieldType().dims, XContentBuilder::field, Objects::toString).setSerializerCheck((id, ic, v) -> v != null)
253+
.setMergeValidator((previous, current, c) -> previous == null || Objects.equals(previous, current))
254+
.addValidator(dims -> {
255+
if (dims == null) {
256+
return;
257+
}
258+
int maxDims = elementType.getValue() == ElementType.BIT ? MAX_DIMS_COUNT_BIT : MAX_DIMS_COUNT;
259+
int minDims = elementType.getValue() == ElementType.BIT ? Byte.SIZE : 1;
260+
if (dims < minDims || dims > maxDims) {
261+
throw new MapperParsingException(
262+
"The number of dimensions should be in the range [" + minDims + ", " + maxDims + "] but was [" + dims + "]"
263+
);
264+
}
265+
if (elementType.getValue() == ElementType.BIT) {
266+
if (dims % Byte.SIZE != 0) {
267+
throw new MapperParsingException(
268+
"The number of dimensions for should be a multiple of 8 but was [" + dims + "]"
269+
);
270+
}
271+
}
272+
});
269273
final boolean indexedByDefault = indexVersionCreated.onOrAfter(INDEXED_BY_DEFAULT_INDEX_VERSION);
270274
final boolean defaultInt8Hnsw = indexVersionCreated.onOrAfter(IndexVersions.DEFAULT_DENSE_VECTOR_TO_INT8_HNSW);
275+
final boolean defaultBBQ8Hnsw = indexVersionCreated.onOrAfter(IndexVersions.DEFAULT_DENSE_VECTOR_TO_BBQ_HNSW);
271276
this.indexed = Parameter.indexParam(m -> toType(m).fieldType().indexed, indexedByDefault);
272277
if (indexedByDefault) {
273278
// Only serialize on newer index versions to prevent breaking existing indices when upgrading
@@ -297,14 +302,7 @@ public Builder(String name, IndexVersion indexVersionCreated) {
297302
this.indexOptions = new Parameter<>(
298303
"index_options",
299304
true,
300-
() -> defaultInt8Hnsw && elementType.getValue() == ElementType.FLOAT && this.indexed.getValue()
301-
? new Int8HnswIndexOptions(
302-
Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN,
303-
Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH,
304-
null,
305-
null
306-
)
307-
: null,
305+
() -> defaultIndexOptions(defaultInt8Hnsw, defaultBBQ8Hnsw),
308306
(n, c, o) -> o == null ? null : parseIndexOptions(n, o, indexVersionCreated),
309307
m -> toType(m).indexOptions,
310308
(b, n, v) -> {
@@ -328,7 +326,7 @@ public Builder(String name, IndexVersion indexVersionCreated) {
328326
|| Objects.equals(previous, current)
329327
|| previous.updatableTo(current)
330328
);
331-
if (defaultInt8Hnsw) {
329+
if (defaultInt8Hnsw || defaultBBQ8Hnsw) {
332330
this.indexOptions.alwaysSerialize();
333331
}
334332
this.indexed.addValidator(v -> {
@@ -351,6 +349,26 @@ public Builder(String name, IndexVersion indexVersionCreated) {
351349
});
352350
}
353351

352+
private DenseVectorIndexOptions defaultIndexOptions(boolean defaultInt8Hnsw, boolean defaultBBQHnsw) {
353+
if (elementType.getValue() == ElementType.FLOAT && this.indexed.getValue()) {
354+
if (defaultBBQHnsw && this.dims != null && this.dims.isConfigured() && this.dims.getValue() >= BBQ_DIMS_DEFAULT_THRESHOLD) {
355+
return new BBQHnswIndexOptions(
356+
Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN,
357+
Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH,
358+
new RescoreVector(DEFAULT_OVERSAMPLE)
359+
);
360+
} else if (defaultInt8Hnsw) {
361+
return new Int8HnswIndexOptions(
362+
Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN,
363+
Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH,
364+
null,
365+
null
366+
);
367+
}
368+
}
369+
return null;
370+
}
371+
354372
@Override
355373
protected Parameter<?>[] getParameters() {
356374
return new Parameter<?>[] { elementType, dims, indexed, similarity, indexOptions, meta };

server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH;
6767
import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN;
6868
import static org.elasticsearch.index.codec.vectors.IVFVectorsFormat.DYNAMIC_NPROBE;
69+
import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.DEFAULT_OVERSAMPLE;
6970
import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.IVF_FORMAT;
7071
import static org.hamcrest.Matchers.containsString;
7172
import static org.hamcrest.Matchers.equalTo;
@@ -85,7 +86,9 @@ public DenseVectorFieldMapperTests() {
8586
this.elementType = randomFrom(ElementType.BYTE, ElementType.FLOAT, ElementType.BIT);
8687
this.indexed = randomBoolean();
8788
this.indexOptionsSet = this.indexed && randomBoolean();
88-
this.dims = ElementType.BIT == elementType ? 4 * Byte.SIZE : 4;
89+
int baseDims = ElementType.BIT == elementType ? 4 * Byte.SIZE : 4;
90+
int randomMultiplier = ElementType.FLOAT == elementType ? randomIntBetween(1, 64) : 1;
91+
this.dims = baseDims * randomMultiplier;
8992
}
9093

9194
@Override
@@ -107,15 +110,28 @@ private void indexMapping(XContentBuilder b, IndexVersion indexVersion) throws I
107110
// Serialize if it's new index version, or it was not the default for previous indices
108111
b.field("index", indexed);
109112
}
110-
if (indexVersion.onOrAfter(DenseVectorFieldMapper.DEFAULT_TO_INT8)
113+
if ((indexVersion.onOrAfter(DenseVectorFieldMapper.DEFAULT_TO_INT8)
114+
|| indexVersion.onOrAfter(DenseVectorFieldMapper.DEFAULT_TO_BBQ))
111115
&& indexed
112116
&& elementType.equals(ElementType.FLOAT)
113117
&& indexOptionsSet == false) {
114-
b.startObject("index_options");
115-
b.field("type", "int8_hnsw");
116-
b.field("m", 16);
117-
b.field("ef_construction", 100);
118-
b.endObject();
118+
if (indexVersion.onOrAfter(DenseVectorFieldMapper.DEFAULT_TO_BBQ)
119+
&& dims >= DenseVectorFieldMapper.BBQ_DIMS_DEFAULT_THRESHOLD) {
120+
b.startObject("index_options");
121+
b.field("type", "bbq_hnsw");
122+
b.field("m", 16);
123+
b.field("ef_construction", 100);
124+
b.startObject("rescore_vector");
125+
b.field("oversample", DEFAULT_OVERSAMPLE);
126+
b.endObject();
127+
b.endObject();
128+
} else {
129+
b.startObject("index_options");
130+
b.field("type", "int8_hnsw");
131+
b.field("m", 16);
132+
b.field("ef_construction", 100);
133+
b.endObject();
134+
}
119135
}
120136
if (indexed) {
121137
b.field("similarity", elementType == ElementType.BIT ? "l2_norm" : "dot_product");
@@ -2038,15 +2054,24 @@ public void testDefaultParamsIndexByDefault() throws Exception {
20382054
public void testValidateOnBuild() {
20392055
final MapperBuilderContext context = MapperBuilderContext.root(false, false);
20402056

2057+
int dimensions = randomIntBetween(64, 1024);
20412058
// Build a dense vector field mapper with float element type, which will trigger int8 HNSW index options
20422059
DenseVectorFieldMapper mapper = new DenseVectorFieldMapper.Builder("test", IndexVersion.current()).elementType(ElementType.FLOAT)
2060+
.dimensions(dimensions)
20432061
.build(context);
20442062

20452063
// Change the element type to byte, which is incompatible with int8 HNSW index options
20462064
DenseVectorFieldMapper.Builder builder = (DenseVectorFieldMapper.Builder) mapper.getMergeBuilder();
20472065
builder.elementType(ElementType.BYTE);
20482066
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> builder.build(context));
2049-
assertThat(e.getMessage(), containsString("[element_type] cannot be [byte] when using index type [int8_hnsw]"));
2067+
assertThat(
2068+
e.getMessage(),
2069+
containsString(
2070+
dimensions >= DenseVectorFieldMapper.BBQ_DIMS_DEFAULT_THRESHOLD
2071+
? "[element_type] cannot be [byte] when using index type [bbq_hnsw]"
2072+
: "[element_type] cannot be [byte] when using index type [int8_hnsw]"
2073+
)
2074+
);
20502075
}
20512076

20522077
private static float[] decodeDenseVector(IndexVersion indexVersion, BytesRef encodedVector) {

0 commit comments

Comments
 (0)