# 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](roadmap-and-status.md), 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:

```json
{
  "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](vector-search.md) 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.

```json
{
  "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](ingesting-documents.md) for batching.

## Query

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

```json
{
  "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](vector-search.md#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:

```json
{
  "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](vector-search.md#nearest-groups) and the collapse section of [Searching](searching.md).

## 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](hybrid-search.md).

## 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:

- An opt-in scoring mode for multi-valued vector fields. The default stays best single vector.
- A query that carries many token vectors instead of one, up to 1,024 of them.
- Scoring under the field's metric: `sum over query vectors of (max over document vectors of
  similarity)`.
- Brute-force scoring over the documents your filters select, with no approximate index for these
  fields. That suits small collections or a filtered candidate set, for example a re-ranking step
  after a first-stage search has narrowed the candidates. It has not been benchmarked at scale.
- Grouped kNN (`unit: "groups"`) is not planned to combine with several query vectors in the first
  version.

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](hybrid-search.md#reranking).

## Limits and tips

- A multi-valued vector field cannot be a collapse key.
- Scores are best-single-vector scores. Documents with many vectors are not penalized or favored by
  how many they hold, but a document with more chunks has more chances to match by luck. Keep chunk
  sizes consistent.
- Search cost grows with the total number of vectors, not the number of documents. Approximate indexes
  help once segments are large enough, see [When indexes get built](vector-search.md#when-indexes-get-built).
- To reduce storage for many-chunk collections, consider [`truncate_dims`](matryoshka-and-dimensions.md)
  with an MRL model.
