GraphRAG vs Neo4j: fraud-ring detection across accounts, devices, and behavior

Catch organized fraud rings in one Cypher query — shared devices and addresses (graph structure) plus behavioral similarity (vector hop) plus LLM risk scoring — and see what the same detection 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: fraud-ring detection across accounts, devices, and behavior will be staged for a preview — nothing runs until you click Run). No instance yet? Install free in ~30s.

Share

GraphRAG vs Neo4j: fraud-ring detection across accounts, devices, and behavior

This is a spoke of a 5-part GraphRAG cluster. For the core pattern, start at the hub: GraphRAG vs Neo4j: a knowledge base that answers multi-hop questions. The other spokes apply the same pattern to supply-chain impact analysis, customer-360 Q&A, and product recommendations. For the architecture argument in depth, read the pillar: GraphRAG vs Neo4j.

Objective

Single-account fraud rules — velocity limits, chargeback ratios, blocklists — are good at catching the lone bad actor and useless against an organized ring. A ring is several accounts that look ordinary one at a time but betray themselves in two dimensions at once: they share infrastructure (the same device fingerprint or shipping address) and they behave alike (near-identical transaction patterns). Neither signal alone is enough — plenty of honest people share a household device, and plenty of unrelated accounts happen to spend similarly. The fraud is in the intersection.

That intersection is exactly what a vector-only RAG stack cannot express. Vector search finds accounts that behave like a known-bad seed, but it can't enforce "and they also touched the same device." The graph knows the shared-device edge, but a plain graph engine has no notion of behavioral similarity. GraphRAG is the combination: semantic recall plus graph structure plus an LLM judgment, in one pass.

The textbook build is a four-system stack: Neo4j for the account/device/address graph, a vector database (Pinecone / Weaviate / pgvector) for behavioral embeddings, an orchestration layer to intersect their results, and an LLM API to score ring risk. SynapCores collapses that into one query on one engine: accounts carry their own behavioral embedding, [:SIMILAR_TO > 0.8] is a semantic hop inside Cypher, and llm_score(prompt, node) grades ring risk inline. This recipe builds a small fintech graph, flags a ring from a known-fraud seed, and then shows exactly what the Neo4j-stack version would take.

Step 1: Build the fraud graph

