GraphRAG vs Neo4j: a customer-360 knowledge graph that recommends the next action

Build a customer-360 knowledge graph in one database — an at-risk account's open ticket finds the resolution that fixed a similar past case, weighted by the account's tier and ARR, in a single Cypher query — and see what the same thing costs on a Neo4j + vector-DB stack.

All recipes· graph· 12 minutesintermediate✓ Certified v1.14.0-cecypher
Instance: localhost:8080

Opens your running SynapCores (GraphRAG vs Neo4j: a customer-360 knowledge graph that recommends the next action will be staged for a preview — nothing runs until you click Run). No instance yet? Install free in ~30s.

Share

GraphRAG vs Neo4j: a customer-360 knowledge graph that recommends the next action

This is a spoke of a 5-part GraphRAG cluster. Start with the hub, which teaches the core pattern — a knowledge base that answers multi-hop questions — then see the other spokes: fraud-ring detection, supply-chain impact analysis, and product recommendations. For the architecture argument in depth, read the pillar: GraphRAG vs Neo4j.

Objective

A support engineer opens an at-risk enterprise account and sees a fresh ticket: "double-charged on our latest invoice." The fastest useful answer isn't a knowledge-base article — it's what actually resolved a similar case before. But whether you drop everything for it depends on something the ticket text can't tell you: is this a $240k enterprise account or a free-tier trial?

Vector-only RAG can find tickets that read like this one, but it throws away the account's value and tier, because those are edges in a graph, not words in the ticket. Neo4j can hold the account graph, but it has no native embedding hop, so ticket-text similarity has to live in a separate vector database with an app-side merge on top. The classic build is a four-system stack: Neo4j for the customer/ticket/order graph, a vector DB (Pinecone / Weaviate / pgvector) for ticket embeddings, an orchestration layer (LangChain / LlamaIndex) to glue them, and an LLM API to grade how useful each past resolution is.

SynapCores collapses that into one query on one engine: tickets carry their own embedding property, [:SIMILAR_TO > 0.8] is a semantic hop inside Cypher (HNSW under the hood), and llm_score(prompt, node) grades each candidate inline. This recipe builds a small customer-360 graph and answers "what should we tell this at-risk account, and how much does it matter?" with a single Cypher statement — then shows you exactly what the Neo4j stack version would take.

Step 1: Build the customer-360 graph

Customers PLACED orders and OPENED tickets. Each C360Ticket carries an embedding of its text, a status ("open" / "resolved"), and — when resolved — the resolution that fixed it. Embedding values here are 5-dimensional demo vectors so the recipe is fast and deterministic; in production you generate them with EMBED('the ticket text') (384-dim by default). Tickets about the same topic share a vector direction, so [:SIMILAR_TO > 0.8] returns same-topic neighbors.

// Customers — tier + ARR are the structural signal vector search can't see
MERGE (c1:C360Customer {id: "CUST-77", name: "Northwind Logistics",
                        tier: "enterprise", arr: 240000, health: "at-risk"})
MERGE (c2:C360Customer {id: "CUST-12", name: "Acme Retail",
                        tier: "pro", arr: 24000, health: "healthy"})
MERGE (c3:C360Customer {id: "CUST-30", name: "Bluebird Studios",
                        tier: "free", arr: 0, health: "trial"})

// Tickets — topic A = billing, B = SSO/login, C = API rate limits.
// Same-topic vectors point the same direction (within +/-0.02).
MERGE (t1:C360Ticket {id: "TCK-1001", status: "open", resolution: "",
        text: "We were double-charged on our latest enterprise invoice and need a refund.",
        embedding: [0.80, 0.10, 0.05, -0.05, 0.20]})            // topic A (billing)
MERGE (t2:C360Ticket {id: "TCK-1002", status: "resolved",
        text: "Invoice showed a duplicate charge for the monthly plan.",
        resolution: "Issued a credit note for the duplicate line item and turned on invoice de-duplication.",
        embedding: [0.79, 0.12, 0.06, -0.06, 0.21]})            // topic A (billing)
MERGE (t3:C360Ticket {id: "TCK-1003", status: "resolved",
        text: "Billing overcharge after a mid-cycle plan upgrade.",
        resolution: "Applied a proration credit for the mid-cycle upgrade and documented the billing cutover.",
        embedding: [0.81, 0.09, 0.04, -0.04, 0.19]})            // topic A (billing)
MERGE (t4:C360Ticket {id: "TCK-2001", status: "resolved",
        text: "SAML SSO login hit a redirect loop after our IdP change.",
        resolution: "Updated the ACS URL in the SAML config to match the new IdP.",
        embedding: [-0.30, 0.60, 0.20, 0.05, 0.10]})            // topic B (SSO)
MERGE (t5:C360Ticket {id: "TCK-2002", status: "open", resolution: "",
        text: "Users can't log in via Okta SSO this morning.",
        embedding: [-0.31, 0.61, 0.19, 0.06, 0.11]})            // topic B (SSO)
MERGE (t6:C360Ticket {id: "TCK-3001", status: "resolved",
        text: "Hitting 429 rate-limit errors on the export API.",
        resolution: "Raised the account's API rate-limit tier and added retry-after backoff guidance.",
        embedding: [0.05, -0.40, 0.65, 0.10, -0.17]})           // topic C (API limits)

// Orders — structural account value
MERGE (o1:C360Order {id: "ORD-9001", item: "Annual enterprise license", amount: 240000})
MERGE (o2:C360Order {id: "ORD-9002", item: "Pro plan renewal", amount: 24000})

// Structure: customers OPENED tickets and PLACED orders
MERGE (c1)-[:OPENED]->(t1)
MERGE (c2)-[:OPENED]->(t2)
MERGE (c3)-[:OPENED]->(t3)
MERGE (c2)-[:OPENED]->(t4)
MERGE (c3)-[:OPENED]->(t5)
MERGE (c2)-[:OPENED]->(t6)
MERGE (c1)-[:PLACED]->(o1)
MERGE (c2)-[:PLACED]->(o2);

Step 2: The vector half — semantic recall alone

[:SIMILAR_TO > 0.8] walks from the at-risk account's open ticket to the tickets whose text is near it. This is the "vector database" part — except it's a Cypher edge, not a separate service.

// "Northwind's open ticket is about a double-charge.
//  What past tickets are semantically related?"
MATCH (c:C360Customer {id: "CUST-77"})-[:OPENED]->(open:C360Ticket {status: "open"})
MATCH (open)-[:SIMILAR_TO > 0.8]->(similar:C360Ticket)
RETURN similar.id AS id, similar.status AS status, similar.text AS text;

Expected: TCK-1002 (duplicate-charge invoice) and TCK-1003 (mid-cycle overcharge) — the other two billing tickets. The SSO and API-limit tickets point in different directions, so they don't come back. The seed ticket (TCK-1001) is excluded from its own SIMILAR_TO set, so you get related tickets, not the one you started from.

Step 3: GraphRAG — semantic recall + graph structure + LLM ranking, one query

Now the payoff. Start from the at-risk account's open ticket, hop to its semantically-similar resolved tickets, pull the account's tier and ARR off the graph, and let llm_score rank how useful each past resolution is for the open issue — all in a single statement.

// "For Northwind's open billing issue, which past resolution is most useful —
//  and how much does this account matter?"
MATCH (c:C360Customer {id: "CUST-77"})-[:OPENED]->(open:C360Ticket {status: "open"})
MATCH (open)-[:SIMILAR_TO > 0.8]->(past:C360Ticket)
WHERE past.status = "resolved"
WITH c, open, past,
     llm_score("Rate 0-1 how useful this past resolution is for resolving the open issue", past) AS usefulness
WHERE usefulness > 0.4
RETURN c.name AS account,
       c.tier AS tier,
       c.arr AS arr,
       open.text AS open_issue,
       past.resolution AS suggested_fix,
       usefulness
ORDER BY usefulness DESC;

Expected: two rows for Northwind Logistics (tier enterprise, ARR 240000) — the credit-note-plus-de-duplication fix (TCK-1002) and the proration-credit fix (TCK-1003) — each with a high LLM usefulness score, most-useful first. Vector-only RAG could surface the similar tickets, but it could not attach "enterprise, $240k, at-risk" — that lives in the graph, and it's what tells the support engineer this ticket jumps the queue.

What's happening

Three capabilities that normally live in three different systems run in one pass:

  • Semantic recall[:SIMILAR_TO > 0.8] is the vector-DB hop, expressed as a Cypher edge. It finds the resolved billing tickets by meaning, not keywords.
  • Graph structureMATCH (c:C360Customer)-[:OPENED]->(open) and the returned c.tier / c.arr are the account context. Similarity alone can't prioritize; the edges to the customer node do.
  • LLM rankingllm_score(...) grades each candidate resolution inside the query, so the most useful fix sorts to the top with no app-side LLM loop.

