# Vector and hybrid search

> Store embeddings with your documents and find the nearest, alone or narrowed by a text match and metadata filters.

Give documents a vector (an embedding) and SkimmerDB finds the nearest ones to a query
vector. Make the embeddings yourself, as on this page, or let SkimmerDB make them from the text
with your own OpenAI, Voyage, Gemini or Cohere key, or hosted: see [Embeddings](/guides/embeddings). A text match and metadata filters can narrow the candidates first (hybrid search):
the answer is the nearest among the documents that pass. One copy of the data serves text,
vectors and SQL, and vectors are as durable and as quick to become searchable as text.

## Create a vector namespace

A namespace's vector settings are fixed for its life: `dims` (1 to 2,048, your embeddings'
length) and `metric`, `cosine` (the default; vectors are stored normalised) or `euclidean`.

```sh title="curl"
B=https://api.skimmerdb.com/v1
curl -s $B/namespaces -H "Authorization: Bearer $KEY" \
  -d '{"name": "notes", "vector": {"dims": 768, "metric": "cosine"}}'
```

```python title="Python"
notes = db.create_namespace("notes", dims=768, exist_ok=True)
```

```ts title="TypeScript"
const notes = await db.createNamespace("notes", { dims: 768, existOk: true });
```

You can also skip this step: the first write of documents with vectors to a new name makes a
namespace with vectors of the first vector's length and the cosine metric.

> **Note** One embedding model per namespace. Switching models means a new namespace, written
> alongside the old one until it has caught up.

## Write vectors

Put `"vector": [...]` in each document, or `"vector_b64"`: base64 of little-endian float32s,
smaller than JSON numbers. `text` may then be left out; a vector is optional per document.

```sh title="curl"
curl -s $B/namespaces/notes/docs -H "Authorization: Bearer $KEY" -d '{"docs": [
  {"id": "n-17", "text": "Webhook timeouts on the api team", "meta": {"team": "api"}, "vector": [0.12, -0.03, ...]}
]}'
```

```python title="Python"
notes.write([{"id": "n-17", "text": "Webhook timeouts on the api team", "meta": {"team": "api"}, "vector": embedding}])
```

```ts title="TypeScript"
await notes.write([{ id: "n-17", text: "Webhook timeouts on the api team", meta: { team: "api" }, vector: embedding }]);
```

- A vector of the wrong length, or a vector written to a namespace without vectors, is a
  `422`.
- `GET .../docs/{id}` returns the vector. A `PATCH` keeps it unless it gives a new one.

## Search by vector

`POST /v1/namespaces/{name}/search` with `"vector"` (or `"vector_b64"`):

```sh title="curl"
curl -s $B/namespaces/notes/search -H "Authorization: Bearer $KEY" \
  -d '{"vector": [0.12, -0.03, ...], "limit": 10}'
```

```python title="Python"
res = notes.search(vector=query_embedding, limit=10)
for hit in res.hits:
    print(hit.id, hit.score)
```

```ts title="TypeScript"
const res = await notes.search("", { vector: queryEmbedding, limit: 10 });
for (const hit of res.hits) console.log(hit.id, hit.score);
```

The answer:

```json title="Answer"
{"namespace": "notes", "hits": [{"id": "n-17", "score": 0.912, "meta": {"team": "api"}, "written_at": "..."}],
 "docs": 1344643, "took_ms": 0.73, "scanned": 15232, "reranked": 64, "partial": false,
 "temperature": "warm", "store_gets": 0, "store_bytes": 0}
```

- Hits come nearest first. `score` is the cosine similarity (cosine namespaces) or the
  euclidean distance (euclidean ones). Ties are broken by id.
- `limit` is 10 by default, at most 1,000.
- `scanned` and `reranked` say how many candidates were looked at and reranked.
- Answers carry `cpu_ms` and `timed_out` as text searches do, and the same
  [time limits](/guides/search#time-limits-and-partial-answers).

## Hybrid: vectors with text and filters

Add `q` (with `regex` if you like) and `filter` to a vector query. Both narrow the candidates
before ranking: the answer is the nearest among the documents that contain the text and pass
the filter.

```sh title="curl"
curl -s $B/namespaces/notes/search -H "Authorization: Bearer $KEY" \
  -d '{"vector": [0.12, -0.03, ...], "q": "timeout", "filter": {"team": "api"}, "limit": 10}'
```

```python title="Python"
res = notes.search("timeout", vector=query_embedding, filter={"team": "api"}, limit=10)
```

```ts title="TypeScript"
const res = await notes.search("timeout", { vector: queryEmbedding, filter: { team: "api" }, limit: 10 });
```

This is the combination agents usually want: meaning from the vector, exactness from the text
(a name, an error code, an id), and scope from the filter (a user, a team, a time window).

- A hybrid query takes the text's candidates from the index, without reading documents, and
  ranks them; only the nearest are checked for the text.
- A text that most documents hold is checked on the nearest vectors instead.
- A top-`limit` query returns `limit` hits whenever that many documents qualify; otherwise the
  answer says `"partial": true`.

## Accuracy

The defaults give recall@10 of about 0.97 on 1.3M 768-dimension vectors, in under a
millisecond warm. Three knobs trade speed for accuracy:

| Option | Default | What it does |
|---|---|---|
| `nprobe` | 48 | Clusters probed in the biggest index (smaller indexes are scaled to it). More: better recall, slower. |
| `rerank` | 64 | Candidates reranked with fuller-precision vectors. |
| `full` | `false` | `true` reranks with the vectors as written (float32) rather than their int8 copies. |

```python title="Python"
res = notes.search(vector=query_embedding, limit=10, nprobe=96, rerank=128)
```

```ts title="TypeScript"
const res = await notes.search("", { vector: queryEmbedding, limit: 10, nprobe: 96, rerank: 128 });
```

## How it works

- **Recent writes** are searched exactly, by brute force.
- **Compaction** builds an IVF index per segment: k-means centroids over RaBitQ 1-bit codes,
  with int8 rows for reranking. The float32 vectors as written are kept in object storage for
  `full` reranks.
- **Cold queries** read the namespace's manifest and a small map of its indexes side by side,
  then the clusters a query probes (with their documents' ids and metadata) and the newest
  writes side by side: two rounds to object storage, without downloading the rest of the
  namespace. A cold query with a filter or text opens the namespace instead.

## Next

- [Search and filters](/guides/search) for the text and filter syntax.
- [Agent memory](/guides/agent-memory): a recipe that uses hybrid search.
- [Performance and cost](/guides/performance).
