Search
Embedders
An embedder turns index documents and queries into vectors for the semantic leg. 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) |
an online OpenAI-compatible API as primary, the in-process local model as fallback — same model both ways | nothing; the local model auto-downloads regardless |
local |
llama.cpp in-process, no external service | 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 | — |
index:
embedder: local
ANY_INDEX_EMBEDDER=none any run # FTS-only server
local — in-process llama.cpp
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 <data-dir>/models/ on first boot — resumable, with progress in the server log and as the index.model_download process — and never blocks boot. Until it lands the embedder reports unavailable and full-text keeps answering.
The bindings are pure Go (no CGO); they load 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 the first embed call (~640 MB mmap + ~200 MB context once it is).
| 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 |
<exe-dir>/llamacpp |
where the shared libraries live |
local.contextSize |
2048 | truncation bound in tokens |
local.queryPrefix |
"" | "" = the Qwen retrieval instruction |
local.dim |
0 | Matryoshka truncation; 0 = the model's 1024 |
local.threads |
0 | 0 = CPU count − 1 |
local.gpuLayers |
absent | absent = offload all when a GPU backend is usable; 0 = force CPU |
local.batchDocs |
16 | documents packed per decode |
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: 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 (see Builds and CI). Mobile builds never construct an embedder.
auto — online primary, local fallback
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.
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:
index.openai.model: Qwen/Qwen3-Embedding-0.6Bon a host that serves it, with the default local Qwen3-Embedding-0.6B. fp16-versus-Q8 drift is negligible. The packagedopenaidefaults are shared development credentials, marked temporary.
ollama and openai
index:
embedder: ollama
ollama:
url: http://localhost:11434
model: embeddinggemma
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/auto, 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;
hybridqueries degrade to full-text withvectorStatus: "unavailable";vectorqueries answer503 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 advisingrm <data-dir>/index, never silent corruption.
"Model still downloading" and "GPU died mid-run" both ride the same path: affected documents stay pending, and a restart re-selects backends cleanly.
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.