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
122 lines
5.0 KiB
Markdown
122 lines
5.0 KiB
Markdown
# 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:
|
||
|
||
```toml
|
||
# config.toml
|
||
[embedding]
|
||
model = "hf:intfloat/multilingual-e5-small@ffdcc22a9a5c973258343b0001a4d483cbd45be9"
|
||
```
|
||
|
||
Or per-shell:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```text
|
||
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.
|
||
|
||
```toml
|
||
[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:
|
||
|
||
```toml
|
||
[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 | ~5–20 ms/text | ~0.05 ms/text (**~500x**) |
|
||
| Model size | 91 MB | ~30 MB |
|
||
| Dimensions | 384 | 256 (probed from the graph) |
|
||
| Quality | baseline | ~92–95 % 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 |
|