Skip to content

Upgrade embedding model to voyage-context-4 - #14

Merged
drewburchfield merged 7 commits into
masterfrom
voyage-context-4-upgrade
Jul 13, 2026
Merged

Upgrade embedding model to voyage-context-4#14
drewburchfield merged 7 commits into
masterfrom
voyage-context-4-upgrade

Conversation

@drewburchfield

@drewburchfield drewburchfield commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces voyage-context-3 with voyage-context-4 as the default embedding model. Voyage positions context-4 as a drop-in replacement (announcement): same 1024-dim vectors, same contextualized_embed() API, better retrieval quality (+2.08% chunk-level / +1.4% document-level NDCG@10 across 39 datasets), lower price ($0.12/1M vs $0.18/1M), and its own 200M free-token allowance (Voyage free tiers are per model).

Changes

  • VOYAGE_MODEL env var (default voyage-context-4), resolved inside VoyageEmbedder so the server and indexer always agree on the model; removes the hardcoded model in server.py
  • Pass VOYAGE_MODEL through docker-compose.yml; document in .env.example
  • Fix docker exec examples to use the image's .venv/bin/python (the system python in the container has no dependencies installed, so the documented indexer command failed)
  • Documented MCP client config now disables the file watcher for exec'd stdio sessions (OBSIDIAN_WATCH_ENABLED=false) and a new scripts/run_vault_mcp.sh wraps the invocation; the main container process already owns watching, so each connected client was spawning a duplicate watcher
  • Add .dockerignore: host __pycache__ (stale bytecode), .venv, and .env no longer leak into the image
  • Fix embedder init log to print the resolved model instead of the raw constructor argument

Migration

Embeddings from different models are not comparable, so switching requires a full re-index:

docker compose build && docker compose up -d
docker exec -i obsidian-graph .venv/bin/python -m src.indexer

The embedding cache is keyed by model name, so it invalidates itself; no schema change is needed (both models are 1024-dim).

Testing

  • 133 unit/integration tests pass
  • Live migration verified end to end on a real vault (367 files): full re-index on context-4, then semantic search through the production path returns relevant results with sensible similarity scores
  • SDK compatibility: works with voyageai 0.3.7 (pin stays >=0.3.0); the model name is server-side

Open in Devin Review

Replace voyage-context-3 with voyage-context-4 as the default embedding
model. It is a drop-in replacement: same 1024-dim vectors, same
contextualized_embed API, better retrieval quality (+2.08% chunk-level
NDCG@10 per Voyage), lower price ($0.12/1M vs $0.18/1M), and its own
200M free-token tier.

- Resolve the model from a new VOYAGE_MODEL env var inside
  VoyageEmbedder (default voyage-context-4) so the server and indexer
  can never disagree; drop the hardcoded model in server.py
- Pass VOYAGE_MODEL through docker-compose and document it in
  .env.example
- Fix docker exec examples to use the image's .venv/bin/python (the
  system python has no dependencies installed)
- Disable the file watcher in the documented MCP client config and add
  scripts/run_vault_mcp.sh: the main container process already owns
  watching, so exec'd stdio sessions should be query-only
- Add .dockerignore so host __pycache__, .venv, and .env never leak
  into the image
- Fix embedder init log to print the resolved model instead of the
  constructor argument

Migration: embeddings from different models are not comparable. After
upgrading, run a full re-index (docker exec -i obsidian-graph
.venv/bin/python -m src.indexer). The embedding cache is keyed by model
name and invalidates itself.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread docs/TOOLS.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Pre-existing docs/TOOLS.md MCP config example is now inconsistent with README

The MCP config example at docs/TOOLS.md:371 still shows "args": ["exec", "-i", "obsidian-graph", "python", "-m", "src.server"] — using bare python instead of .venv/bin/python and missing the OBSIDIAN_WATCH_ENABLED=false flag. This is in unchanged lines so it's pre-existing, but the PR updated the equivalent example in README.md:182 to use .venv/bin/python and disable the watcher. Users following the TOOLS.md example would get a python: command not found error since the Docker image only has .venv/bin/python available.