Accounts carry a behavioral embedding and a coarse risk flag, and they link to the devices they logged in from and the addresses they ship to. The embedding values here are 5-dimensional demo vectors so the recipe is fast and deterministic; in production you generate them with EMBED('the account's transaction-pattern summary') (384-dim by default). Accounts in the same ring point the same direction in embedding space; unrelated legit accounts point elsewhere.

// Ring cluster (behaviorally alike) — ACC-1001 is the known-fraud seed
MERGE (a1:FrAccount {id: "ACC-1001", name: "Dylan Reyes", risk: "confirmed_fraud",
                     embedding: [0.80, 0.10, 0.05, -0.05, 0.20]})
MERGE (a2:FrAccount {id: "ACC-1002", name: "Priya Nkemdirim", risk: "unreviewed",
                     embedding: [0.79, 0.11, 0.04, -0.06, 0.21]})
MERGE (a3:FrAccount {id: "ACC-1003", name: "Marcus Osei", risk: "unreviewed",
                     embedding: [0.81, 0.09, 0.06, -0.04, 0.19]})
MERGE (a4:FrAccount {id: "ACC-1004", name: "Lena Fournier", risk: "unreviewed",
                     embedding: [0.80, 0.12, 0.05, -0.05, 0.18]})

// Legit accounts, different behavior clusters — should NOT come back as ring
MERGE (b1:FrAccount {id: "ACC-2001", name: "Sofia Alvarez", risk: "clean",
                     embedding: [-0.30, 0.60, 0.20, 0.05, 0.10]})
MERGE (b2:FrAccount {id: "ACC-2002", name: "Tom Becker", risk: "clean",
                     embedding: [-0.31, 0.58, 0.22, 0.06, 0.09]})
MERGE (c1:FrAccount {id: "ACC-3001", name: "Wei Zhang", risk: "clean",
                     embedding: [0.05, -0.40, 0.65, 0.10, -0.17]})
MERGE (c2:FrAccount {id: "ACC-3002", name: "Amara Diallo", risk: "clean",
                     embedding: [0.06, -0.39, 0.64, 0.11, -0.18]})

// Shared infrastructure
MERGE (d9:FrDevice {id: "DEV-9F2A", fingerprint: "chrome-linux-x11-sameua"})
MERGE (d3:FrDevice {id: "DEV-3B1C", fingerprint: "ios-safari-legit"})
MERGE (d7:FrDevice {id: "DEV-7E44", fingerprint: "android-legit"})
MERGE (addrX:FrAddress {id: "ADDR-X", line: "44 Canal St, Unit 6, Newark NJ"})
MERGE (addrY:FrAddress {id: "ADDR-Y", line: "902 Oak Ave, Austin TX"})

// The ring shares ONE device and ONE drop address (structural signal)
MERGE (a1)-[:USED_DEVICE]->(d9)
MERGE (a2)-[:USED_DEVICE]->(d9)
MERGE (a3)-[:USED_DEVICE]->(d9)
MERGE (a1)-[:SHIPS_TO]->(addrX)
MERGE (a2)-[:SHIPS_TO]->(addrX)
MERGE (a3)-[:SHIPS_TO]->(addrX)

// ACC-1004 behaves like the ring but used its OWN device (no shared structure)
MERGE (a4)-[:USED_DEVICE]->(d7)
MERGE (a4)-[:SHIPS_TO]->(addrY)

// Legit accounts, ordinary independent infrastructure
MERGE (b1)-[:USED_DEVICE]->(d3)
MERGE (c1)-[:USED_DEVICE]->(d7);

Step 2: The vector half — behaviorally similar accounts

[:SIMILAR_TO > 0.8] walks from the known-fraud seed to accounts whose behavioral embeddings are near it. This is the "vector database" part — except it's a Cypher edge, not a separate service.

// "Which accounts behave like our confirmed-fraud seed ACC-1001?"
MATCH (seed:FrAccount {id: "ACC-1001"})-[:SIMILAR_TO > 0.8]->(similar:FrAccount)
RETURN similar.id AS id, similar.name AS name, similar.risk AS risk;

Expected: ACC-1002, ACC-1003, and ACC-1004 — all three share the seed's behavioral direction. The seed is excluded from its own neighbor set. The clean accounts in the other clusters (ACC-2001/2002, ACC-3001/3002) are behaviorally far, so they do not come back. Note that ACC-1004 behaves like the ring but, as we'll see, never touched the shared device — behavior alone would over-flag it.

Step 3: GraphRAG — shared infrastructure + behavior + LLM scoring, one query

Now the payoff. A real ring member must satisfy both signals: it shares the seed's device (structural edge) and it behaves like the seed (SIMILAR_TO). Then llm_score grades each survivor for organized-ring risk — all in a single statement.

// "Find accounts that BOTH share a device with the fraud seed AND behave like it,
//  then score each for organized-ring risk."
MATCH (seed:FrAccount {id: "ACC-1001"})-[:USED_DEVICE]->(:FrDevice)<-[:USED_DEVICE]-(other:FrAccount)
MATCH (seed)-[:SIMILAR_TO > 0.8]->(other)
WITH DISTINCT other,
     llm_score("Rate 0-1 how likely this account is part of an organized fraud ring, given it shares a device and shipping address with a confirmed-fraud account and mirrors its transaction behavior", other) AS ring_risk
WHERE ring_risk > 0.4
RETURN other.id AS id, other.name AS name, ring_risk
ORDER BY ring_risk DESC;

Expected: ACC-1002 (Priya Nkemdirim) and ACC-1003 (Marcus Osei) come back with high ring_risk scores (~0.8–0.9). Both share device DEV-9F2A and drop address ADDR-X with the seed and sit in the seed's behavioral cluster. Crucially, ACC-1004 is filtered out: it behaves like the ring (it passed Step 2) but it never used the shared device, so the structural half of the join excludes it — no false positive. The seed itself is excluded from its own SIMILAR_TO neighbor set, so you get the ring around the seed, not the seed.

What's happening

This one query is doing what the four-system stack does across four services:

  • Structural recall (the graph): the shared-device self-join (:FrDevice)<-[:USED_DEVICE]-(other) finds accounts wired to the same infrastructure as the seed.
  • Semantic recall (the vector hop): [:SIMILAR_TO > 0.8] keeps only the device-sharers that also behave like the seed — the `MATCH (seed)-[:SIMILAR_TO

    0.8]->(other)` line intersects the two signals in-engine, with no app-side set math.

  • LLM judgment (inline): llm_score(...) turns the surviving pair into a ranked risk list an analyst can action, without a separate model call per row in application code.

Shared device or similar behavior each over-flags on its own; their intersection is the ring. That intersection is a single Cypher statement here because the edges and the embeddings are the same data.

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

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

# 1) Neo4j holds the account/device/address graph (Cypher). It can find the
#    shared-device ring structurally — but Neo4j has NO embedding hop, so there
#    is no SIMILAR_TO and no behavioral filter here:
#    (Neo4j Cypher)
MATCH (seed:FrAccount {id:'ACC-1001'})-[:USED_DEVICE]->(:FrDevice)<-[:USED_DEVICE]-(other:FrAccount)
RETURN other.id                                  # -> [device-sharing account ids]

# 2) A separate vector DB (Pinecone/Weaviate/pgvector) holds the behavioral
#    embeddings. You query it out-of-band for behavioral neighbors of the seed:
index.query(vector=embed("ACC-1001 transaction pattern"), top_k=25)   # -> [similar ids]

# 3) Application/orchestration code intersects the two ID sets by hand, because
#    neither system can see the other's data — this is the ring:
ring_ids = set(device_share_ids) & set(behavior_ids)

# 4) A separate LLM API call scores each surviving account for ring risk:
for a in ring_ids: risk = openai.rank(prompt, a)   # N more network calls

# Then keep Neo4j and the vector DB in sync forever with a CDC/ETL job so a new
# login or a re-embedded behavior profile doesn't silently break the join.

Four systems, three-to-N network round trips per investigation, and a synchronization job standing between "an account changed" and "the ring query is correct." In SynapCores it was one Cypher query against one store — the graph edges and the behavioral vectors are the same rows. That is the whole argument of GraphRAG vs Neo4j.

Step 4: One more — widen the net to shared drop addresses

// "Accounts that ship to the seed's drop address AND behave like it —
//  the address-based view of the same ring."
MATCH (seed:FrAccount {id: "ACC-1001"})-[:SHIPS_TO]->(:FrAddress)<-[:SHIPS_TO]-(other:FrAccount)
MATCH (seed)-[:SIMILAR_TO > 0.8]->(other)
RETURN other.id AS id, other.name AS name, other.risk AS current_flag;

Expected: ACC-1002 and ACC-1003 again — they ship to ADDR-X and behave like the seed. This confirms the ring from a second structural angle, which is how analysts corroborate a signal before freezing accounts.

Cleanup (Optional)

MATCH (n:FrAccount) DETACH DELETE n;
MATCH (n:FrDevice) DETACH DELETE n;
MATCH (n:FrAddress) DETACH DELETE n;

Use it from your app or agent

  • In production: replace the demo vectors with EMBED('the account's transaction-pattern summary') when you MERGE accounts (384-dim), and add CREATE VECTOR INDEX for large populations. The Cypher queries above don't change. Re-embed an account when its behavior profile shifts and the ring query stays correct — no external vector store to resync.
  • 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 Step 3 to a real-time login/checkout hook and you have an inline ring check.
  • Why one engine: the shared-device edges and the behavioral embeddings are the same rows in the same store, so there is no vector-DB sync job and no app-side set-intersection to keep the structural and behavioral halves consistent. Add the LLM risk score with llm_score without leaving the query.

Key Concepts Learned

  • A fraud ring lives in the intersection of shared infrastructure (graph) and shared behavior (vectors) — either signal alone over-flags, as ACC-1004 demonstrates.
  • [:SIMILAR_TO > t] is an inline vector hop — the behavioral vector DB collapses into a Cypher edge, with a tunable threshold for precision/recall.
  • Intersecting structure and semantics is a single join here (MATCH ... USED_DEVICE ... + MATCH (seed)-[:SIMILAR_TO > 0.8]->(other)), not app-side set math across two databases.
  • llm_score(prompt, node) ranks ring risk inside the query — no per-row LLM loop in application code.
  • The Neo4j stack needs four systems and a sync job for the same result; here the graph and the vectors are one dataset.

Back to the hub: GraphRAG knowledge base · Supply-chain impact · Customer-360 Q&A · Product recommendations · Pillar: GraphRAG vs Neo4j

Tags

graphraggraphcyphersimilar_tofraudfintechneo4janomaly-detection

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