Files
lancedb/docs/web/search/vector-search.mdx
T
Brendan Clement 05c7ba4889 docs: correct the labelled pages against the pinned deployment
Authorization wrote objects as namespace:db/ns and table:db/ns/table,
which the deployment rejects; it writes namespace:db:ns and
table:db:ns$table, nesting namespaces with $. The page now lists the
view, function and secret objects, CREATE_FUNCTION and CREATE_SECRET,
USAGE and UPDATE where they apply beyond tables, and what OPERATE and
USAGE on system allow. An a: subject names the principal holding a key
rather than a narrower grantee, creation falls back to the parent's
create privilege only for an unowned database or namespace, and a
listing page holds at most 100 entries.

The materialized view page said a deployment's refresh, options and
stable row id needs depend on version and configuration. Newer versions
rebuild the whole view on every create and refresh as a job, need no
stable row ids, take no refresh options, and have no REST route to create
one; earlier ones differed, and their views have to be recreated. The
page and the REFRESH reference now say so, without naming which version
a deployment runs.

Vector search no longer says it has no SQL form: some deployment
versions plan ORDER BY distance(...) LIMIT k as one.
2026-10-08 21:31:15 -07:00

724 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Vector Search"
sidebarTitle: "Vector Search"
description: "Learn how to run vector search queries in LanceDB. Includes best practices, tips and examples."
icon: "arrow-up-right-dots"
availability:
oss: available
enterprise: available
summary: >-
Vector search runs in the embedded library and on a deployment. A few query
options depend on the SDK or on the deployment's version; their sections
say so.
---
import {
PyConfigureDistanceMetric as ConfigureDistanceMetric,
PySelectVectorColumn as SelectVectorColumn,
PyIndexNestedColumn as IndexNestedColumn,
PyExactVsApproximateDistances as ExactVsApproximateDistances,
PyVectorSearchPrefilter as VectorSearchPrefilter,
PyVectorSearchPostfilter as VectorSearchPostfilter,
PyMultivectorSearch as MultivectorSearch,
PySearchDistanceRange as SearchDistanceRange,
PySearchBinaryVectors as SearchBinaryVectors,
PyBatchSearch as BatchSearch,
PyFastSearch as FastSearch,
PyBruteForceSearch as BruteForceSearch,
PyBypassVectorIndex as BypassVectorIndex,
TsSearch2,
TsSelectVectorColumn,
TsIndexNestedColumn,
TsExactVsApproximate,
TsVectorSearchPrefilter,
TsVectorSearchPostfilter,
TsDistanceRange,
TsBinarySearch,
TsBatchSearch,
TsFastSearch,
TsBruteForceSearch,
TsBypassVectorIndex,
RsConfigureDistanceMetric,
RsExactVsApproximate,
RsVectorSearchPrefilter,
RsVectorSearchPostfilter,
RsSearchDistanceRange,
RsFastSearch,
RsBruteForceSearch,
RsBypassVectorIndex,
RsBatchSearch,
RsBinarySearch,
} from '/snippets/search.mdx';
Vector search is a technique used to search for similar items based on their vector representations, called embeddings. It is also known as similarity search, nearest neighbor search, or approximate nearest neighbor search.
![](/static/assets/images/search/vector-db-basics.png)
Raw data (e.g. text, images, audio, etc.) is converted into embeddings via an embedding model, which are then stored in a multimodal lakehouse like LanceDB. To perform similarity search at scale, an index is created on the stored embeddings, which can then used to perform fast lookups.
<Note>
In [SQL](/sql), vector search depends on the deployment: some versions plan
`ORDER BY distance(vector, query_vector) LIMIT k` as one -- see
[SQL](/basics/sql#no-vector-sql). The query builder on this page works with
every embedded and deployed database.
</Note>
## Supported distance metrics
Distance metrics determine how LanceDB compares vectors to find similar matches. Euclidean or `l2` is the default, and used for general-purpose similarity, `cosine` for unnormalized embeddings, `dot` for normalized embeddings (best performance), or `hamming` for binary vectors.
<Warning>
Ensure you always use the same distance metric that your embedding model was trained with. Most modern embedding models use cosine similarity, so `cosine` is often the best choice. However, if your vectors are normalized, you should use `dot` for best performance.
</Warning>
The right metric improves both search accuracy and query performance. Currently, LanceDB supports the following metrics:
| Distance metric | Mathematical form | Notes |
|---|---|---|
| `l2` | $$\|x-y\|_2= \sqrt{\sum_i (x_i-y_i)^2}$$ | The $\ell_2$ norm. Measures Euclidean distance between two points in geometric space. |
| `cosine` | $1-\frac{x\cdot y}{\|x\|_2\|y\|_2}$ | Measures directional difference between vectors. Insensitive to nonzero scaling of vectors. |
| `dot` | $x\cdot y= \underset{i}{\sum} x_i y_i$ | The standard dot product of real vectors. Sensitive to nonzero scaling of vectors; use on normalized vectors. |
| `hamming` | $\underset{i}{\sum} \,\: \mathbf{1}_{\, [x_i\neq y_i]}$ | Counts the number of bitwise-differing bits between binary vectors. |
For indexed search, supported distance metrics vary by index type:
| Index type | Supported distance metrics |
|---|---|
| `IVF_FLAT` | `["l2", "cosine", "dot", "hamming"]` |
| `IVF_PQ` | `["l2", "cosine", "dot"]` |
| `IVF_SQ` | `["l2", "cosine", "dot"]` |
| `IVF_RQ` | `["l2", "cosine", "dot"]` |
| `IVF_HNSW_FLAT` | `["l2", "cosine", "dot"]` |
| `IVF_HNSW_PQ` | `["l2", "cosine", "dot"]` |
| `IVF_HNSW_SQ` | `["l2", "cosine", "dot"]` |
### Configure Distance Metric
Without a vector index, searches use `l2` by default. You can specify `cosine` or
`dot` instead (`hamming` is supported for `IVF_FLAT` indexes only).
When a vector index exists, the search metric must match the metric used to build
the index. If you explicitly request a different metric, LanceDB cannot use that
index and falls back to an exhaustive (brute-force) search. On LanceDB Cloud, the
brute-force KNN safety limit can reject that search on a large table instead of
executing it. LanceDB Enterprise doesn't have the limit.
If you omit the search metric, current SDKs use the index metric when an index is
available. Some older SDK versions sent `l2` when the metric was omitted; with a
non-`l2` index, explicitly set the matching metric or upgrade the SDK.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{ConfigureDistanceMetric}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsSearch2}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsConfigureDistanceMetric}
</CodeBlock>
</CodeGroup>
Here you can see the same search but using `cosine` similarity instead of `l2`
distance. If the table has a vector index, make sure it was also built with
`cosine`. The result focuses on vector direction rather than absolute distance,
which works better for unnormalized embeddings.
Set `.limit(...)` on vector searches you run in applications. You can page through results with `.offset(...)` and include LanceDB's internal row id with `.with_row_id()` / `.withRowId()` when you need a stable handle for follow-up operations.
## Selecting the vector column
If your table has exactly one vector column, you can omit the column name and LanceDB will pick it for you. This works for both top-level columns (such as `vector`) and vector fields nested inside a struct (such as `image.embedding`).
When LanceDB can't infer a single column, it raises a `ValueError` (Python) or rejects the query (Node/Rust). Two cases trigger this:
- No vector column: the schema has no `fixed_size_list` or `list` of floats.
- Multiple candidates: more than one column matches the query's dimension. The error lists every candidate path so you can pick one explicitly.
To disambiguate, pass the field path with dot notation. Wrap any segment that contains characters outside `[A-Za-z0-9_]` in backticks (for example, `` `image-meta`.`embedding.v1` ``).
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{SelectVectorColumn}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsSelectVectorColumn}
</CodeBlock>
</CodeGroup>
The same field-path syntax works when creating an index on a nested vector column:
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{IndexNestedColumn}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsIndexNestedColumn}
</CodeBlock>
</CodeGroup>
When several columns share a name across structs (for example, `image.embedding` and `text.embedding`), LanceDB still picks the one whose dimension matches your query vector. If two candidates have the same dimension, you must pass the column name explicitly.
## Vector Search With ANN Index
Instead of performing an exhaustive search on the entire database for each and every query, approximate nearest neighbour (ANN) algorithms use an index to narrow down the search space, which significantly reduces query latency.
The trade-off is that the results are not guaranteed to be the true nearest neighbors of the query, but are usually "good enough" for most use cases.
Use ANN search for large-scale applications where speed matters more than perfect recall. LanceDB uses approximate nearest neighbor algorithms to deliver fast results without examining every vector in your dataset.
<Warning>
When a vector index is used, `_distance` is not always the true distance between full vectors. On quantized ANN indexes, LanceDB may compute `_distance` from the compressed representation for speed. Use `refine_factor` when you want reranking on full vectors.
</Warning>
### Exact vs Approximate Distances
When doing vector search, the meaning of "distance" depends on whether you are using an index and whether `refine_factor` is specified as part of your query.
`nprobes` controls how many partitions are searched to find candidates, `approx_mode` controls the query-time speed/recall trade-off for RQ-quantized indexes, and `refine_factor` controls how many candidates are rescored on full vectors for better distance fidelity and reranking quality.
The table below summarizes the behavior of `_distance` in search results based on your query configuration:
| Query mode | Neighbor quality | `_distance` in results |
| :--- | :--- | :--- |
| No index or `.bypass_vector_index()` | Exact kNN (100% recall) | True distance on full vectors |
| Indexed ANN, no `refine_factor` | Approximate neighbors | Distance on the index representation: exact for flat indexes, approximate for quantized indexes |
| Indexed ANN + `refine_factor(1)` | Approximate neighbors (same candidate set) | Distances recomputed on full vectors for reranked candidates |
| Indexed ANN + `refine_factor(>1)` | Better recall than no refine (usually) | Distances recomputed on full vectors for reranked candidates |
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{ExactVsApproximateDistances}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsExactVsApproximate}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsExactVsApproximate}
</CodeBlock>
</CodeGroup>
For deeper tuning guidance on indexing and performance estimation, see the [vector indexes](/indexing/vector-index) page,
For tuning `nprobes`, see below.
### Tuning `approx_mode`
Use `approx_mode` when you want to adjust the speed/recall trade-off for approximate vector search at query time. This setting currently applies only to RQ-quantized indexes, such as `IVF_RQ`; other index types ignore it.
The supported values are:
| Value | Behavior |
| :--- | :--- |
| `fast` | Prefer lower query latency, which can reduce recall. |
| `normal` | Use the default balance between query latency and recall. This only has an effect for RQ indexes built with `num_bits > 1`. |
| `accurate` | Prefer higher recall, which can increase query latency. |
Only the Rust SDK sets `approx_mode`; the Python and TypeScript SDKs do not expose it. You can change `approx_mode` per query without rebuilding the index. For RQ indexes built with `num_bits=1`, `normal` uses the same one-bit scoring path as `fast`. If you also set `refine_factor`, LanceDB first uses `approx_mode` while finding candidates, then reranks the selected candidates on full vectors.
### Tuning `nprobes`
- `nprobes` controls how many partitions are searched at query time.
- `nprobes` improves candidate recall, but does not by itself make `_distance` exact.
- By default, LanceDB automatically tunes `nprobes` to achieve the best performance without noticeably sacrificing accuracy.
- In most cases, leave `nprobes` unset and use the auto-tuned value.
- Only tune `nprobes` manually when recall is below your target, or when you need even higher performance for your workload.
- If recall is too low, increase `nprobes` gradually, but after a certain threshold, increasing `nprobes` yields only marginal accuracy gains.
- If you need higher performance and have recall headroom, decrease `nprobes` gradually.
For filtered ANN searches, you can also set `minimum_nprobes` and `maximum_nprobes`. LanceDB starts
with the minimum and can scan more partitions up to the maximum if the filter leaves too few
candidates. Calling `nprobes(n)` fixes both values to `n`, which disables that adaptive behavior.
Whether a deployment honors this range depends on its version; one that does not searches exactly
`minimum_nprobes` partitions.
### Vector Search with Prefiltering
This is the default vector search setting. You can use prefiltering to boost query performance by reducing the search space before vector calculations begin. The system first applies your filter criteria to the dataset, then conducts vector search operations only on the remaining relevant subset.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{VectorSearchPrefilter}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsVectorSearchPrefilter}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsVectorSearchPrefilter}
</CodeBlock>
</CodeGroup>
This filters out rows where label ≤ 2 before doing vector search, then picks specific columns from the top 5 matches.
The `.where("label > 2")` applies a filter before vector search, `.select(["text", "keywords", "label"])` chooses specific columns to return, and `.limit(5)` restricts results to the top `5` most similar vectors.
As a result, you'll see a result with just the data you want from the most similar vectors.
### Vector Search with Postfiltering
Use postfiltering to prioritize vector similarity by searching the full dataset first, then applying metadata filters to the top results. This approach ensures you get the most similar vectors before filtering, which can be crucial when similarity is more important than metadata constraints.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{VectorSearchPostfilter}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsVectorSearchPostfilter}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsVectorSearchPostfilter}
</CodeBlock>
</CodeGroup>
Here you can see how to do vector search first to get the most similar vectors, then filter by label > 1 on those results.
The `prefilter=False` parameter tells LanceDB to apply the filter after vector search instead of before, `.where("label > 1")` filters the top results by metadata, and `.select()` chooses which columns to include.
In the end, you receive a query result with the best matches that also meet your metadata requirements.
<Card icon="book">
[Post-filtering](/search/filtering/#post-filtering-with-vector-search) in LanceDB applies
the filter condition after obtaining the nearest neighbors based on vector similarity.
</Card>
## Multivector Search
Use multivector search when your documents contain multiple embeddings and you need sophisticated matching between query and document vector pairs. The late interaction approach finds the most relevant combinations across all available embeddings and provides nuanced similarity scoring.
Only `cosine` similarity is supported as the distance metric for multivector search operations.
Every query vector must match the inner dimension of the multivector column; LanceDB rejects mismatched query dimensions rather than guessing how to reshape them.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{MultivectorSearch}
</CodeBlock>
</CodeGroup>
Here you can see how to take 2 query vectors and find the best matching pairs between them and document vectors using late interaction. The `np.random.random(size=(2, 256))` creates a 2×256 array with two random query vectors, `.limit(5)` returns the top 5 best document-query combinations, and `.to_pandas()` provides results in a DataFrame format.
**Read more:** [Multivector search](/search/vector-search)
## Advanced Search Scenarios
### Search With Distance Range
Use `distance_range` search when you need vectors within particular similarity bounds rather than just the closest neighbors. The system filters results to only include vectors that fall within your specified distance thresholds from the query.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{SearchDistanceRange}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsDistanceRange}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsSearchDistanceRange}
</CodeBlock>
</CodeGroup>
This shows three ways to search within distance ranges: bounded, upper bound only, and lower bound only.
The `distance_range()` method filters results by similarity thresholds - the first example finds vectors with distance between `0.1` and `0.5`, the second finds vectors closer than `0.5`, and the third finds vectors farther than `0.1`.
Each approach returns Arrow tables with vectors that fall within your specified distance thresholds.
### Search With Binary Vectors
Use binary vector search for scenarios involving binary embeddings, such as those produced by hashing algorithms. The system stores these efficiently as packed uint8 arrays and uses Hamming distance calculations to determine vector similarity.
<Tip>
The number of dimensions of the binary vector must be a multiple of 8. A vector of dimensionality 128 will be stored as a `uint8` array of size 16.
</Tip>
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{SearchBinaryVectors}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsBinarySearch}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsBinarySearch}
</CodeBlock>
</CodeGroup>
Here you can see how to set up a table for binary vectors, pack them efficiently into bytes, and search using Hamming distance.
The schema defines a 32-byte vector field (256 bits ÷ 8), `np.random.randint(0, 2, size=256)` creates binary vectors, `np.packbits()` compresses them to bytes, and `.distance_type("hamming")` specifies `hamming` distance for similarity calculation.
The search produces an Arrow table with binary vectors ranked by how many bits differ from the query.
## Scaling Vector Search
### Batch Search
Use batch search to handle multiple query vectors simultaneously. This gives you significant efficiency gains over individual queries. LanceDB processes all vectors in parallel and organizes results with a `query_index` field that maps each result set back to its originating query.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{BatchSearch}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsBatchSearch}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsBatchSearch}
</CodeBlock>
</CodeGroup>
This takes 5 query embeddings and finds the top 5 matches for each one in a single batch operation.
The `load_dataset()` loads embeddings from a Hugging Face dataset, `query_embeds` contains `5` query vectors, and `.search(query_embeds)` processes all queries simultaneously.
The final query result contains all results, including a `query_index` to tell you which query each result came from.
<Tip>
When processing batch queries, the results include a `query_index` field
to explicitly associate each result set with its corresponding query in
the input batch.
</Tip>
### Search With Asynchronous Indexing
To optimize for speed over completeness, enable the `fast_search` flag in your query to skip searching unindexed data.
While vector indexing occurs asynchronously, newly added vectors are immediately
searchable through a fallback brute-force search mechanism. This ensures zero
latency between data insertion and searchability, though it may temporarily
increase query response times.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{FastSearch}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsFastSearch}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsFastSearch}
</CodeBlock>
</CodeGroup>
Here you can see how to turn on fast search mode to skip unindexed vectors and only look through indexed data for speed.
The `fast_search=True` parameter tells LanceDB to only search indexed vectors, skipping any recently added data that hasn't been indexed yet.
You'll obtain a query result with the top `5` matches from indexed vectors, but might miss data that was just added.
## Brute Force Search
### Search With No Index
The simplest way to perform vector search is to perform a brute force search, without an index, where the distance between the query vector and all the vectors in the database are computed, with the top-k closest vectors returned.
This is equivalent to a k-nearest neighbours (kNN) search in vector space.
Choose brute force search when you need guaranteed 100% recall, typically with smaller datasets where query speed isn't the primary concern. The system scans every vector in the table and calculates precise distances to find the exact nearest neighbors.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{BruteForceSearch}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsBruteForceSearch}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsBruteForceSearch}
</CodeBlock>
</CodeGroup>
This carries out a brute force search through every vector in the table to find the 3 closest matches to a random 1536-dimensional query. You'll get back a list of the most similar vectors with exact distances.
![](/static/assets/images/search/knn_search.png)
As you can imagine, the brute force approach is not scalable for datasets larger than a few hundred thousand vectors, as the latency of the search grows linearly with the size of the dataset. This is where approximate nearest neighbour (ANN) algorithms come in.
### Bypass the Vector Index
Use `bypass_vector_index` to get exact, ground-truth results by performing exhaustive searches across all vectors. Instead of relying on approximate methods, the system directly compares your query against every vector in the table, ensuring 100% recall at the cost of increased query time.
<CodeGroup>
<CodeBlock filename="Python" language="python" icon="python">
{BypassVectorIndex}
</CodeBlock>
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
{TsBypassVectorIndex}
</CodeBlock>
<CodeBlock filename="Rust" language="rust" icon="rust">
{RsBypassVectorIndex}
</CodeBlock>
</CodeGroup>
This skips the approximate index and checks every single vector for exact, ground-truth results.
The `.bypass_vector_index()` method forces LanceDB to perform an exhaustive search through all vectors instead of using the approximate nearest neighbor index, ensuring exact results but at the cost of slower performance.
The output is a query result with the top 5 exact matches, guaranteeing 100% recall but taking longer to run.
This approach is particularly useful when:
- Evaluating ANN index quality
- Calculating recall metrics to tune index parameters
- Ensuring exact results for critical applications
## Multi-vector search {#multivector}
LanceDB's multivector support enables you to store and search multiple vector embeddings for a single item.
This capability is particularly valuable when working with late-interaction models like ColBERT and ColPaLi, which generate multiple embeddings per document.
In this tutorial, you'll create a table with multiple vector embeddings per document and learn how to perform multivector search. For more end-to-end examples, see the [VectorDB recipes repository](https://github.com/lancedb/vectordb-recipes/tree/main/examples).
### Multivector Support
Each item in your dataset can have a column containing multiple vectors, which LanceDB can efficiently index and search. When performing a search, you can query with either a single vector embedding or multiple vector embeddings.
<Warning>
Currently, only the `cosine` metric is supported for multivector search. The vector value type can be `float16`, `float32`, or `float64`.
</Warning>
Each query vector must match the inner vector dimension in the multivector column. This applies to both single-vector queries and multi-vector query matrices.
### Computing Similarity
MaxSim (Maximum Similarity) is a key concept in late-interaction models that:
- Computes the maximum similarity between each query embedding and all document embeddings
- Sums these maximum similarities to get the final relevance score
- Effectively captures fine-grained semantic matches between query and document tokens
The MaxSim calculation can be expressed as:
$$
\text{MaxSim}(Q, D) = \sum_{i=1}^{|Q|} \max_{j=1}^{|D|} \text{sim}(q_i, d_j)
$$
Where $sim$ is the similarity function (e.g., cosine similarity).
$$
Q = \{q_1, q_2, ..., q_{|Q|}\}
$$
$Q$ represents the query embeddings, and $D = \{d_1, d_2, ..., d_{|D|}\}$ represents the document embeddings.
### Using Multivector Search
#### 1. Setup
Connect to LanceDB and import the required libraries.
<CodeGroup>
```python Python icon="python"
import lancedb
import numpy as np
import pyarrow as pa
db = lancedb.connect(
uri="db://your-project-slug",
api_key="your-api-key",
region="your-region"
)
```
</CodeGroup>
#### 2. Define Schema
Define a schema that specifies a multivector field. A multivector field is a nested list structure in which each document contains multiple vectors. In this case, we'll create a schema with:
1. An ID field as an integer (int64)
2. A vector field that is a list of lists of float32 values
- The outer list represents multiple vectors per document
- Each inner list is a 256-dimensional vector
- Using float32 for memory efficiency while maintaining precision
<CodeGroup>
```python Python icon="python"
db = lancedb.connect("data/multivector_demo")
schema = pa.schema(
[
pa.field("id", pa.int64()),
# float16, float32, and float64 are supported
pa.field("vector", pa.list_(pa.list_(pa.float32(), 256))),
]
)
```
</CodeGroup>
#### 3. Generate Multivectors
Generate sample data where each document contains multiple vector embeddings, which can represent different aspects or views of the same document.
In this example, we create **1024 documents** where each document has **2 random vectors** of **dimension 256**, simulating a real-world scenario where you might have multiple embeddings per item.
<CodeGroup>
```python Python icon="python"
data = [
{
"id": i,
"vector": np.random.random(size=(2, 256)).tolist(), # Each document has 2 vectors
}
for i in range(1024)
]
```
</CodeGroup>
#### 4. Create a Table
Create a table with the defined schema and sample data, which will store multiple vectors per document for similarity search.
<CodeGroup>
```python Python icon="python"
tbl = db.create_table("multivector_example", data=data, schema=schema)
```
</CodeGroup>
#### 5. Build an Index
Only cosine similarity is supported for multivector search operations.
For faster search, build the standard `IVF_PQ` index over your vectors:
<CodeGroup>
```python Python icon="python"
tbl.create_index(metric="cosine", vector_column_name="vector")
```
</CodeGroup>
<Info>
**Indexing matters more for multivector tables than for single-vector ones.**
A brute-force scan over a multivector column has to compare every query vector to every document vector in every row, so the cost grows with both the row count and the number of vectors per row.
In LanceDB OSS, the query will just run, so a large unindexed multivector table can stall a process for a long time before returning results.
On LanceDB Cloud, the brute-force KNN safety check applies a stricter row threshold to multivector columns — roughly 10× lower than for single-vector columns. So an unindexed multivector table can be rejected with a "vector search would use brute-force KNN" error well before a comparable single-vector table would. LanceDB Enterprise doesn't have the limit. Build the index before you start hitting it from production traffic, even if the dataset is small enough that you'd skip indexing for a single-vector workload.
</Info>
#### 6. Query a Single Vector
When searching with a single query vector, it will be compared against all vectors in each document, and the similarity scores will be aggregated to find the most relevant documents.
<CodeGroup>
```python Python icon="python"
query = np.random.random(256)
results_single = tbl.search(query).limit(5).to_pandas()
```
</CodeGroup>
#### 7. Query Multiple Vectors
With multiple query vectors, LanceDB calculates similarity using late interaction, a late-interaction technique that computes relevance by finding the best-matching pairs between query and document vectors. This approach provides more nuanced matching while maintaining fast retrieval speeds.
<CodeGroup>
```python Python icon="python"
query_multi = np.random.random(size=(2, 256))
results_multi = tbl.search(query_multi).limit(5).to_pandas()
```
</CodeGroup>
Visit the [Hugging Face embedding integration](/integrations/embedding/huggingface/) page for information on embedding models.
### Simple Example: ColBERT Embeddings
[ColBERT](https://arxiv.org/abs/2004.12832) is the most well-known late-interaction retrieval model that
represents each document and query as multiple token embeddings and scores matches by taking the best
token-to-token similarities (MaxSim) across them.
Install the dependencies before running this example:
```bash
pip install pylate lancedb pandas
```
<CodeGroup>
```python Python icon="python"
import numpy as np
import pyarrow as pa
import lancedb
from pylate import models
## 1) Load a late-interaction model via PyLate
## PyLate docs show ColBERT() + encode(..., is_query=...) :contentReference[oaicite:2]{index=2}
model = models.ColBERT(model_name_or_path="lightonai/GTE-ModernColBERT-v1")
## You can discover dim from one embedding (avoid guessing)
dim = model.encode(["hello"], is_query=True)[0].shape[1]
## 2) Create a LanceDB table with a multivector column
db = lancedb.connect("./pylate_lancedb")
schema = pa.schema([
pa.field("doc_id", pa.string()),
pa.field("text", pa.string()),
# multivector: list<list<float32, dim>> :contentReference[oaicite:3]{index=3}
pa.field("mv", pa.list_(pa.list_(pa.float32(), dim))),
])
docs = [
{"doc_id": "1", "text": "The train to Tokyo leaves at 5pm."},
{"doc_id": "2", "text": "That Pho restaurant in Hanoi is highly rated."},
{"doc_id": "3", "text": "This is a noodle bar in Osaka, Japan."},
]
## 3) Encode documents with PyLate (token vectors per doc)
doc_texts = [d["text"] for d in docs]
doc_embs = model.encode(doc_texts, is_query=False) # list/array of (T, dim) per doc :contentReference[oaicite:4]{index=4}
rows = []
for d, emb in zip(docs, doc_embs):
emb = np.asarray(emb, dtype=np.float32)
rows.append({**d, "mv": emb.tolist()})
tbl = db.create_table("docs", data=rows, schema=schema, mode="overwrite")
## 4) Build an index + query using a query matrix.
## Multivector brute-force scales with rows × vectors-per-row, so build the index
## at much smaller dataset sizes than you would for single-vector search — and
## always before exposing the table to remote traffic.
tbl.create_index(vector_column_name="mv", metric="cosine")
query = "Tell me about ramen in Japan"
q_emb = np.asarray(model.encode([query], is_query=True)[0], dtype=np.float32) # (Tq, dim) :contentReference[oaicite:5]{index=5}
out = tbl.search(q_emb).limit(5).to_pandas() # multivector search accepts a matrix :contentReference[oaicite:6]{index=6}
print(out[["doc_id", "text"]])
```
</CodeGroup>
Late-interaction model implementations evolve rapidly, so it's a good idea to check the latest popular models
when trying multivector search.
### Advanced Example: XTR Embeddings
[ConteXtualized Token Retriever (XTR)](https://arxiv.org/abs/2304.01982) is a late-interaction retrieval model that represents text as token-level vectors instead of a single embedding.
This lets search score token-to-token matches (MaxSim), which can improve fine-grained relevance.
The notebook linked below shows how to integrate XTR, which prioritizes critical document
tokens during the initial retrieval stage and removes the gathering stage to improve performance significantly.
By focusing on the most semantically salient tokens early in the process, XTR reduces computational complexity
while improving recall and ensuring rapid identification of candidate documents.
<Card
icon="book"
title="Multivector search with ColPali + XTR embeddings and LanceDB"
href="https://colab.research.google.com/github/lancedb/vectordb-recipes/blob/main/examples/multivector_xtr/main.ipynb"
>
</Card>