graph-indexer 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROMPT.md +182 -143
- package/README.md +338 -80
- package/config.mjs +118 -0
- package/core-engine.mjs +631 -188
- package/enrichment.mjs +347 -0
- package/indexer.mjs +101 -71
- package/init.mjs +4 -2
- package/mcp-server.mjs +83 -569
- package/mcp-tools.mjs +545 -0
- package/package.json +16 -7
- package/parser-utils.mjs +319 -10
- package/search-core.mjs +416 -0
- package/sqlite-store.mjs +768 -0
- package/storage.mjs +43 -0
- package/watch-daemon.mjs +156 -67
package/README.md
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<!-- Reemplaza la URL de abajo con la ruta a tu nuevo logo profesional en formato SVG o PNG transparente -->
|
|
3
|
+
<img src="https://raw.githubusercontent.com/MaquinaTech/graph-indexer/main/assets/logo.jpg" alt="Graph Indexer Logo" width="250" />
|
|
2
4
|
|
|
3
|
-
<
|
|
4
|
-
<strong>Zero-Database · Air-Gapped · AST-Precise · Hybrid Search</strong><br>
|
|
5
|
-
<em>An MCP server that lets AI coding agents search your codebase by concept instead of reading files — reducing context usage by 65–88% per query.</em>
|
|
6
|
-
</p>
|
|
5
|
+
<h1>Graph Indexer</h1>
|
|
7
6
|
|
|
8
|
-
<p
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
</p>
|
|
7
|
+
<p>
|
|
8
|
+
<strong>Zero-Database · Air-Gapped · AST-Precise · Hybrid Search · Monorepo-Scale</strong>
|
|
9
|
+
</p>
|
|
10
|
+
<p>
|
|
11
|
+
<em>An MCP server that lets AI coding agents search your codebase by concept instead of reading files, cutting context usage by 65–88% per query.</em>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<!-- Badges -->
|
|
15
|
+
<a href="https://www.npmjs.com/package/graph-indexer"><img src="https://img.shields.io/npm/v/graph-indexer?color=007acc&style=for-the-badge" alt="npm version"></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/graph-indexer"><img src="https://img.shields.io/npm/dt/graph-indexer?color=4caf50&style=for-the-badge" alt="NPM Downloads"></a>
|
|
17
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen?style=for-the-badge&logo=nodedotjs" alt="Node.js 18+"></a>
|
|
18
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge" alt="License: MIT"></a>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<br />
|
|
14
22
|
|
|
15
23
|
---
|
|
16
24
|
|
|
@@ -19,6 +27,8 @@
|
|
|
19
27
|
- [What it does](#what-it-does)
|
|
20
28
|
- [Results](#results)
|
|
21
29
|
- [Getting started](#getting-started)
|
|
30
|
+
- [Storage backends](#storage-backends)
|
|
31
|
+
- [Semantic enrichment](#semantic-enrichment)
|
|
22
32
|
- [MCP tools](#mcp-tools)
|
|
23
33
|
- [Configuration](#configuration)
|
|
24
34
|
- [Supported languages](#supported-languages)
|
|
@@ -33,9 +43,12 @@
|
|
|
33
43
|
|
|
34
44
|
## What it does
|
|
35
45
|
|
|
36
|
-
graph-indexer pre-indexes your codebase into an AST-precise search index and exposes it as an MCP server. Instead of reading full files, AI agents call `search_code("payment validation")` and get back the exact functions that match — with type signatures, docstrings, dependency graph, and call sites — in a fraction of the tokens.
|
|
46
|
+
graph-indexer pre-indexes your codebase into an AST-precise search index and exposes it as an MCP server. Instead of reading full files, AI agents call `search_code("payment validation")` and get back the exact functions that match — with type signatures, docstrings, decorators, inheritance edges, dependency graph, and call sites — in a fraction of the tokens.
|
|
47
|
+
|
|
48
|
+
It runs entirely on your machine. No external database, no cloud APIs, no telemetry. Two design choices make it suitable for any repository size:
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
- A **default in-memory engine** that is instant and dependency-free, and an optional **disk-backed SQLite backend** that holds the index on disk so retrieval RAM stays flat on enterprise monorepos that would otherwise exhaust Node's heap.
|
|
51
|
+
- Optional **local-LLM semantic enrichment** that teaches the index what each core component *does*, so conceptual queries match code that shares none of their words.
|
|
39
52
|
|
|
40
53
|
**Token comparison:**
|
|
41
54
|
|
|
@@ -60,39 +73,96 @@ The difference compounds across a session. Agents that read files also lack topo
|
|
|
60
73
|
|
|
61
74
|
## Results
|
|
62
75
|
|
|
63
|
-
Measured across 5 production open-source codebases (
|
|
76
|
+
Measured across 5 production open-source codebases (8,296 AST chunks, 100 ground-truth queries:
|
|
77
|
+
69 **symbolic** — the developer names or paraphrases the symbol — and 31 **semantic** — agent-style
|
|
78
|
+
behavioural descriptions sharing few or no words with the code).
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
| :--- | :--- | ---: | ---: | ---: | ---: |
|
|
69
|
-
| Axios v1.6.0 | JavaScript | ~2,290 tok | ~655 tok | **71.4%** | 0.2 ms |
|
|
70
|
-
| Express 4.18.2 | JavaScript | ~7,330 tok | ~850 tok | **88.4%** | 0.2 ms |
|
|
71
|
-
| NestJS v10.4.9 | TypeScript | ~2,535 tok | ~900 tok | **64.5%** | 0.7 ms |
|
|
72
|
-
| FastAPI 0.103.0 | Python | ~4,030 tok | ~685 tok | **83.0%** | 1.0 ms |
|
|
73
|
-
| Gin v1.9.1 | Go | ~2,530 tok | ~450 tok | **82.2%** | 0.4 ms |
|
|
74
|
-
| **Mean** | | | | **77.9%** | **0.5 ms** |
|
|
80
|
+
* **Average Token Savings:** 79.0%
|
|
81
|
+
* **Strict Success@5 (Hybrid + Rerank):** 82%
|
|
82
|
+
* **Strict MRR (Symbolic queries):** 0.84
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
### Token savings per query
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
| Project | Language | Chunks | Recall@5 (loose) | Savings |
|
|
87
|
+
| :--- | :--- | ---: | ---: | ---: |
|
|
88
|
+
| Axios v1.6.0 | JavaScript | 450 | 0.89 | **69.7%** |
|
|
89
|
+
| Express 4.18.2 | JavaScript | 389 | 0.95 | **88.7%** |
|
|
90
|
+
| NestJS v10.4.9 | TypeScript | 2,675 | 0.95 | **67.5%** |
|
|
91
|
+
| FastAPI 0.103.0 | Python | 3,694 | 0.76 | **85.5%** |
|
|
92
|
+
| Gin v1.9.1 | Go | 1,088 | 0.94 | **83.7%** |
|
|
93
|
+
| **Mean** | | **8,296** | **0.90** | **79.0%** |
|
|
94
|
+
|
|
95
|
+
> Savings = tokens in the returned chunk snippets vs the full source files containing the top-5
|
|
96
|
+
> results, at 4 chars/token. Search latency is sub-millisecond lexical; the hybrid channel adds
|
|
97
|
+
> one local embedding call plus a streamed vector scan (a few ms at this corpus size).
|
|
98
|
+
|
|
99
|
+
### Search quality (strict)
|
|
100
|
+
|
|
101
|
+
Strict scoring: a result counts **only** if its symbol name (or its class, for method chunks)
|
|
102
|
+
exactly equals the ground truth — no substring matching, no file-path credit. Hybrid =
|
|
103
|
+
BM25 + `nomic-embed-text` vectors with `qwen2.5-coder:1.5b` enrichment; rerank adds the
|
|
104
|
+
opt-in `qwen2.5-coder:7b` judge on natural-language queries.
|
|
105
|
+
|
|
106
|
+
| Project | Strict success@5 | **Rank-1 (strict)** | MRR (strict) |
|
|
107
|
+
| :--- | ---: | ---: | ---: |
|
|
108
|
+
| Axios v1.6.0 | 0.74 | 0.68 | 0.72 |
|
|
109
|
+
| Express 4.18.2 | 0.90 | 0.62 | 0.73 |
|
|
110
|
+
| NestJS v10.4.9 | 0.67 | 0.57 | 0.60 |
|
|
111
|
+
| FastAPI 0.103.0 | 0.81 | 0.67 | 0.74 |
|
|
112
|
+
| Gin v1.9.1 | 1.00 | 0.78 | 0.84 |
|
|
113
|
+
| **Mean (hybrid + rerank)** | **0.82** | **0.66** | **0.73** |
|
|
114
|
+
|
|
115
|
+
Split by query style and configuration (the numbers that matter for agent workflows):
|
|
116
|
+
|
|
117
|
+
| Channel | Rank-1 | MRR | Success@5 |
|
|
118
|
+
| :--- | ---: | ---: | ---: |
|
|
119
|
+
| **Symbolic** (name-lookup, 69q), hybrid | **0.80** | **0.84** | — |
|
|
120
|
+
| **Semantic** (behavioural, 31q), hybrid + rerank | **0.35** | **0.47** | **0.65** |
|
|
121
|
+
| Semantic, hybrid (no rerank) | 0.23 | 0.37 | 0.55 |
|
|
122
|
+
| Semantic, lexical-only (no Ollama) | 0.19 | 0.29 | 0.48 |
|
|
123
|
+
|
|
124
|
+
Read the semantic rows with their denominator in mind: under *strict* scoring several of those
|
|
125
|
+
queries are unwinnable in-repo (the expected symbol is an anonymous default export, or is
|
|
126
|
+
re-exported from a dependency whose implementation isn't in the codebase), and most strict
|
|
127
|
+
"misses" still land the right file in the top-2 — which the loose channel (0.89) credits and an
|
|
128
|
+
agent can use. Symbol-naming queries — the bulk of real agent traffic — find the exact chunk
|
|
129
|
+
first try 80% of the time and in the top ranks (MRR 0.84) almost always.
|
|
130
|
+
|
|
131
|
+
Ranking favours exact symbol matches even for short, high-signal names — `res.json`,
|
|
132
|
+
`req.get`, `app.all` — by gating the name boost on corpus **document frequency** instead of a
|
|
133
|
+
blunt length cutoff (with singular/plural equivalence, so `BackgroundTask` finds
|
|
134
|
+
`BackgroundTasks`). Natural-language queries are detected and ranked **vector-first** (with the
|
|
135
|
+
name/path boosts gated on semantic agreement), so behavioural questions aren't drowned by
|
|
136
|
+
keyword noise. Test, spec, example and sandbox chunks are demoted in both channels.
|
|
137
|
+
|
|
138
|
+
**Backend parity is exact**: every rank-assigning sort uses deterministic tie-breaking and both
|
|
139
|
+
stores funnel vector candidates through one shared finalizer, so the in-memory engine and the
|
|
140
|
+
SQLite store return **identical top-5 chunk ids for 100/100 benchmark queries** — verified, not
|
|
141
|
+
approximate.
|
|
142
|
+
|
|
143
|
+
Reproduce every view:
|
|
79
144
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
```bash
|
|
146
|
+
npm run test:setup # clone the 5 fixture repos
|
|
147
|
+
npm run test # loose hit-rate + token savings + indexing stats
|
|
148
|
+
npm run test:eval # strict symbol-level accuracy, lexical channel
|
|
149
|
+
OLLAMA_HOST=http://localhost:11434 node test/evaluate.mjs --embeddings # hybrid
|
|
150
|
+
OLLAMA_HOST=http://localhost:11434 node test/evaluate.mjs --embeddings --use-sqlite # parity
|
|
151
|
+
OLLAMA_HOST=http://localhost:11434 RERANK_MODEL=qwen2.5-coder:7b \
|
|
152
|
+
node test/evaluate.mjs --embeddings --rerank # + judge
|
|
153
|
+
```
|
|
88
154
|
|
|
89
|
-
|
|
155
|
+
> The strict harness ([test/evaluate.mjs](test/evaluate.mjs)) exists specifically to keep
|
|
156
|
+
> these numbers honest and prevent the tool from being tuned to a friendly benchmark — it
|
|
157
|
+
> reports the inflation gap (loose minus strict) per suite so regressions in real precision
|
|
158
|
+
> can't hide behind a permissive hit-rate, and it splits the symbolic and semantic channels
|
|
159
|
+
> so neither inflates the other.
|
|
90
160
|
|
|
91
161
|
---
|
|
92
162
|
|
|
93
163
|
## Getting started
|
|
94
164
|
|
|
95
|
-
**Requirements:** Node.js v18+ · Ollama (optional, for semantic search)
|
|
165
|
+
**Requirements:** Node.js v18+ · Ollama (optional, for semantic search and enrichment) · Node.js v22.5+ for the optional SQLite backend
|
|
96
166
|
|
|
97
167
|
### 1. Install
|
|
98
168
|
|
|
@@ -135,7 +205,20 @@ npm run mcp:index
|
|
|
135
205
|
INDEXER_EMBEDDINGS=off npm run mcp:index
|
|
136
206
|
```
|
|
137
207
|
|
|
138
|
-
Lexical-only still
|
|
208
|
+
Lexical-only still scores loose recall@5 = 0.90 and strict symbolic MRR 0.81 on the benchmarks above — only the semantic (behavioural-query) channel needs embeddings.
|
|
209
|
+
|
|
210
|
+
**On a large monorepo, keep retrieval RAM flat with the SQLite backend:**
|
|
211
|
+
```bash
|
|
212
|
+
npm run mcp:index -- --use-sqlite
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Sharpen conceptual recall with local-LLM enrichment:**
|
|
216
|
+
```bash
|
|
217
|
+
ollama pull qwen2.5-coder:1.5b
|
|
218
|
+
npm run mcp:index -- --llm-enrichment
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The two options compose (`--use-sqlite --llm-enrichment`) and can be made permanent in `.graph-indexer.json` — see [Configuration](#configuration).
|
|
139
222
|
|
|
140
223
|
### 3. Configure your IDE
|
|
141
224
|
|
|
@@ -181,12 +264,99 @@ Lexical-only still achieves Recall@5 = 1.00 on all benchmarks above.
|
|
|
181
264
|
}
|
|
182
265
|
```
|
|
183
266
|
|
|
267
|
+
The server reads the backend from `.graph-indexer.json`, so the same launch config works for both the in-memory and SQLite indexes.
|
|
268
|
+
|
|
184
269
|
### 4. Add the agent system prompt
|
|
185
270
|
|
|
186
271
|
Copy [PROMPT.md](./PROMPT.md) into your AI agent's system prompt to instruct it to use graph-indexer tools instead of reading files directly.
|
|
187
272
|
|
|
188
273
|
---
|
|
189
274
|
|
|
275
|
+
## Storage backends
|
|
276
|
+
|
|
277
|
+
The same index, the same tools, the same ranking — two ways to hold the data. The MCP tools are written against a storage contract and never see which backend is active.
|
|
278
|
+
|
|
279
|
+
| Backend | Default | Resident RAM* | Dependencies | Best for |
|
|
280
|
+
| :--- | :--- | ---: | :--- | :--- |
|
|
281
|
+
| **In-memory** | ✅ | ~586 MB | none | Single packages and services; instant cold start. |
|
|
282
|
+
| **SQLite** | `--use-sqlite` | ~79 MB | none — uses Node's built-in `node:sqlite` | Monorepos / 1M+ LOC where holding every chunk in the heap would OOM. |
|
|
283
|
+
|
|
284
|
+
> *Resident set serving the **same** 50,000-chunk corpus, measured in isolated processes by
|
|
285
|
+
> [test/scale.mjs](test/scale.mjs). The in-memory engine keeps every chunk and the full inverted
|
|
286
|
+
> index in the heap, so its footprint grows with the codebase; the SQLite store keeps only the
|
|
287
|
+
> small file-level dependency graph resident and reads chunks, posting lists and vectors from disk
|
|
288
|
+
> on demand, so its footprint stays flat regardless of corpus size — an **87% smaller** resident
|
|
289
|
+
> set here, and the gap widens as the repo grows.
|
|
290
|
+
|
|
291
|
+
The SQLite backend adds **no external dependency**: `node:sqlite` ships inside Node (v22.5+). Lexical BM25 reads from an indexed `postings` table, symbols and call edges from indexed columns, and vectors live in the shared `code-index.embeddings.bin` — point reads are `pread` on demand. Because both backends feed the same fusion-and-boost ranker with deterministic tie-breaking, switching is purely an operational choice with **zero** quality trade-off (identical top-5 ids on the full benchmark).
|
|
292
|
+
|
|
293
|
+
### Vector search that stays fast at monorepo scale
|
|
294
|
+
|
|
295
|
+
Below 10k vectors the semantic channel runs an exact streaming scan of the bin (~20 ms worst
|
|
296
|
+
case). Above it, a **binary-quantized sketch** takes over: each vector's sign bits (768 dims →
|
|
297
|
+
96 bytes, 0.1 % of the float data) are kept in RAM; a query does a Hamming-distance pass over
|
|
298
|
+
packed Uint32 words, then exact-cosine rescores only the top candidates from disk. Measured on
|
|
299
|
+
synthetic corpora (warm):
|
|
300
|
+
|
|
301
|
+
| Corpus | Exact scan | Sketch | Speedup | Sketch RAM |
|
|
302
|
+
| ---: | ---: | ---: | ---: | ---: |
|
|
303
|
+
| 50,000 vectors | 104 ms | **5–11 ms** | ~20× | ~9 MB |
|
|
304
|
+
| 200,000 vectors | 519 ms | **11 ms** | ~36× | ~35 MB |
|
|
305
|
+
|
|
306
|
+
The sketch recovers the exact top-10 (10/10 in validation) because it rescores 2× the candidate
|
|
307
|
+
budget with true cosine before ranking. It is **append-aware**: daemon updates extend it by
|
|
308
|
+
scanning only the unseen tail of the bin, and a full re-index is detected by fingerprint and
|
|
309
|
+
triggers a rebuild. `test/scale.mjs` asserts hybrid queries stay interactive (<60 ms) at 50k
|
|
310
|
+
chunks on both backends.
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
npm run mcp:index -- --use-sqlite # writes code-index.db
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Live updates on both backends
|
|
317
|
+
|
|
318
|
+
The watch daemon keeps **whichever backend is configured** fresh, incrementally:
|
|
319
|
+
|
|
320
|
+
- **In-memory** — the daemon rewrites the JSON snapshot; a running MCP server watches the file and reloads it automatically.
|
|
321
|
+
- **SQLite** — each file save becomes one WAL transaction (`applyFileUpdate`): the file's old chunks, postings and call edges are replaced with exact BM25 bookkeeping, and new vectors are *appended* to the embeddings bin (O(changed chunks), never a full rewrite). Running MCP servers detect the commit via `PRAGMA data_version` on their next query and refresh themselves — no re-indexing, no restarts.
|
|
322
|
+
|
|
323
|
+
Daemon startup is also O(changed files): files older than the index artifact are skipped during the initial scan, so edits made while the daemon was down are picked up without re-parsing the whole repo.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Semantic enrichment
|
|
328
|
+
|
|
329
|
+
Embeddings match text proximity, not intent. A query like *"payment gateway webhook bottleneck"* misses the function that handles it if that function never spells out those words. Enrichment closes that gap.
|
|
330
|
+
|
|
331
|
+
With `--llm-enrichment`, the indexer routes every substantive **production-source chunk** (tests, specs and example trees are excluded — agents search for implementations) through a local LLM via Ollama, producing per chunk:
|
|
332
|
+
|
|
333
|
+
- a one-line **summary** in developer vocabulary, and
|
|
334
|
+
- a set of **concept tags** (e.g. `authentication, JWT, middleware`).
|
|
335
|
+
|
|
336
|
+
These ride **three** retrieval paths: the tags join the chunk's BM25 lexical document as high-IDF domain terms; the summary leads the code-payload embedding; and — decisive for natural-language queries — each enriched chunk gets a **second, summary-only vector**. A one-line behavioural query embeds far closer to a one-line summary than to hundreds of characters of code, so conceptual searches hit code that shares none of their words.
|
|
337
|
+
|
|
338
|
+
Enrichment is **incremental**: results are cached in `code-index.enrichment.json` keyed by content hash, so a re-index only sends new or changed code to the LLM (the embedding cache keys account for enrichment too, so nothing is re-embedded needlessly). Each run enriches up to `maxChunks` new chunks, ordered by file centrality (PageRank), and coverage accumulates across runs. The watch daemon re-attaches cached enrichment on every file save and live-enriches changed chunks. Generation is best-effort — if the model is unreachable the index is built without enrichment rather than failing.
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
ollama pull qwen2.5-coder:1.5b
|
|
342
|
+
npm run mcp:index -- --llm-enrichment
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The model, the file-selection ratio and the per-run cap are all configurable (see [Configuration](#configuration)). Enrichment is fully optional and leaves the default index byte-for-byte unchanged when disabled.
|
|
346
|
+
|
|
347
|
+
### LLM reranking (query time, opt-in)
|
|
348
|
+
|
|
349
|
+
Fusion resolves most queries, but a natural-language query can end in a near-tie between the
|
|
350
|
+
semantically right chunk and a lexically similar neighbour — a gap no static boost can close.
|
|
351
|
+
With `rerank.enabled` (or `search_code(..., rerank: true)`), the fused top-8 is shown to a local
|
|
352
|
+
LLM judge — one line per candidate — which reorders them. Measured effect on the strict semantic
|
|
353
|
+
channel: **rank-1 0.23 → 0.35, MRR 0.37 → 0.47, success@5 0.55 → 0.65**, with the symbolic
|
|
354
|
+
channel untouched (the judge only fires on natural-language queries, never on symbol lookups or
|
|
355
|
+
`exact_tokens` calls). Cost: one generation call (~1–2 s with `qwen2.5-coder:7b`). Best-effort —
|
|
356
|
+
any model failure preserves the original order.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
190
360
|
## MCP tools
|
|
191
361
|
|
|
192
362
|
### `search_code`
|
|
@@ -202,6 +372,7 @@ Hybrid BM25 + vector search over the index. The primary entry point for agents.
|
|
|
202
372
|
| `include_topology` | boolean | `true` | Include imports / used-by / calls |
|
|
203
373
|
| `min_score` | number | `0.3` | Minimum relevance threshold |
|
|
204
374
|
| `token_budget` | number? | auto | Cap total tokens returned |
|
|
375
|
+
| `rerank` | boolean? | config | LLM-judge rerank for natural-language queries (see `rerank.*` config) |
|
|
205
376
|
|
|
206
377
|
**`detail` levels:**
|
|
207
378
|
|
|
@@ -211,6 +382,8 @@ Hybrid BM25 + vector search over the index. The primary entry point for agents.
|
|
|
211
382
|
| `"smart"` (default) | ~150 tok | Signature + lines relevant to the query |
|
|
212
383
|
| `"full"` | ~300 tok | Signature + complete source body |
|
|
213
384
|
|
|
385
|
+
For purely conceptual queries with no lexical overlap, `"smart"` falls back to a structural skeleton — control-flow lines and call sites — so the agent always gets meaningful signal about what the code does, never a blind truncation.
|
|
386
|
+
|
|
214
387
|
---
|
|
215
388
|
|
|
216
389
|
### `resolve_symbol`
|
|
@@ -238,9 +411,10 @@ Returns the source code of a single chunk by its ID.
|
|
|
238
411
|
|
|
239
412
|
Signature + docstring + calls without the body. ~50 tokens vs ~300 for full.
|
|
240
413
|
|
|
241
|
-
| Parameter | Type | Description |
|
|
242
|
-
| :--- | :--- | :--- |
|
|
243
|
-
| `chunk_id` | string | ID from `search_code` results |
|
|
414
|
+
| Parameter | Type | Default | Description |
|
|
415
|
+
| :--- | :--- | :--- | :--- |
|
|
416
|
+
| `chunk_id` | string | required | ID from `search_code` results |
|
|
417
|
+
| `expand_calls` | boolean | `false` | Resolve each outgoing call's signature inline (~150 tok) instead of issuing a follow-up tool call per dependency |
|
|
244
418
|
|
|
245
419
|
---
|
|
246
420
|
|
|
@@ -280,7 +454,7 @@ Compact symbol map ordered by PageRank (most-imported files first). Orients agen
|
|
|
280
454
|
|
|
281
455
|
### `list_index_stats`
|
|
282
456
|
|
|
283
|
-
Index health snapshot: chunk count, file count, symbol table size, vector count, search mode, daemon status, index age.
|
|
457
|
+
Index health snapshot: storage backend, chunk count, file count, symbol table size, vector count, search mode, daemon status, index age.
|
|
284
458
|
|
|
285
459
|
---
|
|
286
460
|
|
|
@@ -292,32 +466,60 @@ Full bidirectional dependency topology for a file: what it imports and what impo
|
|
|
292
466
|
|
|
293
467
|
## Configuration
|
|
294
468
|
|
|
295
|
-
###
|
|
469
|
+
### `.graph-indexer.json`
|
|
296
470
|
|
|
297
|
-
|
|
298
|
-
| :--- | :--- | :--- |
|
|
299
|
-
| `MCP_PROJECT_ROOT` | `process.cwd()` | Project root directory |
|
|
300
|
-
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama API endpoint |
|
|
301
|
-
| `INDEXER_EMBEDDINGS` | — | Set to `off` to disable vector embeddings |
|
|
302
|
-
|
|
303
|
-
### Language selection
|
|
304
|
-
|
|
305
|
-
`init` saves your selection to `.graph-indexer.json` in the project root:
|
|
471
|
+
All persistent settings live in one file at the project root, written by `init` and read by the indexer, watcher and server:
|
|
306
472
|
|
|
307
473
|
```json
|
|
308
|
-
{
|
|
474
|
+
{
|
|
475
|
+
"languages": ["typescript", "javascript", "python"],
|
|
476
|
+
"storage": "sqlite",
|
|
477
|
+
"enrichment": {
|
|
478
|
+
"enabled": true,
|
|
479
|
+
"model": "qwen2.5-coder:1.5b",
|
|
480
|
+
"coreRatio": 0.15,
|
|
481
|
+
"maxChunks": 400
|
|
482
|
+
}
|
|
483
|
+
}
|
|
309
484
|
```
|
|
310
485
|
|
|
311
|
-
|
|
486
|
+
| Key | Default | Description |
|
|
487
|
+
| :--- | :--- | :--- |
|
|
488
|
+
| `languages` | all installed | Parsers to load. Valid keys: `typescript`, `javascript`, `python`, `go`, `rust`, `php`, `java`, `kotlin`, `csharp`, `ruby`, `css`. |
|
|
489
|
+
| `storage` | `"memory"` | `"memory"` (in-heap, zero-dependency) or `"sqlite"` (disk-backed). |
|
|
490
|
+
| `ollamaHost` | `"http://localhost:11434"` | Ollama endpoint (also settable via `OLLAMA_HOST`). |
|
|
491
|
+
| `embedModel` | `"nomic-embed-text"` | Embedding model (index and queries must use the same one). |
|
|
492
|
+
| `enrichment.enabled` | `false` | Run local-LLM enrichment during indexing. |
|
|
493
|
+
| `enrichment.model` | `"qwen2.5-coder:1.5b"` | Ollama model used for generation. |
|
|
494
|
+
| `enrichment.coreRatio` | `1.0` | Share of production files eligible (by PageRank). `1.0` = all; tests/examples are always excluded. |
|
|
495
|
+
| `enrichment.maxChunks` | `500` | Cap on **new** LLM calls per index run — the cache accumulates coverage across runs. |
|
|
496
|
+
| `enrichment.concurrency` | `12` | Parallel Ollama requests during enrichment. |
|
|
497
|
+
| `rerank.enabled` | `false` | LLM-judge reranking of natural-language queries (+50% semantic rank-1, ~1–2 s per NL query). |
|
|
498
|
+
| `rerank.model` | `"qwen2.5-coder:7b"` | Judge model. Quality matters: 7B measured a large gain where 1.5B measured ~none. |
|
|
499
|
+
| `rerank.topM` | `8` | Fused results shown to the judge (8 measured better than 10). |
|
|
500
|
+
|
|
501
|
+
### CLI flags
|
|
502
|
+
|
|
503
|
+
Flags override the config file for a single run:
|
|
504
|
+
|
|
505
|
+
| Flag | Equivalent |
|
|
506
|
+
| :--- | :--- |
|
|
507
|
+
| `--repo <dir>` | Index a directory other than the cwd |
|
|
508
|
+
| `--use-sqlite` | `"storage": "sqlite"` |
|
|
509
|
+
| `--llm-enrichment` | `"enrichment.enabled": true` |
|
|
510
|
+
| `--enrich-model <name>` | `"enrichment.model"` |
|
|
511
|
+
| `--enrich-max <n>` | `"enrichment.maxChunks"` for this run |
|
|
512
|
+
| `--enrich-concurrency <n>` | `"enrichment.concurrency"` for this run |
|
|
312
513
|
|
|
313
|
-
|
|
514
|
+
### Environment variables
|
|
314
515
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
516
|
+
| Variable | Default | Description |
|
|
517
|
+
| :--- | :--- | :--- |
|
|
518
|
+
| `MCP_PROJECT_ROOT` | `process.cwd()` | Project root directory |
|
|
519
|
+
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama API endpoint |
|
|
520
|
+
| `INDEXER_EMBEDDINGS` | — | Set to `off` to disable vector embeddings |
|
|
319
521
|
|
|
320
|
-
|
|
522
|
+
Re-run `init` at any time to change languages, or `init --all-languages` to enable every installed parser without the prompt.
|
|
321
523
|
|
|
322
524
|
---
|
|
323
525
|
|
|
@@ -337,23 +539,52 @@ If `.graph-indexer.json` is absent or has no `languages` key, all installed pars
|
|
|
337
539
|
| PHP | `.php` | function, class |
|
|
338
540
|
| CSS / SCSS | `.css`, `.scss` | rule sets |
|
|
339
541
|
|
|
542
|
+
Oversized "god classes" are split automatically: a class longer than ~200 lines is indexed as a compact skeleton header plus one independently searchable chunk per method, so a single `get_chunk` never blows the token budget and every method stays reachable by search.
|
|
543
|
+
|
|
340
544
|
---
|
|
341
545
|
|
|
342
546
|
## How it works
|
|
343
547
|
|
|
548
|
+
### Architecture
|
|
549
|
+
|
|
550
|
+
graph-indexer is organised around a small set of cohesive modules with a strict separation between *retrieval math*, *storage*, and *transport*:
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
config.mjs Resolves CLI flags > env > .graph-indexer.json into one config.
|
|
554
|
+
parser-utils.mjs Tree-sitter parsing, chunk extraction, Ollama embeddings.
|
|
555
|
+
search-core.mjs Shared retrieval math: tokenisation, BM25, RRF fusion + boosts
|
|
556
|
+
(query-adaptive NL weighting), PageRank, embedding cache keys.
|
|
557
|
+
The numbers are measured once here and reused.
|
|
558
|
+
core-engine.mjs MemoryGraphIndex — the default in-heap store — plus the
|
|
559
|
+
embeddings-bin codecs (write/read/append/scan) and the
|
|
560
|
+
binary-quantized vector sketch (Hamming prefilter + rescore).
|
|
561
|
+
sqlite-store.mjs SqliteGraphStore — the disk-backed store (node:sqlite) with
|
|
562
|
+
per-file incremental writes and data_version live refresh.
|
|
563
|
+
storage.mjs createStore(config) — picks a backend; documents the contract
|
|
564
|
+
both implement (searchHybrid, getChunk, applyFileUpdate, …).
|
|
565
|
+
enrichment.mjs Optional LLM summaries + concept tags, cached by content hash.
|
|
566
|
+
mcp-tools.mjs The eight tools, written against the storage contract only.
|
|
567
|
+
mcp-server.mjs Thin bootstrap: config → store → tools → stdio.
|
|
568
|
+
indexer.mjs Bootstrap indexer. watch-daemon.mjs Incremental updates.
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Because both stores call the identical `fuseAndRank` from `search-core`, the in-memory and SQLite backends are rank-consistent by construction, and a single change to the ranking math applies everywhere.
|
|
572
|
+
|
|
344
573
|
### Indexing
|
|
345
574
|
|
|
346
575
|
When you run `npm run mcp:index`:
|
|
347
576
|
|
|
348
577
|
1. **Parses** every source file with Tree-sitter — no regex, exact AST boundaries.
|
|
349
|
-
2. **Extracts chunks**: named functions, classes, methods, and exports
|
|
578
|
+
2. **Extracts chunks**: named functions, classes, methods, and exports — including Go/Rust `type`/`struct` declarations by their real name — each with docstring, parameters, return type, call sites, type references, **decorators/annotations** (`@Controller`, `@Injectable`, `@app.route`), and **inheritance edges** (`extends`/`implements`/base classes).
|
|
350
579
|
3. **Builds a dependency graph**: bidirectional import map so each chunk knows what it imports and what imports it.
|
|
351
|
-
4. **
|
|
352
|
-
5. **
|
|
580
|
+
4. **(Optional) Enriches** production-source chunks with LLM summaries + concept tags (cache-first; only new code pays an LLM call).
|
|
581
|
+
5. **Creates two indexes**: a BM25 inverted index (lexical) and per-chunk float32 embeddings via Ollama `nomic-embed-text` (optional) — two vectors per enriched chunk (code payload + summary-only).
|
|
582
|
+
6. **Persists** to the configured backend: `code-index.json` + `code-index.embeddings.bin`, or `code-index.db` + the same embeddings binary.
|
|
353
583
|
|
|
354
|
-
A background watcher daemon
|
|
355
|
-
|
|
356
|
-
|
|
584
|
+
A background watcher daemon applies changed files incrementally to **either** backend (JSON
|
|
585
|
+
snapshot rewrites for in-memory, per-file WAL transactions for SQLite). It respects
|
|
586
|
+
`.gitignore` and skips `node_modules`, build output, and dot-directories, so it never traverses
|
|
587
|
+
(or exhausts OS file-watcher limits on) dependency trees.
|
|
357
588
|
|
|
358
589
|
### Search pipeline
|
|
359
590
|
|
|
@@ -365,14 +596,20 @@ Tree-sitter AST
|
|
|
365
596
|
│
|
|
366
597
|
├──► Chunks ──► BM25 inverted index ──┐
|
|
367
598
|
│ └──► Float32 embeddings ──┤── RRF fusion ──► Ranked results ──► MCP tools
|
|
368
|
-
│ │
|
|
599
|
+
│ │ (search-core)
|
|
369
600
|
└──► Dependency graph ────────────────┘
|
|
370
601
|
```
|
|
371
602
|
|
|
372
|
-
1. **BM25 lexical search** — O(query_terms) over the inverted index.
|
|
373
|
-
2. **Vector search** — cosine
|
|
374
|
-
|
|
375
|
-
|
|
603
|
+
1. **BM25 lexical search** — O(query_terms) over the inverted index / `postings` table.
|
|
604
|
+
2. **Vector search** — exact cosine below 10k vectors; binary-sketch Hamming prefilter +
|
|
605
|
+
exact rescore above it (5–11 ms at 50k+, see [Storage backends](#storage-backends)). Both
|
|
606
|
+
code and summary vectors compete; hits fold onto their chunk through one shared finalizer.
|
|
607
|
+
3. **RRF fusion** merges both lists — lexical-led (1.5×/1.0×) for keyword queries,
|
|
608
|
+
vector-led (1.0×/1.6×) for natural-language ones.
|
|
609
|
+
4. **Score boosts**: exact name match +2.0× (df-gated, plural-aware), suffix match +1.4×,
|
|
610
|
+
path token +1.4×, test/example files demoted; on NL queries, boosts require semantic
|
|
611
|
+
agreement (the chunk must also be a vector candidate).
|
|
612
|
+
5. **(Opt-in) LLM rerank** of the fused top-8 on natural-language queries.
|
|
376
613
|
|
|
377
614
|
---
|
|
378
615
|
|
|
@@ -386,6 +623,8 @@ Tree-sitter AST
|
|
|
386
623
|
|
|
387
624
|
**Avoid catch-all utility files.** Files that mix unrelated utilities produce weaker per-chunk signals. One responsibility per module indexes more cleanly.
|
|
388
625
|
|
|
626
|
+
**Reach for the SQLite backend when the heap gets tight.** If indexing a large monorepo pushes Node toward its memory limit, switch to `--use-sqlite`; retrieval quality is identical and resident memory stops scaling with the codebase.
|
|
627
|
+
|
|
389
628
|
---
|
|
390
629
|
|
|
391
630
|
## Troubleshooting
|
|
@@ -394,19 +633,26 @@ Tree-sitter AST
|
|
|
394
633
|
- Verify Ollama is running: `curl http://localhost:11434/api/tags`
|
|
395
634
|
- Use lexical-only mode: `INDEXER_EMBEDDINGS=off npm run mcp:index`
|
|
396
635
|
|
|
636
|
+
**Indexing a huge repo runs out of memory**
|
|
637
|
+
- Build with `--use-sqlite` so chunks live on disk instead of the heap
|
|
638
|
+
- Confirm the active backend with `list_index_stats()`
|
|
639
|
+
|
|
640
|
+
**`--use-sqlite` reports node:sqlite is unavailable**
|
|
641
|
+
- The SQLite backend needs Node v22.5+; upgrade Node, or use the default in-memory backend
|
|
642
|
+
|
|
397
643
|
**Search returns irrelevant results**
|
|
398
644
|
- Use `exact_tokens` for known symbol names
|
|
399
|
-
-
|
|
400
|
-
-
|
|
645
|
+
- For conceptual queries, index with `--llm-enrichment` and embeddings enabled
|
|
646
|
+
- Check coverage with `list_index_stats()`; increase `top_k` to see more candidates
|
|
401
647
|
|
|
402
648
|
**MCP server won't connect**
|
|
403
649
|
- Verify `MCP_PROJECT_ROOT` points to the indexed directory
|
|
404
|
-
- Check
|
|
650
|
+
- Check the index exists: `ls -la code-index.json` (or `code-index.db`)
|
|
405
651
|
- Test manually: `npm run mcp:start`
|
|
406
652
|
|
|
407
653
|
**Results are stale after file changes**
|
|
408
|
-
- The daemon
|
|
409
|
-
-
|
|
654
|
+
- The daemon updates both backends live and running servers refresh automatically; check `list_index_stats()` for daemon status
|
|
655
|
+
- If the daemon isn't running, restart the MCP server (it spawns the daemon) or run `npm run mcp:index` manually
|
|
410
656
|
|
|
411
657
|
---
|
|
412
658
|
|
|
@@ -417,18 +663,30 @@ git clone https://github.com/MaquinaTech/graph-indexer.git
|
|
|
417
663
|
cd graph-indexer
|
|
418
664
|
npm install
|
|
419
665
|
npm run mcp:index # index this repo
|
|
420
|
-
npm run test # run test suite (lexical-only)
|
|
421
666
|
npm run mcp:start # start MCP server
|
|
422
667
|
```
|
|
423
668
|
|
|
669
|
+
### Tests
|
|
670
|
+
|
|
671
|
+
| Command | Scope |
|
|
672
|
+
| :--- | :--- |
|
|
673
|
+
| `npm run test` | Integration suite: indexing + loose hit-rate + token savings across 5 fixtures |
|
|
674
|
+
| `npm run test:eval` | Strict symbol-level accuracy (rank-1, precision, nDCG) |
|
|
675
|
+
| `npm run test:unit` | Pure helpers, chunk splitting |
|
|
676
|
+
| `npm run test:sqlite` | SQLite round-trip + rank consistency vs the in-memory engine |
|
|
677
|
+
| `npm run test:enrich` | HyDE enrichment, including the lexically-disjoint rank-1 flip |
|
|
678
|
+
| `npm run test:mcp` | End-to-end MCP server over stdio |
|
|
679
|
+
| `npm run test:scale` | Mock 50k-chunk corpus proving SQLite RAM stays bounded |
|
|
680
|
+
| `npm run test:all` | Every dependency-free suite above, no Ollama required |
|
|
681
|
+
|
|
424
682
|
---
|
|
425
683
|
|
|
426
684
|
## Security
|
|
427
685
|
|
|
428
|
-
graph-indexer runs locally and is air-gapped by default — its only outbound
|
|
429
|
-
|
|
430
|
-
`INDEXER_EMBEDDINGS=off`). It never executes the code it indexes,
|
|
431
|
-
artifacts contain source snippets, so keep them git-ignored (as `init` configures).
|
|
686
|
+
graph-indexer runs locally and is air-gapped by default — its only outbound calls
|
|
687
|
+
are to a local Ollama endpoint for embeddings and optional enrichment (skipped entirely with
|
|
688
|
+
`INDEXER_EMBEDDINGS=off` and without `--llm-enrichment`). It never executes the code it indexes,
|
|
689
|
+
and the index artifacts contain source snippets, so keep them git-ignored (as `init` configures).
|
|
432
690
|
|
|
433
691
|
See [SECURITY.md](SECURITY.md) for the full threat model and how to report a
|
|
434
692
|
vulnerability.
|