For AI agents: this page is also available as Markdown at /docs/multi-vector-fields.md. Every docs page is: add .md to its URL. llms.txt lists them all.

Multi-vector fields

A long document rarely fits one embedding well. A multi-vector field lets one document hold many vectors, typically one per passage or chunk, and search returns one hit per document, scored by the single vector that matches your query best.

Define a multi-vector field

Availability: the search engine supports several vectors per document, but the public API has not been verified end to end for it. Mappings created through the Elasticsearch-compatible API do not set multi, and dense_vector arrays of arrays are not covered by the compatibility notes. Until this is confirmed in Feature status, model chunks as one document per chunk with a shared parent id field, and use collapse on that field to get one hit per parent. The engine-level behavior is described below.

Set multi: true on a vector field, or name the field with the _vs suffix:

{
  "fields": {
    "passages_vs": {
      "type": "vector",
      "dims": 768,
      "metric": "cosine",
      "multi": true
    }
  }
}

As with any vector field, a _vs field is storage only until you give it a metric, so define dims and metric explicitly. Everything in Vector search applies: exact and approximate search, filters, similarity and the tuning knobs.

Write the vectors

Send an array of arrays. A bare array is accepted as a list of one vector.

{
  "id": "doc-17",
  "title_t": "Shipping and returns policy",
  "passages_vs": [
    [0.12, 0.40, 0.05],
    [0.33, 0.21, 0.58],
    [0.09, 0.71, 0.14]
  ]
}

(The vectors here have 3 components to keep the example short. Yours must match the field's dims.)

Every vector must have the same dimension, and every element must be a finite float32. Replacing a document replaces all of its vectors. Each vector counts toward index size, so a document with 50 chunks costs about 50 times the storage of a single-vector document. See Ingesting documents for batching.

Query

You query a multi-vector field exactly like a single-vector one, with one query vector:

{
  "query": {
    "knn": {
      "field": "passages_vs",
      "query": [0.30, 0.25, 0.55],
      "k": 10
    }
  },
  "filter": ["lang_s:en"],
  "limit": 10,
  "get_scores": true,
  "fields": ["id", "title_t"]
}

Scoring is the maximum over a document's vectors: WarpSearch compares your query with every vector the document holds and keeps the best similarity. The result has one hit per document, however many of its vectors were close. k counts documents, not vectors.

Thresholds follow the same rule. With similarity, a document stays if its best vector passes. See Similarity thresholds.

Chunking pattern for long documents

The usual recipe has three parts:

  1. Split the document into passages. Pick a size your embedding model handles well, often a few hundred tokens, with a little overlap between neighbors.
  2. Embed each passage and write all of them into one multi-vector field on the document.
  3. Search by the query's embedding. The best passage decides the document's rank.

Keep the passage text out of the vector field. If you want to show the matching passage, store the chunk text yourself and look it up after the search. There are two common layouts:

Layout How it works Use it when
One document, many vectors A _vs field on the parent document. Chunk text lives in your own storage, keyed by document id. You only need the parent document back and the best chunk's position is enough.
One document per chunk Each chunk is a document with a parent_id_s field, its own text and a single vector. You need the matching chunk text in the response, or per-chunk filters.

A search returns the score, not which vector won. If you need to display the winning passage, the second layout is the simplest because the hit is the chunk itself.

Parent and child style with groups

With one document per chunk, you can get one result per parent by collapsing on the parent id and asking kNN for groups:

{
  "query": {
    "knn": {
      "field": "chunk_v",
      "query": [0.30, 0.25, 0.55],
      "k": 10,
      "unit": "groups"
    }
  },
  "collapse": "parent_id_s",
  "limit": 10,
  "get_scores": true,
  "fields": ["id", "parent_id_s", "chunk_t"]
}

k now selects up to 10 parents. Several close chunks from the same parent no longer use up other parents' places, and the hit shown for each parent is its best chunk, which carries the text you wanted. A group's score is its best member's score. Filters and deletes apply before groups are chosen.

If you combine a multi-vector field with groups, WarpSearch first takes each document's best vector and then lets documents compete inside their group. See Nearest groups and the collapse section of Searching.

Multi-vector search in hybrid queries

A multi-vector kNN query is an ordinary source in hybrid fusion. Put it next to a keyword query and fuse the two lists. See Hybrid search.

Late interaction (MaxSim)

Models such as ColBERT and ColPali embed every token of a text or every patch of an image, and score a document by late interaction. For each query token vector, find the best matching document vector, then sum those best scores over all query tokens. That is called summed MaxSim, and it is different from the best single vector scoring on this page.

Coming soon: late interaction scoring with summed MaxSim is in development. We give no dates, and details may change before release.

What is being built, so you can plan around it:

Until it ships, you can approximate late interaction in your own code: fetch candidates with the best single vector search, then compute MaxSim in your application on vectors you hold. The same advice applies to any reranking, see Hybrid search.

Limits and tips

View this page as Markdown