SurfSense/docs/adr/0037-embedding-is-a-type-not-a-slot.md
Anish Sarkar 84c7ffad18 feat(local): choose the embedding model in onboarding
The embedding model is now chosen in the last onboarding step and fixed
for the whole library. Skipping keeps bge-small, which ships with the
app and can't be deleted. Changing the model later is not built.

- An embedding_indexes row records the model's spec and its vector
  table, and each document records which index embedded it. Migration
  0021 stamps existing libraries with bge-small. Routes that queue
  embedding work return 409 embedding_not_chosen until a model is
  chosen.
- Ingest, Studio and search embed through the active index; the old
  worker embedding module is removed. Workers wait for the schema, and
  documents interrupted when the app closed are marked failed.
- Curated embedders (bge-small, granite-97m, granite-311m) live in the
  local manifest, run by a new onnxruntime engine. EMBEDDING is a
  catalog type, never a selection slot.
- Hugging Face search finds ONNX embedders. Each download is checked: a
  probe sets its width, and a ten-question search check must pass.
  Small tokenizer.json files that Hugging Face lists without a checksum
  are hashed at the pinned commit. Results use the GGUF search's shapes.
- Onboarding renders the embedding step with the same component and
  shared list as the other steps; only its choice hook differs. Use
  marks the choice and Finish sends it. Bundled models have no Delete.
- Settings has a read-only Embedding section.
- ADRs 0036 and 0037, a new embedding architecture doc, and updates to
  the docs it touches. New strings are translated into every language.
2026-10-02 01:45:10 +05:30

2.2 KiB

ADR 0037: Curated embedders are a fourth engine in the local catalog, and embedding is a type, never a slot

Context

Onboarding offers a short list of embedders to download (ADR 0036). Chat, image and audio models are already listed, pinned, licence-checked, downloaded and deleted by the local catalog. A second catalog for embedders would repeat all of it. But every model type the catalog knows is also a selection slot (selection), changed freely, and the embedder must not be: every vector in the library depends on it.

Decision

  • One manifest. Curated embedders live in the local manifest, run by a fourth engine, onnxruntime, whose embedding block holds what the index's spec needs and no file states, including the ranking weight the retrieval eval measured.
  • EMBEDDING is a catalog type and never a slot. The classifier's embedder group answers it, so the manifest and the engine can describe one. SLOTS leaves it out: selectable_for never offers it, not even to a model nothing recognises, and the selection routes refuse it. The existing selected_models check lists the five slot types, so the database refuses it too.
  • A converted build must match the original. A build pinned from someone else's conversion records its cosine to the original model's vectors, and the schema refuses one under 0.99 mean or 0.98 minimum. granite-97m's int8 build measured 0.961 and is not listed; its full-precision build is.
  • Rejected: a separate manifest for embedders. It would keep ModelType purely a slot, at the price of a second refresh script, pinning and licence pipeline, and install and delete path.

Consequences

  • An embedder is downloaded under its catalog id, not its weights' file name: every ONNX repo calls its weights model.onnx.
  • Downloads land in embeddings/ under the data directory, not the models pack, which a packaged app ships read-only.
  • The delete route refuses the embedder the active index names, as it refuses a model the app ships.