> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
> Use this file to discover all available pages before exploring further.

# GraphRAG

> LLM-to-Cypher retrieval with Neo4j + Memgraph and Hybrid (vector + graph) fusion

# GraphRAG

## When to use a graph instead of vectors

Vector RAG is great for **semantic similarity**: "find documents about X". It struggles with **relationship reasoning**:

* "Which projects does Alice's direct reports work on?"
* "How many bugs has team B closed in the last 30 days?"
* "Trace this transaction's path through the ledger."

Those questions need a graph database. GraphRAG generates a Cypher query from the natural-language question, runs it against Neo4j or Memgraph, and returns the rows.

Combine the two via `HybridRetriever` for the best coverage.

## Architecture

```
                              ┌──────────────────────────────┐
   user question  ──────────▶│  GraphRAGRetriever            │
                              │   1. get live schema (labels, │
                              │      relationship types,      │
                              │      property keys)           │
                              │   2. LLM ➜ Cypher              │
                              │   3. ensure LIMIT              │
                              │   4. run Cypher                │
                              └──────────┬───────────────────┘
                                         │
                                         ▼
                              ┌──────────────────────────────┐
                              │       CypherStore             │
                              │   neo4j-driver or compatible  │
                              └──────────────────────────────┘
```

## `CypherStore`

Low-level interface for any graph DB that speaks the Cypher / Bolt protocol.

```typescript theme={null}
interface CypherStore {
  readonly providerId: string;
  connect(): Promise<void>;
  runCypher(cypher: string, params?: Record<string, unknown>): Promise<CypherRecord[]>;
  getSchema(): Promise<CypherSchema>;
  close(): Promise<void>;
}

interface CypherRecord {
  values: Record<string, unknown>;   // column name -> value
}

interface CypherSchema {
  nodeLabels: string[];
  relationshipTypes: string[];
  propertyKeys?: string[];
}
```

### `Neo4jCypherStore`

```typescript theme={null}
import { Neo4jCypherStore } from "@agentium/core";

const store = new Neo4jCypherStore({
  uri: process.env.NEO4J_URI ?? "bolt://localhost:7687",
  username: process.env.NEO4J_USER ?? "neo4j",
  password: process.env.NEO4J_PASSWORD ?? "neo4j",
  database: "neo4j",                   // optional, default is "neo4j"
});
await store.connect();
```

**Requires:** `npm install neo4j-driver` (already an optional peer dep of `@agentium/core`).

Defaults pull from `NEO4J_URI`, `NEO4J_USER`, `NEO4J_PASSWORD` environment variables.

### `MemgraphCypherStore`

Memgraph speaks the same Bolt protocol as Neo4j, so the adapter just sub-classes:

```typescript theme={null}
import { MemgraphCypherStore } from "@agentium/core";

const store = new MemgraphCypherStore({
  uri: process.env.MEMGRAPH_URI ?? "bolt://localhost:7687",
  username: process.env.MEMGRAPH_USER,
  password: process.env.MEMGRAPH_PASSWORD,
});
```

Differences from Neo4j: Memgraph defaults to no auth (`username: ""`). It's faster for streaming analytics and ships some unique procedures (e.g. `mgps.*`).

### `runCypher(cypher, params?)`

Execute any Cypher query, return rows as `CypherRecord[]`:

```typescript theme={null}
const rows = await store.runCypher(
  "MATCH (p:Person {name: $name})-[:KNOWS]->(other) RETURN other.name AS knows",
  { name: "Alice" },
);
for (const r of rows) console.log(r.values.knows);
```

Always parameterize. Never string-interpolate user input into Cypher.

### `getSchema()`

Returns the live schema by calling `db.labels()`, `db.relationshipTypes()`, `db.propertyKeys()`:

```typescript theme={null}
const schema = await store.getSchema();
console.log(schema.nodeLabels);          // ["Person", "Project"]
console.log(schema.relationshipTypes);   // ["KNOWS", "WORKS_ON"]
console.log(schema.propertyKeys);        // ["name", "email", "title"]
```

This is what `GraphRAGRetriever` reads to ground the LLM's Cypher generation.

## `GraphRAGRetriever`

LLM-to-Cypher with schema-aware prompting.

```typescript theme={null}
import { GraphRAGRetriever, Neo4jCypherStore, openai } from "@agentium/core";

const store = new Neo4jCypherStore({ /* ... */ });
await store.connect();

const retriever = new GraphRAGRetriever({
  store,
  model: openai("gpt-4o"),
  maxRecords: 25,             // default 25 - auto-appends LIMIT 25 if missing
  systemPrompt: undefined,    // optional override - see below
});

const result = await retriever.retrieve("Who works on the Atlas project?");
console.log(result.cypher);   // "MATCH (p:Person)-[:WORKS_ON]->(:Project {name: 'Atlas'}) RETURN p.name LIMIT 25"
console.log(result.text);     // "name=Alice\nname=Bob"
```

### `retrieve(query)` return shape

