Source: https://docs.any.org/search/embedders.html
# Embedders
An embedder turns index documents and queries into vectors for the [semantic leg](https://docs.any.org/search/vector.md.txt). `index.embedder` picks one; the default needs no configuration at all, and no embedder can ever break indexing or full-text search.
## The options
| `index.embedder` | What runs | Needs |
| --- | --- | --- |
| `auto` (default) | the local model alone until `index.openai.apiKey` is set; with a key, an online OpenAI-compatible API as primary and the local model as fallback — same model both ways, and indexed text and search queries go to the online API | nothing in a build with the local embedder (its model auto-downloads); without one, a key, or the index is full-text only |
| `local` | llama.cpp in a child process of the server, no external service | a build with the local embedder (`make build`, a release tarball, or `go install -tags llamacpp`); the llama.cpp shared libraries next to the binary; a system `libffi` on Linux |
| `ollama` | a local Ollama server's `/api/embed` | Ollama running (default `http://localhost:11434`, model `embeddinggemma`) |
| `openai` | any OpenAI-compatible `/embeddings` endpoint | `index.openai.{baseUrl, model, apiKey}` |
| `none` | no embedder — the index is full-text only | — |
> **Note.** An embedder reads the text it embeds. `openai`, and `auto` once it has a key, send the text of every indexed document and every search query to `index.openai.baseUrl`; `ollama` sends them to the Ollama server. `local` keeps them on the device.
```yaml
index:
embedder: local
```
```bash
ANY_INDEX_EMBEDDER=none any run # FTS-only server
```
## `local` — llama.cpp in a child process
The default model is **Qwen3-Embedding-0.6B (Q8\_0)**: 1024-dimensional, last-token pooling, L2-normalized vectors; queries carry the Qwen retrieval instruction, documents embed bare. The GGUF (639 MB, sha256-pinned) downloads into `/models/` on first boot — resumable, with progress in the server log and as the `index.model_download` [process](https://docs.any.org/notifications/processes.md.txt) — and never blocks boot. Until it lands the embedder reports unavailable and full-text keeps answering.
Decoding never runs inside the server. On the first embed call the server re-executes its own binary as a hidden `any run embedder` child and talks to it over stdin/stdout; one child serves every space. The bindings are pure Go (no CGO); the child loads the prebuilt llama.cpp shared libraries at runtime from `index.local.libDir`, default `llamacpp/` next to the binary. `make build` fetches them; the release tarballs ship them. Nothing is loaded into memory until that first call (~640 MB mmap + ~200 MB context once it is).
The child exists so a llama.cpp fault costs a round of embedding, not the server: a GPU device loss or an internal assertion kills only the child, the batch's documents stay `pending`, and the next round respawns it (with backoff). A frame that outlives `requestTimeout` counts as the same fault — a wedged GPU stops answering rather than failing. A search query takes the child ahead of queued document batches, so a busy re-index delays it by at most the decode in flight, and the child runs at lower scheduling priority (`niceness`) so a full re-index yields to interactive work.
| Key | Default | Purpose |
| --- | --- | --- |
| `local.modelPath` | "" | an existing GGUF; set it and no download is attempted (air-gapped) |
| `local.modelUrl` / `modelSha256` | "" | override the download source / checksum |
| `local.libDir` | `/llamacpp` | where the shared libraries live |
| `local.contextSize` | 2048 | truncation bound in tokens, shared by the texts of one decode |
| `local.queryPrefix` | "" | "" = the Qwen retrieval instruction |
| `local.dim` | 0 | Matryoshka truncation; 0 = the model's 1024 |
| `local.threads` | 0 | the child's CPU budget; 0 = CPU count − 1 |
| `local.gpuLayers` | absent | absent = offload all when a GPU backend is usable; 0 = force CPU |
| `local.batchDocs` | 1 | documents packed per decode; each then gets `contextSize / batchDocs` tokens, so a wider batch embeds less of each text |
| `local.requestTimeout` | `3m` | bound on one request to the child before it is killed and respawned |
| `local.niceness` | 10 | the child's scheduling priority below the server; 0 = same as the server |
**GPU offload** is automatic: the bundles carry Metal (macOS arm64) and Vulkan (Linux, Windows — NVIDIA/AMD/Intel) backends alongside the CPU variants, and a backend whose driver is missing simply does not register. Full offload of the default model takes ~2 GB of VRAM; set `gpuLayers: 0` if the embedder should not have it. CUDA/ROCm builds are not bundled — point `libDir` at your own llama.cpp build to use them.
Platform notes: only builds with `-tags llamacpp` carry the local embedder; without it `local` fails at boot ([Builds and CI](https://docs.any.org/operations/builds-and-ci.md.txt)). Linux needs a loadable system `libffi.so.8` (on NixOS use the repo's `nix develop` shell); macOS bundles it, and the `-sandbox` release variants load the system one instead so they work inside an App-Sandboxed host. Mobile builds have full-text search and force vector search off; they never construct an embedder, even when the configuration names one.
## `auto` — online primary, local fallback
No provider ships with the binary, so a fresh install's `auto` is the local model and nothing leaves the device. Point `index.openai.baseUrl` at any OpenAI-compatible `/embeddings` host and set `index.openai.apiKey` (`ANY_INDEX_OPENAI_BASE_URL` / `ANY_INDEX_OPENAI_API_KEY`), and `auto` prefers the online API for speed and falls back to the local model during an outage through a circuit breaker (repeated failures skip the primary for a cooldown, then re-probe), so vector search stays fresh instead of pausing. A semantic query gives the primary half of the remaining timeout budget so the local fallback still has time to answer.
> **Note.** Both sides must be the **same embedding model** — the index holds one vector space and one dimension, and mixing models yields incoherent similarity. The supported pairing is one model served two ways: the local Qwen3-Embedding-0.6B and a host that serves the same model. `index.openai.model` defaults to that model's common name, `Qwen/Qwen3-Embedding-0.6B`; override it only when your provider spells the same model differently. A provider serving a different model is not a fallback pair but a second, incompatible vector space. fp16-versus-Q8 drift is negligible.
## `ollama` and `openai`
```yaml
index:
embedder: ollama
ollama:
url: http://localhost:11434
model: embeddinggemma
```
```yaml
index:
embedder: openai
openai:
baseUrl: https://api.openai.com/v1
model: text-embedding-3-small
apiKey: sk-… # sent as Bearer, never logged
```
`index.embedConcurrency` embeds several batches in parallel — the throughput win for online APIs (default 4 for `openai` and for `auto` with a key, 1 for `local`, which serializes internally anyway). `index.embedBatch` (default 64) is the documents per request.
## Outage semantics
There is no boot-time probe. Whenever an embedder is *configured*, text-bearing documents are marked `pending` regardless of reachability, so:
- an outage — at boot or mid-run — freezes only the vector side; full-text indexes and answers normally;
- when the embedder returns, the next embed round (nudged by a page, or the 1-minute retry tick) drains the queue automatically;
- `hybrid` queries degrade to full-text with `vectorStatus: "unavailable"`; `vector` queries answer `503 index.embedder_unavailable`;
- the vector dimension is learned from the first successful batch (or `index.vector.dim`) and pinned in the index database — a later dimension change against a populated index is a loud boot error advising `rm /index`, never silent corruption. A model change is a rebuild for the same reason — never mix vectors from two models.
"Model still downloading" and "GPU died mid-run" both ride the same path: affected documents stay `pending`. A child that crashes with GPU offload active also moves the server to CPU decoding for the rest of the run; the next start tries the GPU again, so a driver fix recovers on its own.
To skip the download, point `index.local.modelPath` at an existing compatible GGUF; to run with no model at all, `index.embedder: none` keeps full-text search working.
> **Why it matters.** The embedder is the only component of search that might live outside the process. Making it optional, swappable and outage-tolerant keeps the encrypted data searchable on every device — including one that never installs a model or never goes online.