The Single-Vector Compression Problem
Dense embedding models compress entire documents into fixed-size vectors—384, 768, maybe 1024 dimensions—and that compression is lossy in a specific way. A product SKU, a rare surname, or the one clause in a long paragraph that actually answers the query all have to fight for room in the same blob of floats.
When you search for "green sofa with wooden legs and rounded cushions," a single vector blends all four constraints into one point. The result? A green sofa with metal legs ends up sitting right next to the one you actually wanted.
Sentence Transformers v6.0 just shipped support for multi-vector models—also called late-interaction or ColBERT-style models—that skip that compression entirely. Instead of pooling token embeddings into one summary vector, they keep one vector per token and defer the interaction until scoring time.
How Late Interaction Works
A multi-vector model runs the same transformer as a dense encoder, but instead of mean-pooling the output, it projects each token embedding down to a smaller dimension (typically 128) and preserves all of them. A 9-token document becomes a 9×128 matrix, not a 1×128 vector.
The magic happens at scoring time with the MaxSim operator: for each query token, find its highest similarity against any document token, then sum those maxima across the query.
MaxSim(Q, D) = Σ max(Qi · Dj) for all query tokens Qi
Because token embeddings are L2-normalized, each dot product is a cosine similarity in [-1, 1], so the final score lands in [-num_query_tokens, num_query_tokens].
You can read MaxSim as a soft alignment: every query token points at the document token that best explains it, and the score reflects how well the document supports the query overall. The alignment doesn't have to be lexical—encode "Where do penguins live?" against "Penguins inhabit Antarctica." with lightonai/mLateOn and the query token live finds its best match on inhabit at 0.94, despite sharing zero characters.
What You Gain
Late interaction preserves token-level matching information that single-vector models average away. This matters most on:
- Multi-requirement queries where each constraint gets to find its own evidence
- Exact-match scenarios like product codes, surnames, or function names that would otherwise drown in the pooled representation
- Out-of-domain data where a dense model's learned compression was tuned for a different distribution
- Long documents where more content has to squeeze into the same fixed vector
The retrieval quality improvement grows with document length and query specificity. When one specific piece of a document makes it relevant, MaxSim can isolate that evidence while a single vector had to blend it with everything else.
What It Costs You
The index size blows up. One vector per token instead of one per document is a lot more vectors, only partly offset by the lower dimensionality.
Encoding 4,874 Natural Questions passages with lightonai/LateOn produced 608,414 token vectors—an average of 124.8 per passage:
| Representation | Vectors | Dimensions | float32 size |
|---|---|---|---|
| Dense, all-MiniLM-L6-v2 | 4,874 | 384 | 7.5 MB |
| Dense, gte-modernbert-base | 4,874 | 768 | 15.0 MB |
| Multi-vector, LateOn | 608,414 | 128 | 311.5 MB |
That's 42× the storage of the MiniLM index, or roughly 62 KiB per passage.
However, indexes compress. The same 608,414 vectors take 92 MB as a fast-plaid index, which stores a centroid ID plus a quantized residual per vector rather than the raw floats. For context, a 4096-dimensional dense model like Qwen3-Embedding-8B would need about 80 MB for those same passages, so compressed multi-vector indexes land in the same territory as the high-dimensional dense models people already run.
Using These Models Today
Loading a multi-vector model looks identical to loading any other Sentence Transformers model:
from sentence_transformers import MultiVectorEncoder
model = MultiVectorEncoder("lightonai/LateOn")
The new MultiVectorEncoder class reads three checkpoint formats that have emerged over the years:
- PyLate checkpoints (native Sentence Transformers schema)
- Stanford-NLP ColBERT checkpoints (detected via
HF_ColBERTarchitecture marker) - ColPali-family models for visual document retrieval (configuration currently being added)
Any PyLate or Stanford-NLP checkpoint loads directly, even if it predates the v6.0 release. Look for the multi-vector and sentence-transformers tags on the Hub, though the ecosystem tag migration is still underway.
Encoding and Scoring
Queries and documents encode separately:
query_vecs = model.encode("Where do penguins live?", prompt_name="query")
doc_vecs = model.encode("Penguins inhabit Antarctica.", prompt_name="document")
The prompt_name parameter is critical—it triggers the marker prefixes and padding behavior each checkpoint expects. Query embeddings often get padded with [MASK] tokens to a fixed length, while documents truncate at a different limit.
Scoring uses the similarity function built into the model:
score = model.similarity(query_vecs, doc_vecs)
Under the hood, that's MaxSim: query_vecs is shape [num_query_tokens, 128], doc_vecs is [num_doc_tokens, 128], and the function computes the sum of per-query-token maxima.
Beyond Text: Visual Document Retrieval
The state of the art for visual document retrieval is late interaction applied to page images. Models like ColPali match a text query directly against page images with no OCR step, producing one vector per image patch.
This matters for documents where layout, figures, or formatting carry semantic weight—technical manuals, financial reports, academic papers. Traditional pipelines OCR the page, chunk the text, embed the chunks, then throw away everything the OCR missed or mangled. Vision-based retrieval skips that lossy conversion.
With the [image] extra installed, the same MultiVectorEncoder API handles vision models:
from sentence_transformers import MultiVectorEncoder
from PIL import Image
model = MultiVectorEncoder("vidore/colpali-v1.3")
query_vecs = model.encode("What were Q3 revenues?", prompt_name="query")
page_img = Image.open("report_page_5.png")
doc_vecs = model.encode(page_img, prompt_name="document")
score = model.similarity(query_vecs, doc_vecs)
The model processes the image through a vision transformer, produces patch embeddings, and MaxSim aligns query tokens to image patches. The versatility extends to audio and video retrieval as well, using the same operator.
Retrieve-and-Rerank: No Index Required
If index size or complexity is a dealbreaker, you can sidestep it entirely with a retrieve-and-rerank pipeline: use a cheap dense model for the first-pass retrieval, then rescore the top-k with a multi-vector model.
This works because late-interaction models are bi-encoders—documents encode independently, so you can encode the top-100 candidates on-demand at query time without re-encoding the full corpus.
from sentence_transformers import SentenceTransformer, MultiVectorEncoder
retriever = SentenceTransformer("all-MiniLM-L6-v2")
reranker = MultiVectorEncoder("lightonai/LateOn")
# First pass: dense retrieval
candidates = retriever.encode(docs).search(query, top_k=100)
# Second pass: rerank with late interaction
reranker_scores = reranker.similarity(
reranker.encode(query, prompt_name="query"),
reranker.encode(candidates, prompt_name="document")
)
This keeps the index small and fast while still getting the quality lift where it matters.
The PyLate Legacy
Sentence Transformers historically handled dense and sparse models but not late interaction, so LightOn built PyLate on top of it to fill that gap. Much of the multi-vector ecosystem—training recipes, checkpoints, fast-plaid indexing—emerged from PyLate's work.
With v6.0, those capabilities migrate upstream into Sentence Transformers itself. The MultiVectorEncoder class consumes PyLate checkpoints natively, and the same familiar API now spans dense, sparse, reranker, and multi-vector models.
If you're coming from PyLate or colpali-engine, the migration guide in the source post walks through the import changes and new patterns.
When to Use Multi-Vector Models
Late interaction makes sense when:
- Retrieval quality matters more than index size
- Queries have multiple specific requirements
- Documents are long or domain-specific
- Exact matches (entities, codes, identifiers) coexist with semantic relevance
- You're doing visual, audio, or video retrieval where patch-level alignment is the entire point
It's not a silver bullet. Dense models with careful prompt engineering and domain fine-tuning still win on cost, speed, and simplicity for many workloads. But the tradeoff is now accessible through the same library and API you already use.
The fact that PyLate checkpoints, Stanford-NLP ColBERT models, and ColPali vision retrievers all load with one MultiVectorEncoder() call means late interaction just became boring infrastructure—and that's when things get interesting.