```typescript theme={null}
interface GraphRAGResult {
  cypher: string;          // the Cypher the model generated (post-LIMIT-injection)
  records: CypherRecord[]; // raw rows from the DB
  text: string;            // plain-text rendering of records (great as RAG context)
}
```

### Default system prompt

```
You convert a natural-language question into a Cypher query that runs against a Neo4j-compatible graph.
Rules:
1. Use ONLY the labels and relationship types listed in the schema.
2. Return only Cypher - no prose, no markdown fences.
3. Always end with a LIMIT clause (default 25 rows).
4. Prefer MATCH ... RETURN over WRITE operations.
```

Pass `systemPrompt` to override for domain-specific instructions (e.g. "use the German names for nodes" or "always project full names + emails").

### Safety: automatic `LIMIT`

If the model omits `LIMIT`, `GraphRAGRetriever` appends one with the configured `maxRecords`. This is the difference between a runaway query and a useful one.

For destructive operations (CREATE, DELETE, MERGE, SET), the default system prompt says "prefer MATCH ... RETURN". For an extra hard fence, run a Cypher parser yourself or use a read-only Neo4j user.

### Markdown fence stripping

Models sometimes wrap their Cypher in `cypher ...` fences despite being told not to. The retriever strips that automatically.

## `HybridRetriever`

Compose vector search + graph search + optional rerank into a single retrieval pipeline.

```typescript theme={null}
import {
  GraphRAGRetriever,
  HybridRetriever,
  InMemoryVectorStore,
  Neo4jCypherStore,
  OpenAIEmbedding,
  openai,
  CohereReranker,
} from "@agentium/core";

const vector = new InMemoryVectorStore(new OpenAIEmbedding());
await vector.upsert("notes", { id: "1", content: "Alice manages Bob and Carol." });
await vector.upsert("notes", { id: "2", content: "Project Atlas is shipping in Q4." });

const graphStore = new Neo4jCypherStore({ /* ... */ });
await graphStore.connect();
const graph = new GraphRAGRetriever({ store: graphStore, model: openai("gpt-4o") });

const hybrid = new HybridRetriever({
  vector: { store: vector, collection: "notes", topK: 10 },
  graph: { retriever: graph },
  rerank: new CohereReranker(),
  topK: 5,
});

const results = await hybrid.retrieve("Who works on Atlas?");
for (const r of results) {
  console.log(`[${r.source}] ${r.score.toFixed(3)}  ${r.content}`);
}
```

### Algorithm

1. **Parallel fan-out:** vector search + graph retrieval run concurrently.
2. **Score normalization:** vector cosine scores (0–1) and graph row-rank scores (1 - rank/N) are unioned.
3. **Reciprocal Rank Fusion:** combine the two ranked lists with `k=60` (the standard RRF constant). Each result keeps the better of its vector or graph rank, plus a small bonus for appearing in both.
4. **Optional rerank:** if a `Reranker` is configured, the top `topK * 3` fused results are passed to it for a final reorder.

### `HybridResult`

```typescript theme={null}
interface HybridResult {
  source: "vector" | "graph"; // which retriever surfaced this
  id: string;                  // unique id, prefixed with v: or g:
  content: string;
  score: number;               // RRF or rerank score
}
```

### Config

```typescript theme={null}
interface HybridRetrieverConfig {
  vector?: { store: VectorStore; collection: string; topK?: number };
  graph?: { retriever: GraphRAGRetriever };
  rerank?: Reranker;
  topK?: number;     // default 10
}
```

Either `vector` or `graph` may be omitted — `HybridRetriever` becomes a one-side pipeline. Useful when you want to keep the rerank-and-fuse boilerplate but only have one source.

## Performance characteristics

| Step                                         | Latency |
| -------------------------------------------- | ------- |
| `store.getSchema()` (cached at the DB level) | \~5ms   |
| LLM Cypher generation (GPT-4o-mini)          | \~600ms |
| `runCypher` (small Neo4j)                    | \~10ms  |
| RRF fusion                                   | \< 1ms  |
| Rerank (Cohere, 30 candidates)               | \~200ms |

Total Hybrid retrieve(): \~800ms for typical queries.

## Best practices

* **Cache the schema** at startup if your graph schema is stable. Read it once and pass it into `GraphRAGRetriever` via a custom `systemPrompt`.
* **Use a read-only DB user.** The LLM should never have credentials that can DROP nodes.
* **Cap costs.** Set `maxRecords` low (10-25) — the LLM can ask for more by paginating in subsequent calls if it really needs them.
* **Log all generated Cypher.** Add a hook that emits `result.cypher` to your observability stack so you can spot prompt regressions.
* **Pair with vector for breadth.** Pure GraphRAG misses semantic matches that aren't in the graph. Pure vector misses relationship answers. Hybrid is almost always the right choice in production.

## See also

* [Embeddings](/knowledge/embeddings) — the vector half
* [Reranking](/features/reranking) — the optional final step
* [Vector Stores](/knowledge/vector-stores) — what to plug into `vector.store`
