Vector & semantic search

This section documents the current Elasticsearch 9.x line (with 8.19 as the final 8.x release) as published at the Elasticsearch documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Kibana-only UIs, the ML/NLP model-management workflow, cross-cluster replication, and parts of the paid / serverless-only surface) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Elasticsearch iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Elasticsearch retrieves by vector similarity as well as by BM25 term matching. A dense_vector field holds an embedding and is searched with kNN; a sparse_vector field holds term-weight pairs produced by a model such as ELSER; a semantic_text field hides the embedding step entirely. Retrievers then compose these with ordinary full-text queries into one hybrid request. For where vector search fits among the alternatives see Choosing the Right Database (vector-database section); for the same idea in a document database see Couchbase Vector Search.

dense_vector and kNN

The dense_vector field

A dense_vector field stores a float array of fixed length. Set dims to the model’s output size (inferred from the first indexed vector if omitted), similarity to the distance function used for scoring (cosine is the default and normalises for you; dot_product needs unit-length vectors; l2_norm is Euclidean), and leave index at its default true so the values go into an HNSW graph for approximate search. index_options tunes that graph: type (int8_hnsw is the default and quantises to one byte per dimension, hnsw keeps full float32, int4_hnsw and bbq_hnsw compress further), m (neighbours per node, default 16) and ef_construction (build-time candidate list, default 100).

