ADR-026: Tier-3 RAG engine is cocoindex-code, wired alongside Graphify (option A)
- Status: Accepted
- Date: 2026-06-26
- Implements: #495 (tier-3 RAG — pick the engine & decide scope), un-parking ADR-024 §4
- Relates to: ADR-009 (Graphify preset / LightRAG removal — the constraints this honours),
ADR-024 (memory-tier model, #478), #505/#506 (the tier-3 seam this builds on),
#498/ADR-025 (the
rag_endpointdescriptor a root orchestrator reads)
Context
ADR-024 §4 accepted tier-3 RAG as a seam and deferred the engine to #495, which held two
open questions until a hands-on test: (1) which upstream tool, and (2) the A-vs-B
scope — a distinct vector surface alongside Graphify (A), or one tool that replaces
Graphify with graph-only/graph+vector modes (B, which would supersede ADR-009). #495 was
parked with a "revisit ~2026-06-26" date and a vetted starting candidate (codebase-memory-mcp).
On 2026-06-26 the owner greenlit the build. A verification pass (recorded below) was run first, as the plan required.
Decision
Engine: cocoindex-code (ccc). Scope:
option A — a distinct semantic/vector recall surface that sits alongside Graphify,
never replacing it. ADR-009's Graphify pick stands.
The #505 seam becomes a real engine: setup_rag.sh installs ccc at tool level, pins a
keyless on-device embedding model, and builds an embedded sqlite-vec index. Tier 3 stays
opt-in (--memory obsidian-graphify-rag), never a preset — it earns its keep only at
multi-project / monorepo scale.
Why cocoindex-code clears the ADR-009 bar
| ADR-009 hard constraint | How cocoindex-code satisfies it |
|---|---|
| Upstream-maintained tool/MCP, never hand-rolled | cocoindex-code CLI + ccc mcp server; we ship only docs + a user-run script |
| No API key on the default path | Local sentence-transformers via the [full] extra; provider: sentence-transformers pinned. (A cloud litellm provider exists but is never on the scaffolded path.) |
| Tool-level install, not a pinned project dep | uv tool install 'cocoindex-code[full]==0.2.37' — isolated, mirrors graphifyy |
| Index is a gitignored derived cache | Embedded sqlite-vec in .cocoindex_code/, auto-gitignored; no server, no Docker |
Why option A (alongside), not B (replace)
The structural code graph (Graphify: "who calls this / how does it fit together") and a semantic vector store ("where did we touch X", concept-level matches across unrelated names) are complementary retrieval modes, not substitutes. Keeping them separate preserves ADR-009 and lets each be authoritative for what it is good at (composition rule, ADR-024 §2: Graphify authoritative for code structure; RAG authoritative for nothing — additive recall).
Embedding model: a single keyless default — CodeRankEmbed
setup_rag.sh defaults to nomic-ai/CodeRankEmbed (137M, MIT) — on-device, no API key,
~550MB, laptop-CPU fast. It is the default because a hands-on bake-off (below) showed it is
the best code-recall model that actually loads in cocoindex-code today. RAG_EMBED_MODEL
remains an escape hatch, but is intentionally de-emphasised: the smaller arctic-embed-xs
(22M) is weaker on code, and the larger 1.5–2B code models do not load at all (see below), so a
multi-model wizard would be over-engineering for what is effectively one viable choice.
Bake-off (2026-06-26, hands-on, 14-concept confusable corpus, no-shared-keyword queries)
Measured top-1 retrieval accuracy, all keyless/on-device on a 15GB-RAM box:
| Model | Loads in cocoindex-code? | Top-1 | Note |
|---|---|---|---|
nomic-ai/CodeRankEmbed (137M) |
✅ | 13/14 | chosen default |
snowflake-arctic-embed-xs (22M) |
✅ | 11/14 | general-purpose, weaker on code |
BAAI/bge-code-v1 (~1.5B) |
❌ | — | Unrecognized processing class |
Qodo/Qodo-Embed-1-1.5B |
❌ | — | Qwen2Config has no attribute rope_theta |
Salesforce/SFR-Embedding-Code-2B_R |
❌ | — | cannot import HybridCache |
nomic-ai/nomic-embed-code (7B) |
⚠️ | — | curated, but ~28GB download + OOMs on 15GB RAM |
Root cause for the failures: cocoindex-code 0.2.37 pins a bleeding-edge transformers==5.12.1,
and the current crop of large Qwen2-based code embedding models break against it. So benchmark
score is moot — pluggability is the binding constraint, and CodeRankEmbed wins it.
This ceiling is temporary — follow-up tracked in #515. Re-run this bake-off when cocoindex-code's
transformers pin and those models' code converge; if a larger model then loads keyless and
beats CodeRankEmbed on recall, wire it into setup_rag.sh + using-rag.md and amend this ADR.
Consequences
- A project scaffolded with tier 3 gets a one-command, keyless, container-free semantic search
over its corpus, discoverable by a root orchestrator via
memory.rag_endpoint. - Maturity risk (accepted): cocoindex-code is alpha (0.2.x, ~weekly releases). Contained by pinning an exact version, opt-in scope, and a user-run (not auto-run) installer — the owner upgrades deliberately. Revisit the pin when it reaches a stable release.
- The two silent key-traps (slim install; omitting
provider:) are defused in the script and pinned by tests (test_rag_seam.py::TestEngineWired).
Verification (2026-06-26, sources fetched same day)
- Rejected
codebase-memory-mcp(the prior front-runner): itssemantic_queryis an 11-signal hybrid dominated by TF-IDF + AST scoring (embeddings 1 of 11) — primarily an AST graph that overlaps Graphify; its "768-dimnomic-embed-codecompiled into a static binary" claim is internally contradictory (the real model is 7B/3584-dim). Wrong shape for a vector L3. - Rejected for needing keys/servers:
claude-context(Milvus server + OpenAI/Voyage keys; Milvus Lite is Python-only, unavailable to its Node SDK),Cognee(OpenAI default, pip framework),Tabby(GPU inference server). - Considered, embedding-free:
Serena(LSP symbol graph, mature, keyless) — but it is structural (overlaps Graphify), so it does not deliver fuzzy semantic recall; noted as an alternative, not the tier-3 engine. - cocoindex-code verified from source (
pyproject.toml,settings.py,embedder_defaults.py,query.py,cli.py): keyless local default, both Nomic models explicitly pluggable,sqlite-vecembedded store, auto-gitignore, no server. Latest 0.2.37 (2026-06-23).