Skip to content

perf(mcp): lazy-load vector backend (warm search skips torch import)#441

Merged
HumanBean17 merged 3 commits into
masterfrom
perf/mcp-v2-lazy-vector-backend
Jul 12, 2026
Merged

perf(mcp): lazy-load vector backend (warm search skips torch import)#441
HumanBean17 merged 3 commits into
masterfrom
perf/mcp-v2-lazy-vector-backend

Conversation

@HumanBean17

Copy link
Copy Markdown
Owner

Summary

  • Lazy-load the vector backend so a warm jrag search skips the heavy torch import entirely, cutting first-response latency.
  • Follow-up hardening: sentinel + lock guarding the lazy init against concurrent first-touch races.
  • Bumps version to 0.10.2 (published to PyPI from this branch).

Commits

  • ce8e294 perf(mcp): lazy-load vector backend so warm jrag search skips torch import
  • b844cb6 fix(mcp): sentinel + lock for lazy vector backend (review follow-up)
  • d7cbcb0 bump version to 0.10.2

Note

PyPI was published from this branch ahead of merge — merging brings master back in line with the released 0.10.2.

🤖 Generated with Claude Code

HumanBean17 and others added 3 commits July 12, 2026 00:28
… import

`mcp_v2` eagerly imported `search_lancedb` at module load, which imports
`sentence_transformers`/`torch` at its own top level (~2.8s). The warm
`jrag watch` client reconstructs `SearchOutput` from `mcp_v2`
(`watch/client._reconstruct`) without ever calling the vector backend, so
every warm CLI read paid that ~2.8s import — masking the daemon's win and
leaving warm `jrag search` at ~2.6s instead of near-instant.

Convert the eager import to lazy: `run_search`/`TABLES` start as sentinels
and are populated by `_ensure_vector_backend()` on the first `search_v2`
call. The guard preserves existing semantics:
- tests that monkeypatch `mcp_v2.run_search` (non-None) are not overwritten;
- graph-only installs (ImportError) leave `run_search=None` -> lexical fallback.

Measured on tests/bank-chat-system (130 Java files):
- `SearchOutput` import: 2.85s -> 0.16s
- warm `jrag search`: 2.6s -> 0.15s steady (17x); cold path unchanged
- results byte-identical warm vs cold

Tests: run_search monkeypatchers (185), lightweight watch (101), heavy
warm-path (6), all 6 golden IPC byte-identity (warm == cold) — all green.

Co-Authored-By: Claude <noreply@anthropic.com>
Address two code-review findings on the lazy-load change:

1. Critical — sentinel conflation. The guard `run_search is not None` could
   not distinguish "unloaded" (`None` sentinel) from a test that forces
   lexical mode via `monkeypatch.setattr(mcp_v2, "run_search", None)`. The
   loader ran anyway, overwrote the patch with the real backend, and
   `lexical_mode` flipped to False — regressing
   `test_search_v2_lexical_dispatch_end_to_end` (Lance "Table not found").
   Fix: a distinct `_NOT_LOADED = object()` sentinel for the unloaded state,
   so `None` unambiguously means "lexical/disabled" and is authoritative in
   both monkeypatch directions (None → lexical; non-None → vector).

2. Important — thread race. The MCP server dispatches `search_v2` via
   `asyncio.to_thread` (server.py:659), so two concurrent first-batch
   searches could race the unguarded sentinel mutation: Thread B sees
   `_VECTOR_BACKEND_LOADED=True` but `run_search` still `None` and wrongly
   takes the lexical path / reads `TABLES={}`. Fix: double-checked locking
   with a `threading.Lock`, mirroring the existing `_st_lock` pattern.

The sentinel also lets us drop `_VECTOR_BACKEND_LOADED` entirely (the
sentinel itself tracks state), and a non-ImportError now propagates and
leaves `run_search` as `_NOT_LOADED` so the next call retries instead of
poisoning the process.

Verified: previously-failing lexical test passes; 206 lexical + run_search-
monkeypatcher tests pass; 12 heavy warm + golden IPC byte-identity tests
(warm == cold) pass; SearchOutput import stays ~0.14s.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@HumanBean17 HumanBean17 merged commit c5f0f03 into master Jul 12, 2026
4 checks passed
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