Skip to content

Commit 622471c

Browse files
pmpailismridula-s109
authored andcommitted
Make bbq_hnsw the default index option for dense-vector fields with more than 384 dimensions (elastic#129825)
1 parent 20b0d87 commit 622471c

File tree

10 files changed

+210
-70
lines changed

10 files changed

+210
-70
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`.

rest-api-spec/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ tasks.named("yamlRestCompatTestTransform").configure ({ task ->
6363
task.skipTest("cat.shards/10_basic/Help", "sync_id is removed in 9.0")
6464
task.skipTest("search/500_date_range/from, to, include_lower, include_upper deprecated", "deprecated parameters are removed in 9.0")
6565
task.skipTest("search.highlight/30_max_analyzed_offset/Plain highlighter with max_analyzed_offset < 0 should FAIL", "semantics of test has changed")
66+
task.skipTest("search.vectors/70_dense_vector_telemetry/Field mapping stats with field details", "default dense vector field mapping has changed")
6667
task.skipTest("range/20_synthetic_source/Double range", "_source.mode mapping attribute is no-op since 9.0.0")
6768
task.skipTest("range/20_synthetic_source/Float range", "_source.mode mapping attribute is no-op since 9.0.0")
6869
task.skipTest("range/20_synthetic_source/Integer range", "_source.mode mapping attribute is no-op since 9.0.0")

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/70_dense_vector_telemetry.yml

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
setup:
22
- requires:
3-
cluster_features: [ "gte_v8.4.0" ]
4-
reason: "Cluster mappings stats for indexed dense vector was added in 8.4"
3+
cluster_features: [ "search.vectors.mappers.default_bbq_hnsw" ]
4+
reason: "Test cluster feature 'search.vectors.mappers.default_bbq_hnsw' is required for using bbq as default
5+
indexing for vector fields."
56
- skip:
67
features: headers
78

@@ -13,7 +14,7 @@ setup:
1314
index.number_of_shards: 2
1415
mappings:
1516
properties:
16-
vector1:
17+
vector_hnsw_explicit:
1718
type: dense_vector
1819
dims: 768
1920
index: true
@@ -23,12 +24,16 @@ setup:
2324
type: hnsw
2425
m: 16
2526
ef_construction: 100
26-
vector2:
27+
vector_bbq_default:
2728
type: dense_vector
2829
dims: 1024
2930
index: true
3031
similarity: dot_product
31-
vector3:
32+
vector_int8_hnsw_default:
33+
type: dense_vector
34+
dims: 100
35+
index: true
36+
vector_no_index:
3237
type: dense_vector
3338
dims: 100
3439
index: false
@@ -52,10 +57,10 @@ setup:
5257
- do: { cluster.stats: { } }
5358
- length: { indices.mappings.field_types: 1 }
5459
- match: { indices.mappings.field_types.0.name: dense_vector }
55-
- match: { indices.mappings.field_types.0.count: 4 }
60+
- match: { indices.mappings.field_types.0.count: 5 }
5661
- match: { indices.mappings.field_types.0.index_count: 2 }
57-
- match: { indices.mappings.field_types.0.indexed_vector_count: 3 }
58-
- match: { indices.mappings.field_types.0.indexed_vector_dim_min: 768 }
62+
- match: { indices.mappings.field_types.0.indexed_vector_count: 4 }
63+
- match: { indices.mappings.field_types.0.indexed_vector_dim_min: 100 }
5964
- match: { indices.mappings.field_types.0.indexed_vector_dim_max: 1024 }
6065
---
6166
"Field mapping stats with field details":
@@ -70,15 +75,16 @@ setup:
7075
- do: { cluster.stats: { } }
7176
- length: { indices.mappings.field_types: 1 }
7277
- match: { indices.mappings.field_types.0.name: dense_vector }
73-
- match: { indices.mappings.field_types.0.count: 4 }
78+
- match: { indices.mappings.field_types.0.count: 5 }
7479
- match: { indices.mappings.field_types.0.index_count: 2 }
75-
- match: { indices.mappings.field_types.0.indexed_vector_count: 3 }
76-
- match: { indices.mappings.field_types.0.indexed_vector_dim_min: 768 }
80+
- match: { indices.mappings.field_types.0.indexed_vector_count: 4 }
81+
- match: { indices.mappings.field_types.0.indexed_vector_dim_min: 100 }
7782
- match: { indices.mappings.field_types.0.indexed_vector_dim_max: 1024 }
7883
- match: { indices.mappings.field_types.0.vector_index_type_count.hnsw: 1 }
79-
- match: { indices.mappings.field_types.0.vector_index_type_count.int8_hnsw: 2 }
84+
- match: { indices.mappings.field_types.0.vector_index_type_count.int8_hnsw: 1 }
85+
- match: { indices.mappings.field_types.0.vector_index_type_count.bbq_hnsw: 2 }
8086
- match: { indices.mappings.field_types.0.vector_index_type_count.not_indexed: 1 }
8187
- match: { indices.mappings.field_types.0.vector_similarity_type_count.l2_norm: 2 }
8288
- match: { indices.mappings.field_types.0.vector_similarity_type_count.dot_product: 1 }
83-
- match: { indices.mappings.field_types.0.vector_element_type_count.float: 3 }
89+
- match: { indices.mappings.field_types.0.vector_element_type_count.float: 4 }
8490
- match: { indices.mappings.field_types.0.vector_element_type_count.byte: 1 }

server/src/internalClusterTest/java/org/elasticsearch/index/mapper/DynamicMappingIT.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_IGNORE_DYNAMIC_BEYOND_LIMIT_SETTING;
5454
import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_NESTED_FIELDS_LIMIT_SETTING;
5555
import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING;
56+
import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.BBQ_DIMS_DEFAULT_THRESHOLD;
5657
import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.MIN_DIMS_FOR_DYNAMIC_FLOAT_MAPPING;
5758
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
5859
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
@@ -908,6 +909,59 @@ public void testKnnSubObject() throws Exception {
908909
client().index(
909910
new IndexRequest("test").source("obj.vector", Randomness.get().doubles(MIN_DIMS_FOR_DYNAMIC_FLOAT_MAPPING, 0.0, 5.0).toArray())
910911
).get();
912+
}
913+
914+
public void testDenseVectorDynamicMapping() throws Exception {
915+
assertAcked(indicesAdmin().prepareCreate("test").setMapping("""
916+
{
917+
"dynamic": "true"
918+
}
919+
""").get());
920+
921+
client().index(
922+
new IndexRequest("test").source("vector_int8", Randomness.get().doubles(BBQ_DIMS_DEFAULT_THRESHOLD - 1, 0.0, 5.0).toArray())
923+
).get();
924+
client().index(
925+
new IndexRequest("test").source("vector_bbq", Randomness.get().doubles(BBQ_DIMS_DEFAULT_THRESHOLD, 0.0, 5.0).toArray())
926+
).get();
927+
Map<String, Object> mappings = indicesAdmin().prepareGetMappings(TEST_REQUEST_TIMEOUT, "test")
928+
.get()
929+
.mappings()
930+
.get("test")
931+
.sourceAsMap();
932+
assertTrue(new WriteField("properties.vector_int8", () -> mappings).exists());
933+
assertTrue(new WriteField("properties.vector_int8.index_options.type", () -> mappings).get(null).toString().equals("int8_hnsw"));
934+
assertTrue(new WriteField("properties.vector_bbq", () -> mappings).exists());
935+
assertTrue(new WriteField("properties.vector_bbq.index_options.type", () -> mappings).get(null).toString().equals("bbq_hnsw"));
936+
}
937+
938+
public void testBBQDynamicMappingWhenFirstIngestingDoc() throws Exception {
939+
assertAcked(indicesAdmin().prepareCreate("test").setMapping("""
940+
{
941+
"properties": {
942+
"vector": {
943+
"type": "dense_vector"
944+
}
945+
}
946+
}
947+
""").get());
911948

949+
Map<String, Object> mappings = indicesAdmin().prepareGetMappings(TEST_REQUEST_TIMEOUT, "test")
950+
.get()
951+
.mappings()
952+
.get("test")
953+
.sourceAsMap();
954+
assertTrue(new WriteField("properties.vector", () -> mappings).exists());
955+
assertFalse(new WriteField("properties.vector.index_options.type", () -> mappings).exists());
956+
957+
client().index(new IndexRequest("test").source("vector", Randomness.get().doubles(BBQ_DIMS_DEFAULT_THRESHOLD, 0.0, 5.0).toArray()))
958+
.get();
959+
Map<String, Object> updatedMappings = indicesAdmin().prepareGetMappings(TEST_REQUEST_TIMEOUT, "test")
960+
.get()
961+
.mappings()
962+
.get("test")
963+
.sourceAsMap();
964+
assertTrue(new WriteField("properties.vector", () -> updatedMappings).exists());
965+
assertTrue(new WriteField("properties.vector.index_options.type", () -> updatedMappings).get(null).toString().equals("bbq_hnsw"));
912966
}
913967
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ private static Version parseUnchecked(String version) {
177177
public static final IndexVersion MAPPER_TEXT_MATCH_ONLY_MULTI_FIELDS_DEFAULT_NOT_STORED = def(9_029_0_00, Version.LUCENE_10_2_1);
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);
180+
public static final IndexVersion DEFAULT_DENSE_VECTOR_TO_BBQ_HNSW = def(9_032_0_00, Version.LUCENE_10_2_2);
180181

181182
/*
182183
* STOP! READ THIS FIRST! No, really,

server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ private static void postProcessDynamicArrayMapping(DocumentParserContext context
806806
fieldName,
807807
context.indexSettings().getIndexVersionCreated()
808808
);
809+
builder.dimensions(mappers.size());
809810
DenseVectorFieldMapper denseVectorFieldMapper = builder.build(builderContext);
810811
context.updateDynamicMappers(fullFieldName, List.of(denseVectorFieldMapper));
811812
}

0 commit comments

Comments
 (0)