(Refers to line 371)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in d256395: TOOLS.md now matches the README example (.venv/bin/python plus OBSIDIAN_WATCH_ENABLED=false so exec'd MCP sessions don't spawn a duplicate watcher).

Review findings from the voyage-context-4 migration review:

- Detect model drift (Critical): new index_metadata table records which
  embedding model built the index (auto-created for existing databases);
  the server logs a loud error at startup when its configured model
  differs, instead of silently comparing incomparable vectors
- Honest indexer accounting (High): report chunks actually upserted this
  run, log an error instead of a success line when files failed, and
  exit non-zero so partial migrations are visible; only record the
  corpus model after a fully successful run
- Cover VOYAGE_MODEL resolution (High): new tests/test_embedder.py
  covers default fallback, env override, explicit-arg precedence,
  empty/whitespace values, model-keyed cache isolation, and
  invalid-model retry behavior
- Fix documented test commands (High): the image ships without dev deps
  or tests/, so in-container pytest could never work; docs now use
  host-side uv commands and drop the nonexistent test_e2e.py reference
- Harden model resolution (Medium): empty or whitespace VOYAGE_MODEL
  falls back to the default, matching compose ${VAR:-} semantics
- Fail fast on invalid model names (Medium): invalid-model API errors
  are deterministic and no longer retried with backoff
- Replace broken deadlock diagnostic in docs/CONCURRENCY.md (a fresh
  interpreter cannot inspect the server's event loop) with log-based
  diagnosis
@drewburchfield

Copy link
Copy Markdown
Owner Author

Quality gate review (T3, 6 parallel lenses + clawpatch)

Reviewed by six isolated reviewers (correctness, silent failures, tests, types, comments, simplify) plus the clawpatch CLI. Findings remediated in 3f68a51:

  • Critical (silent-failures): nothing recorded which model built the stored vectors, so upgrading without re-indexing silently produced meaningless similarity scores. Fixed: new index_metadata table (auto-created for existing DBs) records the model; the server logs a loud startup error naming the re-index command on mismatch. Verified live: pre-metadata boot errors correctly, post-index boot is clean.
  • High (silent-failures): a partial indexer run logged "Indexing complete! N notes indexed successfully" (N = all rows incl. stale ones) and exited 0. Fixed: honest per-run counts, error log on failures, non-zero exit, and the corpus model is only recorded after a fully successful run.
  • High (tests): the VOYAGE_MODEL resolution chain had zero coverage. Fixed: tests/test_embedder.py covers default/env/arg precedence, empty and whitespace values, model-keyed cache isolation, and invalid-model retry behavior (140 tests pass).
  • High (comments): documented in-container pytest commands could never work (image has no dev deps or tests/). Fixed: docs use host-side uv run pytest; removed the nonexistent test_e2e.py reference.
  • Mediums: empty/whitespace VOYAGE_MODEL now falls back to the default (matching compose ${VAR:-} semantics); invalid-model API errors fail fast instead of retrying a deterministic 400; broken deadlock diagnostic in docs/CONCURRENCY.md replaced.

Deferred (pre-existing on master, out of scope): watcher swallowing per-file embed failures, stale chunk rows when notes shrink, get_similar_notes chunk-level semantics, pool cleanup on failed init (tracked in clawpatch backlog).

Dogfooding against a real vault found two gaps:

- Files that fail to read (e.g. iCloud dataless placeholders that Docker
  file sharing cannot materialize) were logged and skipped without
  counting as failures, so the indexer exited 0 and recorded the corpus
  model despite leaving files unindexed. Read errors now join the
  failure list, fail the run, and block the embedding_model metadata
  write. Regression test added.
- scripts/run_e2e_tests.py (35-check standalone E2E suite) had a broken
  documented invocation: the image ships only src/ and no system-python
  deps. Docstring and README now show the working docker cp invocation.
actions/setup-python v5 and astral-sh/setup-uv v3 target the deprecated
Node 20 runtime on GitHub runners; bump to v6/v7.
@drewburchfield
drewburchfield merged commit 0ca70a7 into master Jul 13, 2026
4 checks passed
@drewburchfield
drewburchfield deleted the voyage-context-4-upgrade branch July 13, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant