diff --git a/AGENTS.md b/AGENTS.md index b8121c0..6220cf7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ cp .env.example .env # once; add BRAVE_API_KEY ./add-project.sh # register a project for code indexing (idempotent; starts stack) ./add-project.sh --remove docker compose up -d --build +docker compose --profile embeddings up -d # same + ollama embeddings container docker compose ps / logs -f mcp / logs -f mcp-codebase-index opencode reload && sleep 3 && opencode mcp list # acceptance check ``` @@ -31,8 +32,9 @@ index ships NAPI binaries — do not switch the base to alpine): One endpoint per registered project: `//mcp`. Mounts `PROJECTS_ROOT` (default `~/src`, kept in `.env`) at the identical container path so index results return valid host paths. Runs as `user: node` (uid 1000) so index data is host-user owned. Index - data lives in each project at `.opencode/index/` (structural mode: BM25 + call graph, - no embedding provider; ollama upgrade documented in README). + data lives in each project at `.opencode/index/` (hybrid mode: BM25 + call graph + + ollama `nomic-embed-text` embeddings; the ollama container ships dormant behind the + `embeddings` profile and indexing degrades to keyword-only while it is off). Per-project client configs (written into each project by add-project.sh): `opencode.json`, `.crush.json`, `.roo/mcp.json`, each pointing at `http://localhost:8001//mcp`. @@ -45,6 +47,11 @@ Global client configs only carry the shared tools (fetch/context7/brave-search). working bridge leave it unset — newer compose delegates builds to buildx bake, which rejects `network: host` ("Pass --allow=network.host...") and compose cannot grant the entitlement. +- The `ollama` container is profile-gated: plain `docker compose up -d` / `down` ignore + it. `INDEXING WARNING: N chunks failed to embed` in index output means ollama was down + or a batch exceeded the 30s embedding timeout — search falls back to keyword ranking + until a re-index succeeds. Batches are pre-tuned small (`OLLAMA_MAX_BATCH_TOKENS` / + `OLLAMA_MAX_BATCH_ITEMS` env on `mcp-codebase-index`) for CPU-only ollama. - `opencode mcp list` can print "No MCP servers configured" for a few seconds after `opencode reload` (daemon reconnects asynchronously). Retry before assuming breakage. - After changing client configs, crush/opencode sessions must be restarted/reloaded; diff --git a/README.md b/README.md index 8640358..f6a8d20 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ Two containers do all the work: - **`mcp-codebase-index`** (port 8001) — a private search index for each of your code projects, built with [open-codebase-index](https://github.com/Helweg/open-codebase-index). It lets the AI find things in a large codebase quickly (keyword search, "where is this - defined", "who calls this") instead of reading files one by one. Every project gets its + defined", "who calls this", and — with the bundled ollama embeddings — "find this by + meaning") instead of reading files one by one. Every project gets its own address: `http://localhost:8001//mcp`. ## Quick start (once per machine) @@ -164,44 +165,56 @@ tools everywhere, add them once to your plugin's global settings file: ## What the code index actually searches -By default the index runs in **structural mode**: keyword (BM25) search, symbol -definitions, call graphs, branch awareness — entirely **offline**, no AI provider needed. -This is usually plenty and it works out of the box. +The index runs in **hybrid mode**: keyword (BM25) search, symbol definitions, call +graphs, branch awareness — plus **semantic search** ("find code by meaning") via local +[ollama](https://ollama.com) embeddings. The `ollama` container ships with this stack +but stays dormant (it lives behind the `embeddings` profile), so everything already +works fully offline without it: while ollama is off, semantic ranking just falls back +to keyword ranking. -To add **semantic search** ("find code by meaning"), plug in an embedding provider: +Enable semantic search (once per machine): -1. Start local Ollama embeddings: `docker compose --profile embeddings up -d ollama`, then - `docker compose exec ollama ollama pull nomic-embed-text`. -2. In `codebase-index.config.json` switch `"mode": "structural"` to `"hybrid"` and add: - - ```json - { - "embeddingProvider": "custom", - "customProvider": { - "baseUrl": "http://ollama:11434/v1", - "model": "nomic-embed-text", - "dimensions": 768 - } - } - ``` - -3. Rebuild each project's index: - - ```bash - docker compose exec mcp-codebase-index ocbi index --project ~/src/git/my-project --config /etc/codebase-index/config.json --force - ``` +```bash +docker compose --profile embeddings up -d # start the ollama container +docker compose exec ollama ollama pull nomic-embed-text # one-time, ~274 MB, kept in a volume +# build vectors for your projects (safe to repeat): +docker compose exec mcp-codebase-index ocbi index --project ~/src/git/my-project \ + --config /etc/codebase-index/config.json --force +``` -(Or ask the AI to run the `index_codebase` / `index_status` tools — rebuilding and -refreshing indexes is something the assistants can do themselves through MCP.) +Projects registered later with `./add-project.sh` pick embeddings up automatically. +Check a project's mode: `docker compose exec mcp-codebase-index ocbi status --project +~/src/git/my-project --config /etc/codebase-index/config.json` — semantic search is +live when it reports `Provider: ollama` and the index shows embedded chunks. (You can +also just ask the AI to run `index_status` / `codebase_search`; semantic hits carry +similarity scores near 1.0.) + +Gotchas: + +- Plain `docker compose up -d` / `down` / `ps` do **not** manage the ollama container. + Pass `--profile embeddings` whenever a command should include it; + `docker compose --profile embeddings down` stops everything. +- If indexing logs `INDEXING WARNING: N chunks failed to embed`, ollama was unreachable + or too slow (CPU). Start it, make sure the model is pulled, re-run the index — failed + chunks are simply rebuilt on the next run. Search keeps working keyword-only meanwhile. +- Batch sizes for the index container are pre-tuned small (`OLLAMA_MAX_BATCH_TOKENS` / + `OLLAMA_MAX_BATCH_ITEMS` env in `docker-compose.yml`) so CPU-only ollama stays under + the embedding API's 30s timeout. Raise them if you have a GPU. +- Embeddings don't have to be local: any OpenAI-compatible endpoint works. Switch + `embeddingProvider` to `"custom"` in `codebase-index.config.json` and set + `customProvider: { "baseUrl", "model", "apiKey" }`. The chat model provider (z.ai, + OpenRouter, cortecs, …) is configured in your AI tool and is completely independent + of this stack — the index never talks to it. ## Everyday commands ```bash docker compose ps # status +docker compose --profile embeddings ps # status, including ollama docker compose logs -f mcp # shared-tools gateway logs docker compose logs -f mcp-codebase-index # index gateway logs (per-project lines prefixed) docker compose restart mcp mcp-codebase-index # after editing supergateway.config.jsonc / codebase-index.config.json -docker compose down # stop everything (index data survives, it lives in the projects) +docker compose --profile embeddings down # stop everything incl. ollama (index data survives, it lives in the projects) ``` ## Adding or removing shared tools (port 8000) @@ -262,6 +275,12 @@ docker compose ps # both services "Up" which refuses `network: host` builds with "Pass `--allow=network.host`..." unless the host really needs it — on machines with working bridge networking leave `BUILD_NETWORK` unset (or set it to `default`). If it still fails, check your firewall. +- **Indexing logs `INDEXING WARNING: N chunks failed to embed` (no semantic scores)** — + the ollama embeddings container isn't running or the model isn't pulled yet. Fix with + `docker compose --profile embeddings up -d` + + `docker compose exec ollama ollama pull nomic-embed-text`, then re-run the index. + Search still works meanwhile, just keyword-ranked (see "What the code index actually + searches"). - **`brave-search` tools fail** — missing/invalid `BRAVE_API_KEY` in `.env`. - **Index seems stale** — re-run the project's index: `./add-project.sh ~/src/git/my-project` (safe to repeat), or ask the AI to run diff --git a/codebase-index.config.json b/codebase-index.config.json index 811d168..360282b 100644 --- a/codebase-index.config.json +++ b/codebase-index.config.json @@ -1,10 +1,11 @@ { "scope": "project", "indexing": { - "mode": "structural", + "mode": "hybrid", "autoIndex": false, "watchFiles": true }, + "embeddingProvider": "ollama", "search": { "maxResults": 20 } diff --git a/docker-compose.yml b/docker-compose.yml index 6339b80..054c872 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,13 @@ services: - ${PROJECTS_ROOT:?run ./add-project.sh first}:${PROJECTS_ROOT:?run ./add-project.sh first} - ./codebase-index.config.json:/etc/codebase-index/config.json:ro - ./supergateway.projects.jsonc:/etc/supergateway/projects.jsonc:ro + environment: + # Where the ollama embeddings service lives (compose network name). + OLLAMA_HOST: http://ollama:11434 + # Small batches so each embedding request finishes well under the 30s + # client timeout when ollama runs on CPU (defaults intermittently time out). + OLLAMA_MAX_BATCH_TOKENS: 4096 + OLLAMA_MAX_BATCH_ITEMS: 4 command: - --config - /etc/supergateway/projects.jsonc @@ -64,20 +71,21 @@ services: - /healthz restart: unless-stopped - # Optional: local embeddings for semantic (hybrid) codebase indexing. - # Start with: docker compose --profile embeddings up -d - # Then switch indexing.mode to "hybrid" in codebase-index.config.json and set - # embeddingProvider "custom" with baseUrl http://ollama:11434/v1, - # model nomic-embed-text, dimensions 768, and run: - # docker compose exec mcp-codebase-index ocbi index --project $PWD --config /etc/codebase-index/config.json --force - # ollama: - # image: ollama/ollama:latest - # profiles: ["embeddings"] - # ports: - # - "11434:11434" - # volumes: - # - ollama:/root/.ollama - # restart: unless-stopped + # Local embeddings for semantic (hybrid) codebase indexing. Dormant by default — + # plain `docker compose up -d` ignores it; it only starts with the embeddings + # profile. Always pass the profile on later `up`/`down` calls that should + # include it (see README "Semantic search"): + # docker compose --profile embeddings up -d + # docker compose exec ollama ollama pull nomic-embed-text + ollama: + image: ollama/ollama:latest + profiles: ["embeddings"] + container_name: mcp-ollama + ports: + - "11434:11434" + volumes: + - ollama:/root/.ollama + restart: unless-stopped -# volumes: -# ollama: +volumes: + ollama: