Skip to main content

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

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.
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

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:
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[]:
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():
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.
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

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.
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

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

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

StepLatency
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