Elasticsearch uses the Hierarchical Navigable Small World (HNSW) algorithm for approximate kNN search. Because HNSW is graph-based, vector search performs best when most vector data resides in off-heap memory. This topic covers key tuning areas that affect vector search performance and memory efficiency.
Set HNSW parameters
The m and ef_construction parameters control the recall rate of the HNSW graph. Configure them when creating a dense_vector index. For parameter details, see Dense vector field type.
HNSW is an approximate kNN search method and does not guarantee retrieval of all nearest neighbors. The m and ef_construction parameters are the primary levers for controlling recall rate.
|
Parameter |
Description |
Default |
Strict recall recommendation |
|
|
Number of neighbors per node. Larger values increase recall rate but raise memory usage and reduce indexing performance. |
16 |
64 or higher |
|
|
Number of nearest documents examined when building the HNSW graph for a new node. Larger values increase recall rate with significant impact on indexing performance, but no effect on memory usage. |
100 |
512 or higher |
Reduce memory usage with quantization
Quantization reduces vector memory by compressing each value. Use the following table to estimate memory usage by element_type and quantization level.
|
|
Quantization |
Memory per vector |
Reduction factor |
|
|
None |
|
1× |
|
|
|
|
~4× |
|
|
|
|
~8× |
|
|
|
|
~32× |
|
|
None |
|
4× vs float |
|
|
None |
|
32× vs float |
When quantization is enabled, Elasticsearch stores both the quantized vectors and the original vectors on disk. For example, applying int8 quantization to 40 GB of floating-point vectors adds 10 GB for quantized vectors, bringing total disk usage to 50 GB — while memory required for fast search drops to 10 GB.
BBQ quantization provides the greatest memory reduction but increases the proportion of memory consumed by the HNSW graph index itself. Factor this into your memory estimates when using bbq.
Calculate vector index memory
Total memory has two components: vector data and the HNSW graph index.
Vector data memory (multiply by num_vectors):
|
Configuration |
Formula |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HNSW graph index memory:
num_vectors * 4 * HNSW.m
With the default m=16, this equals num_vectors * 4 * 16.
If the index uses the flat type (no HNSW graph), only the vector data memory applies.
Replicas: The default number_of_replicas is 1, so total memory is 2× the single-copy calculation.
Total memory = (vector data + graph index) × (1 + number_of_replicas)
Example
10 million vectors, 1,024 dimensions, int8 quantization, m=16, number_of_replicas=1:
2 × (10,000,000 × (1,024 + 4) + 10,000,000 × 4 × 16) = 20.34 GB
Two data nodes with 16 GB each: off-heap = (16 / 2) × 2 = 16 GB — not enough
Two data nodes with 32 GB each: off-heap = (32 / 2) × 2 = 32 GB — sufficient
In production, reserve additional off-heap memory for other indexes, source documents, and read/write network traffic. Insufficient off-heap memory typically manifests as high disk I/O and a large volume of random reads.
Check off-heap memory capacity
Off-heap memory is the portion of node memory available for vector data after the Java heap is reserved.
Node with ≤64 GB total memory: off-heap ≈ half of total memory
Node with >64 GB total memory: off-heap = total memory − 31 GB (default)
To get the exact value for a node, run:
GET _nodes/stats?human&filter_path=**.os.mem.total,**.jvm.mem.heap_max
Off-heap memory = os.mem.total − jvm.mem.heap_max
Preload the file system buffer
After a node restarts, the OS clears the file system buffer and must reload index data from disk before searches run at full speed. Use index.store.preload to instruct the OS to load specific index files into memory immediately on startup.
Eagerly preloading files for too many indexes can slow down searches if the file system buffer is smaller than the total data. Use this setting selectively.
Set index.store.preload when creating an index:
PUT /my_vector_index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"index.store.preload": ["vex", "veq"]
},
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text": {
"type": "keyword"
}
}
}
}
Which files to preload
For quantized indexes, preload only the quantized values and the HNSW graph. Preloading raw vectors (vec) is unnecessary and counterproductive when quantization is in use.
|
Index type |
Files to preload |
Notes |
|
HNSW (no quantization) |
|
|
|
|
|
|
|
BBQ quantization |
|
|
File extension reference:
vex: HNSW graph structurevec: non-quantized vector values (float, byte, bit element types)veq: quantized vectors (int4 or int8)veb: binary vectors (bbq)vem,vemf,vemq,vemb: metadata files — small, no need to preload
Update preload on an existing index
index.store.preload is a static parameter. To change it on an existing index, close the index, update the setting, then reopen it:
POST my_vector_index/_close
PUT my_vector_index/_settings
{
"index.store.preload": ["vex", "veq"]
}
POST my_vector_index/_open
Reduce segment count
Each Elasticsearch shard stores vectors as a separate HNSW graph per segment. kNN search must scan every segment, so fewer segments directly improve search performance — often by several times.
Elasticsearch merges smaller segments in the background automatically. If the default merge behavior is insufficient, use the following approaches.
Increase the maximum segment size
The index.merge.policy.max_merged_segment setting controls the maximum size of a merged segment. The default is 5 GB, which may be too small for high-dimensional vectors. Increasing it to 10–20 GB reduces segment count:
PUT my_vector_index/_settings
{
"index.merge.policy.max_merged_segment": "10gb"
}
Optimize settings during bulk indexing
During an initial bulk load, adjust index settings to encourage larger segments from the start:
Disable refresh: Set
index.refresh_intervalto-1to prevent refresh operations from creating extra segments.Increase the indexing buffer:
indices.memory.index_buffer_sizedefaults to 10% of heap size. For a 32 GB heap, this is usually sufficient.Raise the translog flush threshold: Increase
index.translog.flush_threshold_sizeto allow the full indexing buffer to be used before flushing.
Exclude vector fields from _source
Elasticsearch stores the full source document in the _source field. For documents with high-dimensional dense_vector fields, loading _source on every kNN search hit adds significant overhead.
Use the excludes mapping parameter to omit vector fields from _source:
PUT /my_vector_index
{
"mappings": {
"_source": {
"excludes": [
"my_vector"
]
},
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text": {
"type": "keyword"
}
}
}
}
A vector field excluded from _source remains fully usable for kNN search — the search process uses a separate data structure, not _source.
Excluding fields from _source affects reindex, update, and update by query operations. For example, the dense_vector field may not carry over to the new index during a reindex. Review these limitations before excluding _source fields.
Retrieve excluded vector values
On Elasticsearch 8.17 or later, use docvalue_fields:
GET my_vector_index/_search
{
"docvalue_fields": ["my_vector"]
}
On earlier versions, use script_fields:
GET my_vector_index/_search
{
"script_fields": {
"vector_field": {
"script": {
"source": "doc['my_vector'].vectorValue"
}
}
}
}
As an alternative to field exclusion, see synthetic _source.
Upgrade to a Turbo instance type
Vector similarity calculation is CPU-intensive. Turbo instance types provide more than double the CPU performance of standard instance types. Use a blue-green deployment to upgrade to a Turbo instance type of the same specification with minimal disruption.