The WHERE past.status = "resolved" filter guarantees you only suggest fixes that actually closed a case, and the seed ticket's exclusion from its own SIMILAR_TO set means you never recommend the open ticket back to itself. One query, one store: the ticket embeddings and the customer edges are the same rows in the same database.

What this takes on a Neo4j + vector-DB stack (for reference — not executed)

The same capability on the classic stack spans four systems. This block is a reference comparison, not runnable SynapCores code:

# 1) Neo4j holds the customer/ticket/order graph (Cypher) — but Neo4j has no
#    native embedding hop, so SIMILAR_TO does not exist. You store the account
#    context and structure only:
#    (Neo4j Cypher)
MATCH (c:Customer {id:'CUST-77'})-[:OPENED]->(open:Ticket {status:'open'})
RETURN c.tier, c.arr, open.id

# 2) A separate vector DB (Pinecone/Weaviate/pgvector) holds the ticket
#    embeddings. You query it out-of-band for semantic neighbors of the open
#    ticket, then filter to resolved ones app-side:
index.query(vector=embed(open_text), top_k=10)   # -> [ticket ids]

# 3) Application/orchestration code (LangChain) joins the two result sets by
#    ticket id, because neither system can see the other's data — and re-attaches
#    the account tier/ARR from the Neo4j rows:
rows = join(vector_ids, graph_rows_by_ticket_id)

# 4) A separate LLM API call scores each surviving resolution for usefulness:
for r in rows: r.score = openai.rank(prompt, r.resolution)   # N more network calls

# Then keep Neo4j and the vector DB in sync forever with a CDC/ETL job so a new
# or edited ticket's text and its embedding never drift apart.

Four systems, three-to-N network round trips per ticket, and a synchronization job. In SynapCores it was one Cypher query against one store — the account graph and the ticket vectors are the same data. That is the whole argument of GraphRAG vs Neo4j.

Step 4: One more — combine structure and semantics freely

// "Resolved fixes semantically near Northwind's open billing ticket,
//  but only ones that worked for a paying (non-free) account."
MATCH (c:C360Customer {id: "CUST-77"})-[:OPENED]->(open:C360Ticket {status: "open"})
MATCH (open)-[:SIMILAR_TO > 0.8]->(past:C360Ticket {status: "resolved"})
MATCH (owner:C360Customer)-[:OPENED]->(past)
WHERE owner.tier <> "free"
RETURN owner.name AS solved_for, owner.tier AS tier, past.resolution AS fix;

Cleanup (Optional)

MATCH (n:C360Customer) DETACH DELETE n;
MATCH (n:C360Ticket) DETACH DELETE n;
MATCH (n:C360Order) DETACH DELETE n;

Use it from your app or agent

  • In production: replace the demo vectors with EMBED('ticket text') when you MERGE tickets (384-dim), and add CREATE VECTOR INDEX for large ticket volumes. The Cypher queries above don't change.
  • REST/SDK: every query here is a POST /v1/graph/match with {"sql": "<cypher>"}, or client.graph.cypher(...) in the Python SDK — one call, one auth token. Wire it into your helpdesk so the "suggested fix" panel is a single request.
  • Why one engine: the customer edges and the ticket embeddings are the same rows in the same store, so there is no vector-DB sync job and no app-side set-intersection to re-attach tier/ARR. Add the LLM usefulness step with llm_score without leaving the query.

Key Concepts Learned

  • GraphRAG = semantic recall + graph structure + LLM ranking. Vector-only RAG finds similar tickets but can't tell you the account is enterprise and at-risk.
  • [:SIMILAR_TO > t] is an inline vector hop — the vector DB collapses into a Cypher edge, with a tunable threshold for precision/recall.
  • Structural edges (OPENED, PLACED, tier, arr) turn a similar-ticket lookup into a prioritized next action — the thing vector search alone can't do.
  • llm_score(prompt, node) ranks candidate resolutions inside the query — no app-side LLM loop.
  • The Neo4j stack needs four systems and a sync job for the same result; here the graph and vectors are one dataset.

Next in this cluster: Fraud-ring detection · Supply-chain impact · Product recommendations · Hub: Knowledge base · Pillar: GraphRAG vs Neo4j

Tags

graphraggraphcyphersimilar_tocustomer-360supportsalesneo4j

Run this on your own machine

Install SynapCores Community Edition free, paste the SQL or Cypher above into the bundled web UI, and watch it run.

Download Free CE