PUT /products
{
  "mappings": {
    "properties": {
      "name":              { "type": "text" },
      "name_embedding": {
        "type": "dense_vector",
        "dims": 384,
        "similarity": "cosine",
        "index": true,
        "index_options": {
          "type": "int8_hnsw",
          "m": 16,
          "ef_construction": 100
        }
      }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html

PUT /products/_doc/1
{
  "name": "wireless noise-cancelling headphones",
  "name_embedding": [0.12, -0.98, 0.33, "...384 floats..."]
}

Setting index: false stores the vector but builds no graph — it can then only be scored by the brute-force script_score shown below. See dense_vector field type for every index_options parameter and the quantization trade-offs.

Approximate kNN: the top-level knn option

The knn option on _search runs approximate nearest-neighbour search over the HNSW graph. field and query_vector say what to compare; k is how many neighbours to return; num_candidates (>= k, capped at 10000) is how many the graph keeps per shard while searching — raising it improves recall at the cost of latency. Hits are scored by similarity and merged into the normal hits.hits[].

GET /products/_search
{
  "knn": {
    "field": "name_embedding",
    "query_vector": [0.11, -0.95, 0.30, "...384 floats..."],
    "k": 10,
    "num_candidates": 100
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html

To search from text instead of a pre-computed vector, replace query_vector with query_vector_builder and let an inference endpoint embed the query at search time:

GET /products/_search
{
  "knn": {
    "field": "name_embedding",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": "my-text-embedding-model",
        "model_text": "headphones that block background noise"
      }
    },
    "k": 10,
    "num_candidates": 100
  }
}

knn may also be given as an array of clauses to search several vector fields at once. Full parameter list and tuning guidance: k-nearest neighbor (kNN) search.

Exact kNN with script_score

When the candidate set is already small (a tight filter, or a field mapped with index: false), a script_score query over every matching document gives exact results with no graph. The vector functions (cosineSimilarity, dotProduct, l1norm, l2norm) take the query vector as a script parameter; add a constant so the final _score stays non-negative.

GET /products/_search
{
  "query": {
    "script_score": {
      "query": { "term": { "category": "audio" } },
      "script": {
        "source": "cosineSimilarity(params.qv, 'name_embedding') + 1.0",
        "params": {
          "qv": [0.11, -0.95, 0.30, "...384 floats..."]
        }
      }
    }
  }
}
// Brute force: cost scales with the number of docs matching the inner query.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#exact-knn

Filtered kNN

A filter inside the knn option restricts which documents can be returned while the graph is traversed (pre-filtering), so k results come back even when the filter is highly selective — unlike a post-filter that would thin an already-small neighbour list. The filter is a standard term-level query.

GET /products/_search
{
  "knn": {
    "field": "name_embedding",
    "query_vector": [0.11, -0.95, 0.30, "...384 floats..."],
    "k": 10,
    "num_candidates": 100,
    "filter": {
      "bool": {
        "must":   [ { "term":  { "in_stock": true } } ],
        "filter": [ { "range": { "price": { "lte": 300 } } } ]
      }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#knn-search-filter-example

Sparse vectors, ELSER and semantic_text

sparse_vector and ELSER

ELSER (Elastic Learned Sparse EncodeR) is a built-in model that turns text into a set of weighted term tokens rather than a dense embedding. Those pairs are stored in a sparse_vector field, and querying is lexical-style term expansion, so no dims or similarity is involved. The usual path is an inference endpoint plus an ingest pipeline (or semantic_text, below) that fills the field on write.

// 1. Create an inference endpoint backed by ELSER (downloads/starts the model).
PUT /_inference/sparse_embedding/my-elser
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".elser_model_2"
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/infer-service-elasticsearch.html

PUT /articles
{
  "mappings": {
    "properties": {
      "body":        { "type": "text" },
      "body_tokens": { "type": "sparse_vector" }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html

At search time the sparse_vector query points at the same endpoint, which expands the query string into weighted tokens and scores documents by their overlap:

GET /articles/_search
{
  "query": {
    "sparse_vector": {
      "field": "body_tokens",
      "inference_id": "my-elser",
      "query": "how do I renew an expired certificate"
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html

Background and deployment sizing for the model: ELSER.

semantic_text and the semantic query

A semantic_text field wires the model in at mapping time: you name an inference_id once, and every value indexed into the field is sent to that endpoint, chunked into passages that fit the model’s input limit, embedded, and stored — dense or sparse depending on the endpoint’s task type. No ingest pipeline, no manual vector field.

PUT /docs
{
  "mappings": {
    "properties": {
      "content": {
        "type": "semantic_text",
        "inference_id": "my-elser",
        "chunking_settings": {
          "strategy": "sentence",
          "max_chunk_size": 250,
          "sentence_overlap": 1
        }
      }
    }
  }
}
// inference_id defaults to the built-in ".elser-2-elasticsearch" endpoint if omitted.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html

PUT /docs/_doc/1
{ "content": "Long multi-paragraph text. Elasticsearch splits it into chunks and embeds each one." }

The semantic query takes only the field and the natural-language string; Elasticsearch embeds the query with the same endpoint and matches it against the stored chunks, returning the best passages in inner_hits:

GET /docs/_search
{
  "query": {
    "semantic": {
      "field": "content",
      "query": "how is long text broken up before embedding"
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html

The end-to-end walk-through is Semantic search with semantic_text; the conceptual overview of all three approaches (dense, ELSER, semantic_text) is Semantic search.

A retriever is a node in a tree that produces a ranked document set, replacing the flat query/knn top-level keys. Composing retrievers is how lexical and vector results are combined and re-ranked in a single call.

Retriever What it returns

standard

The hits of any Query DSL query — the wrapper for BM25 / full-text retrieval

knn

Approximate kNN hits, same parameters as the top-level knn option

rrf

A fused ranking of its child retrievers via Reciprocal Rank Fusion

text_similarity_reranker

Its child’s hits re-ordered by a cross-encoder / rerank inference endpoint

rrf: Reciprocal Rank Fusion for hybrid results

rrf merges its retrievers by rank position, not by score, so a BM25 score and a cosine similarity never have to be normalised onto one scale. Each document scores sum(1 / (rank_constant + rank)) across the lists it appears in; rank_constant (default 60) damps the contribution of low ranks and rank_window_size (default 10) is how deep into each child list the fusion looks.

GET /products/_search
{
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": { "match": { "name": "noise cancelling headphones" } }
          }
        },
        {
          "knn": {
            "field": "name_embedding",
            "query_vector": [0.11, -0.95, 0.30, "...384 floats..."],
            "k": 20,
            "num_candidates": 100
          }
        }
      ],
      "rank_constant": 60,
      "rank_window_size": 20
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html

text_similarity_reranker

Wrap any retriever to have a rerank model re-score the top rank_window_size hits against the query text — typically as the outer stage over an rrf node.

GET /docs/_search
{
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "standard": { "query": { "semantic": { "field": "content", "query": "certificate renewal steps" } } }
      },
      "field": "content",
      "inference_id": "my-rerank-endpoint",
      "inference_text": "certificate renewal steps",
      "rank_window_size": 50
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/retrievers-overview.html

The retriever catalogue, nesting rules and which _search features (pagination, aggregations, highlighting) work with each are in Retrievers. Retrievers, aggregations and result shaping across the rest of the search surface are covered in Search extras and Search API & pagination.