feat(search): support Ollama embedding models - #84
Merged
Conversation
mrrobertkent
force-pushed
the
feat/ollama-embeddings
branch
3 times, most recently
from
August 11, 2026 20:54
d0b59c5 to
7e49e0a
Compare
`getDb` called `transformersJs()` with no arguments, pinning every index and query to retriv's smallest default (bge-small-en-v1.5, 384d) on whatever device transformers.js chose, which is the CPU under Node. Neither was reachable through config, a flag, or an env var. That default thins out as skills accumulate: search builds one sqlite-vec DB per package and pools scores across all of them at query time, so cross-corpus ranking depends directly on embedding quality. Adds `embedModel` and `embedDevice` config keys, matching entries in `skilld config`, and `SKILLD_EMBED_MODEL` / `SKILLD_EMBED_DEVICE` overrides for single runs. Precedence is env, then config, then default. Both defaults are unchanged: `bge-small-en-v1.5`, and a device of `auto` that resolves to undefined so the option is omitted entirely. Device measurements on an Apple M5 Max, 120 documents, best of 3 (docs/sec): model cpu coreml webgpu bge-small-en-v1.5 664 198 1713 bge-base-en-v1.5 198 68 580 Xenova/bge-large-en-v1.5 71 9 201 webgpu is 2.6-2.9x faster than cpu at every size, 4.4x end to end through the index pipeline; coreml is consistently slower. The ranking is hardware-specific, so the device is offered rather than defaulted, and the picker leads with that caveat. Two correctness details: The bge-large entry pins the full repo id `Xenova/bge-large-en-v1.5`. retriv's bare `bge-large-en-v1.5` preset maps to `onnx-community/bge-large-en-v1.5`, whose weights return 401, so selecting it would fail at first index. The embedding cache keyed vectors by text hash and validated only dimensions. That was safe while the model was fixed; selecting one makes it reachable, since bge-large-en-v1.5 and bge-m3 are both 1024d. Switching kept every cached vector and served one model's embeddings against another's queries. No crash, just silently wrong ranking, with the correct answer dropping out of the top 3 on a 43-document corpus. Cache identity is now `<model>@<device>`, cleared on change, because the same model on a different backend can differ numerically.
Search was limited to the transformers.js models bundled through retriv. Ollama already hosts stronger local embedders, and skilld already talks to Ollama for completions, so the capability was one HTTP call away. Models are addressed as `ollama:<name>`, matching the enhancement-model syntax. `skilld config` lists locally-pulled models advertising the `embedding` capability alongside the built-in ones. Discovery is additive: an unreachable daemon contributes nothing rather than erroring. Talks to `/api/embed` over plain fetch rather than retriv's Ollama provider, which would pull in `ai` and `ollama-ai-provider-v2`. That keeps the dependency footprint unchanged and matches src/agent/clis/ollama.ts, which already uses fetch against /api/chat, /api/tags and /api/show. `ollamaHost()` moves to core/ so the search worker can resolve OLLAMA_HOST without importing the agent registry. Dimensions and context length come from /api/show when the model reports them, falling back to a probe embedding. Capability confirmation is required rather than fail-open: unlike completions, a chat model errors on /api/embed, so an unconfirmed model would break indexing later. Vectors are L2-normalised on the way out. The index scores by L2 distance and assumes unit vectors; Ollama normalises server-side today, but that behaviour is undocumented and silently depending on it would make ranking correctness hostage to an implementation detail. Normalising is idempotent for unit vectors. Ollama manages its own execution device, so the embedding device setting does not apply; the picker says so rather than ignoring it silently.
mrrobertkent
force-pushed
the
feat/ollama-embeddings
branch
from
August 11, 2026 21:02
7e49e0a to
d0c5992
Compare
Collaborator
|
Amazing, thank you for your efforts 💪 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Search is limited to the transformers.js models bundled through retriv. Ollama already hosts stronger local embedders (
qwen3-embedding,embeddinggemma,nomic-embed-text), and skilld already talks to Ollama for completions, so the capability was one HTTP call away.Usage
Models are addressed as
ollama:<name>, matching the existing enhancement-model syntax.skilld configlists locally-pulled models that advertise theembeddingcapability, alongside the built-ins:Discovery is additive and never blocks. An unreachable daemon contributes nothing rather than erroring.
No new dependencies
retriv ships an Ollama provider, but it imports
aiandollama-ai-provider-v2. This talks to/api/embedover plainfetchinstead, matchingsrc/agent/clis/ollama.ts, which already usesfetchagainst/api/chat,/api/tags, and/api/show. Dependency footprint is unchanged.ollamaHost()moves tocore/so the search worker can resolveOLLAMA_HOSTwithout importing the agent registry.Normalization
This is the part I wanted to verify rather than assume.
The sqlite-vec table is created as
vec0(embedding float[N])with nodistance_metric=, so it defaults to L2, andscore: 1 / (1 + distance). That only ranks correctly if vectors are unit length.transformersJsguarantees it vianormalize: true, and measured norms are exactly1.0000.Magnitude genuinely corrupts ranking under L2. Scaling only the most-relevant document, everything else fixed:
I checked, and Ollama does normalize server-side:
nomic-embed-text,embeddinggemma, andqwen3-embedding:0.6ball return norms of exactly1.000000on Ollama 0.32.9. This still normalizes on the way out, because that behaviour is undocumented and silently depending on it would make ranking correctness hostage to an Ollama implementation detail. It's idempotent for unit vectors.Worth flagging separately: retriv's own
ollama()provider returns raw embeddings with no normalization, so it carries this risk today when paired with the sqlite driver.Metadata without a probe
/api/showreports everything needed, so the common path costs one request and no wasted inference:Falls back to a probe embedding when a model omits
embedding_length. Capability confirmation is required rather than fail-open. Unlike completions, a chat model errors on/api/embed, so an unconfirmed model would break indexing later.Measured
Apple M5 Max, 120 documents, after warm-up:
ollama:nomic-embed-textXenova/bge-large-en-v1.5(webgpu)ollama:embeddinggemmaollama:qwen3-embedding:0.6bEnd to end through skilld's own index pipeline, all three Ollama models returned the correct top hit.
Testing
test/unit/ollama-embeddings.test.tshas 12 tests withfetchmocked, so CI needs no Ollama daemon:/api/showin exactly one requestembedding_lengthis absent[3,4]becomes[0.6,0.8])try pulling it first)[]when the daemon is down, filters to embedding-capable, drops unconfirmable modelsAlso verified live against Ollama 0.32.9 with all three models pulled.
README documents the
ollama:syntax, capability filtering,OLLAMA_HOST, and the device interaction. Full suite: 895 passing. The onegit-skillsfailure reproduces on an unmodified checkout, andpnpm lintstill cannot run repo-wide, both as noted in #83.Interaction with the device setting
Ollama manages its own execution device, so
embedDevicedoes not apply. Rather than ignore it silently, the device picker says so when an Ollama model is active.