Files
yvgude--lean-ctx/docs/guides/custom-embeddings.md
T
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

5.0 KiB
Raw Blame History

Custom Embedding Models (hf:org/repo)

ctx_semantic_search ships three built-in ONNX models — but every team's corpus is different. Since GL #397 you can point lean-ctx at any HuggingFace repo with an ONNX export, fully local, no API keys:

# config.toml
[embedding]
model = "hf:intfloat/multilingual-e5-small@ffdcc22a9a5c973258343b0001a4d483cbd45be9"

Or per-shell:

export LEAN_CTX_EMBEDDING_MODEL="hf:intfloat/multilingual-e5-small@ffdcc22..."

The env var always wins over config.toml. The first semantic search after a model switch triggers a one-shot re-index (the vector index stores the model id and detects the mismatch automatically — no manual steps).

What the repo must contain

File Purpose
onnx/model.onnx The ONNX export of the encoder
tokenizer.json HuggingFace fast-tokenizer config (WordPiece/BPE)

Most sentence-transformers-compatible repos already publish both (look for the onnx/ folder on the model page). If yours doesn't, export it once:

pip install optimum[onnxruntime]
optimum-cli export onnx --model org/your-model --task feature-extraction ./out
# upload ./out/model.onnx as onnx/model.onnx + the generated tokenizer.json

Syntax

hf:<org>/<repo>[@<revision>]
  • revision may be a tag, branch, or commit SHA. Pin a revision. Without a pin lean-ctx resolves main and logs a warning — an upstream force-push could otherwise change your embeddings silently.
  • Optional [embedding].dimensions declares the vector width as a fallback. You can usually omit it: lean-ctx probes the real width from the ONNX graph at load time.
[embedding]
model = "hf:org/repo@v1.2"
dimensions = 1024   # optional fallback, probed value wins

Static embeddings: model2vec (GL #452)

Besides classic transformer encoders, lean-ctx drives model2vec static-embedding exports — ONNX graphs with an EmbeddingBag topology (input_ids + offsets, output already pooled to [batch, dim]). Topology is detected from the graph's input signature at load time; no configuration needed:

[embedding]
model = "hf:minishlab/potion-base-8M@main"   # pin a commit SHA in production

Why you would want this:

Metric Transformer (minilm) model2vec (potion-base-8M)
Inference ~520 ms/text ~0.05 ms/text (~500x)
Model size 91 MB ~30 MB
Dimensions 384 256 (probed from the graph)
Quality baseline ~9295 % of MiniLM on MTEB retrieval

The trade-off is deliberate: static embeddings skip the attention pass entirely, so initial indexing of large repos and search on weak hardware (CI runners, laptops on battery) get a massive throughput win for a moderate quality loss. Everything else — hf: download, SHA-256 lockfile, re-index-on-switch, BM25 fallback — behaves exactly like any other custom model.

Supply-chain integrity

Downloads are cached under ~/.lean-ctx/models/hf-<org>-<repo>[-<rev>]/. After the first successful download lean-ctx writes a model.lock.json with the SHA-256 of every artifact (trust-on-first-use). Any later re-download that doesn't reproduce the pinned hash fails hard — a repo that swaps bytes under the same revision is rejected.

To intentionally accept new upstream content: delete the model directory (or just model.lock.json) and re-run.

Operational notes

  • Storage isolation: every repo+revision combination gets its own directory, so switching back and forth never re-downloads.
  • Re-index semantics: the index records the canonical model id (hf:org/repo@rev). Changing repo or revision re-indexes once; vectors from different models never mix.
  • Offline: once downloaded, no network access is needed (or attempted).
  • Fallback: if the model fails to load (bad export, unsupported tokenizer), semantic search degrades gracefully to BM25 — search keeps working.

Troubleshooting

Symptom Cause Fix
Unknown embedding model 'hf:…' Typo in repo id (must be exactly org/repo) Check the repo URL on huggingface.co
Download … returned HTTP 404 Repo has no onnx/model.onnx at that revision Export with optimum-cli (see above) or pick a repo with an ONNX export
Failed to load tokenizer.json Repo ships no fast-tokenizer config or uses an unsupported model type Re-export the tokenizer (AutoTokenizer.from_pretrained(...).save_pretrained() writes tokenizer.json)
SHA-256 mismatch Upstream changed bytes under the same revision Verify upstream intent, then delete model.lock.json to re-pin
Search results look wrong after switch Old index still loading in a long-running daemon lean-ctx restart — the re-index happens on the next search

Built-ins (no setup required)

Alias Model Dims Best for
minilm (default) all-MiniLM-L6-v2 384 Fast general-purpose
jina-code-v2 jina-embeddings-v2-base-code 768 Code + natural language
nomic nomic-embed-text-v1.5 768 Long-form text, MTEB-strong