Vector engine best practices

更新时间:
复制 MD 格式

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.

Note

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

m

Number of neighbors per node. Larger values increase recall rate but raise memory usage and reduce indexing performance.

16

64 or higher

ef_construction

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.

element_type

Quantization

Memory per vector

Reduction factor

float

None

num_dimensions * 4 bytes

float

int8

num_dimensions + 4 bytes

~4×

float

int4

num_dimensions / 2 + 4 bytes

~8×

float

bbq (Better Binary Quantization)

num_dimensions / 8 + 12 bytes

~32×

byte

None

num_dimensions bytes

4× vs float

bit

None

num_dimensions / 8 bytes

32× vs float

Note

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

element_type: float

num_vectors * num_dimensions * 4

element_type: float + int8

num_vectors * (num_dimensions + 4)

element_type: float + int4

num_vectors * (num_dimensions / 2 + 4)

element_type: float + bbq

num_vectors * (num_dimensions / 8 + 12)

element_type: byte

num_vectors * num_dimensions

element_type: bit

num_vectors * (num_dimensions / 8)

HNSW graph index memory:

num_vectors * 4 * HNSW.m

With the default m=16, this equals num_vectors * 4 * 16.

Note

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 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.

Important

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)

["vex", "vec"]

vex = HNSW graph; vec = raw vectors

int8 or int4 quantization

["vex", "veq"]

veq = quantized vectors

BBQ quantization

["vex", "veb"]

veb = binary vectors

File extension reference:

  • vex: HNSW graph structure

  • vec: 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_interval to -1 to prevent refresh operations from creating extra segments.

  • Increase the indexing buffer: indices.memory.index_buffer_size defaults to 10% of heap size. For a 32 GB heap, this is usually sufficient.

  • Raise the translog flush threshold: Increase index.translog.flush_threshold_size to 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.

Important

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.