GraphRAG vs Neo4j: supply-chain impact analysis and part substitution
This is a spoke of a 5-part GraphRAG cluster. It applies the hub's core pattern — semantic recall + graph structure + LLM ranking in one Cypher query — to supply-chain risk. For the architecture argument in depth, read the pillar: GraphRAG vs Neo4j.
Objective
When a supplier goes dark — a fire, a port closure, a bankruptcy — the buyer's first two questions are structural and semantic: "Which of my products lose a part?" (a graph traversal) and "Is there a drop-in substitute part I can source elsewhere?" (a similarity search over part specs). Neither question alone is enough. Knowing a product is at risk without an alternative is just bad news; finding a spec-similar part without knowing it comes from a different, still-alive supplier is useless.
GraphRAG answers both in one shot. The textbook way to build it is a four-system stack: Neo4j for the bill-of-materials graph, a vector database for the part-spec embeddings, an orchestration layer to join them, and an LLM API to judge whether an alternative is really a viable substitute. Four services, several network hops per query, and a sync job to keep the graph and the vectors from drifting apart.
SynapCores collapses that into one query on one engine: parts carry their own
spec embedding, [:SIMILAR_TO > 0.8] is a semantic hop inside Cypher, and
llm_score(prompt, node) grades substitution viability inline. This recipe builds a
small bill-of-materials graph and answers "supplier SUP-9 just failed — what breaks,
and what do I swap in?" with a single Cypher statement — then shows you exactly what
the Neo4j stack version would take.
Step 1: Build the supply-chain graph
Suppliers SUPPLY parts, parts are USED_IN products. Each part carries a
5-dimensional demo embedding of its engineering spec so the recipe is fast and
deterministic; in production you generate them with EMBED('part spec sheet text')
(384-dim by default). Parts with similar specs share a vector direction (±0.02),
so a part and its viable alternative land in the same cluster.
// Parts — each with a spec embedding. Same cluster = interchangeable specs.
// Cluster A (deep-groove ball bearings): P-A1 and its alternative P-A2
MERGE (pA1:ScPart {id: "P-A1", name: "6205-2RS deep-groove ball bearing",
spec: "25x52x15mm, sealed, 14kN dynamic load",
embedding: [0.80, 0.10, 0.05, -0.05, 0.20]})
MERGE (pA2:ScPart {id: "P-A2", name: "6205-ZZ deep-groove ball bearing",
spec: "25x52x15mm, shielded, 14kN dynamic load",
embedding: [0.82, 0.08, 0.06, -0.04, 0.21]})
// Cluster B (1200V IGBT power modules): P-B1 and its alternative P-B2
MERGE (pB1:ScPart {id: "P-B1", name: "FZ600R12 IGBT power module",
spec: "1200V, 600A, half-bridge",
embedding: [-0.30, 0.60, 0.20, 0.05, 0.10]})
MERGE (pB2:ScPart {id: "P-B2", name: "SKM600GB IGBT power module",
spec: "1200V, 600A, half-bridge",
embedding: [-0.29, 0.61, 0.18, 0.06, 0.11]})
// Cluster C (die-cast housing) — unrelated specs, on its own axis
MERGE (pC1:ScPart {id: "P-C1", name: "Die-cast aluminum drive housing",
spec: "ADC12 alloy, 3.2kg, IP67",
embedding: [0.05, -0.40, 0.65, 0.10, -0.17]})
// Suppliers (one of them is about to fail)
MERGE (s9:ScSupplier {id: "SUP-9", name: "Baotou Precision Components", region: "Baotou, CN"})
MERGE (s3:ScSupplier {id: "SUP-3", name: "Nordwerk Bearings GmbH", region: "Schweinfurt, DE"})
MERGE (s7:ScSupplier {id: "SUP-7", name: "Fuji Power Devices K.K.", region: "Matsumoto, JP"})
MERGE (s5:ScSupplier {id: "SUP-5", name: "Éclair Semikron S.A.", region: "Nuremberg, DE"})
MERGE (s2:ScSupplier {id: "SUP-2", name: "Alcoa Castings Inc.", region: "Knoxville, US"})
// Products the parts go into
MERGE (prod1:ScProduct {id: "PRD-1", name: "Meridian-3 EV drive unit"})
MERGE (prod2:ScProduct {id: "PRD-2", name: "Aster-7 traction inverter"})
// Who supplies what
MERGE (s9)-[:SUPPLIES]->(pA1)
MERGE (s3)-[:SUPPLIES]->(pA2)
MERGE (s7)-[:SUPPLIES]->(pB1)
MERGE (s5)-[:SUPPLIES]->(pB2)
MERGE (s2)-[:SUPPLIES]->(pC1)
// Where the parts are used
MERGE (pA1)-[:USED_IN]->(prod1)
MERGE (pC1)-[:USED_IN]->(prod1)
MERGE (pB1)-[:USED_IN]->(prod2)
MERGE (pA2)-[:USED_IN]->(prod2);
Step 2: The vector half — find spec-similar parts
[:SIMILAR_TO > 0.8] walks from a seed part to the parts whose spec embeddings are
near it. This is the "vector database" part — except it's a Cypher edge, not a
separate service.
// "The 6205-2RS bearing (P-A1) is going out of supply.
// Which other parts have interchangeable specs?"
MATCH (seed:ScPart {id: "P-A1"})-[:SIMILAR_TO > 0.8]->(alt:ScPart)
RETURN alt.id AS id, alt.name AS name, alt.spec AS spec;
Expected: P-A2 (6205-ZZ bearing) — the other cluster-A part, same 25x52x15mm
14kN spec. The IGBT modules and the aluminum housing sit on different embedding
axes, so they don't come back. The seed is excluded from its own SIMILAR_TO
neighbor set, so you get alternatives, not the part you started from.
Step 3: GraphRAG — impact traversal + substitution + LLM viability, one query
Now the payoff. Start from the failing supplier, traverse the structural edges to
the products that lose a part (impact), then for each affected part take a semantic
hop to a spec-similar alternative supplied by a different supplier, and let
llm_score rate how viable a drop-in substitute it is — all in a single statement.
// "SUP-9 just failed. What products break, and what can I swap in from someone else?"
MATCH (s:ScSupplier {id: "SUP-9"})-[:SUPPLIES]->(p:ScPart)-[:USED_IN]->(prod:ScProduct)
MATCH (p)-[:SIMILAR_TO > 0.8]->(alt:ScPart)<-[:SUPPLIES]-(alt_s:ScSupplier)
WHERE alt_s <> s
WITH prod, p, alt, alt_s,
llm_score("Rate 0-1 how viable a drop-in substitute this alternative part is for the missing part, given interchangeable specs", alt) AS viability
RETURN prod.name AS at_risk_product,
p.name AS missing_part,
alt.name AS alternative,
alt_s.name AS alt_supplier,
alt_s.region AS alt_region,
viability
ORDER BY viability DESC;
Expected: one row — the Meridian-3 EV drive unit is at risk because it loses the 6205-2RS bearing (P-A1) from failed SUP-9; the viable substitute is the 6205-ZZ bearing (P-A2) from Nordwerk Bearings GmbH in Schweinfurt, DE — a different supplier on a different continent — with a high LLM viability score (~0.9, same 25x52x15mm 14kN spec). Vector-only search could surface the similar bearing, but it could not tell you which product breaks or that the alternative comes from a still-alive supplier — both of those live in the graph.
What's happening
Three capabilities compose in one statement:
- Impact (structure):
(:ScSupplier)-[:SUPPLIES]->(:ScPart)-[:USED_IN]->(:ScProduct)is a plain graph traversal — from the failing supplier to the products that lose a part. This is the part Neo4j does well on its own. - Substitution (semantics):
(p)-[:SIMILAR_TO > 0.8]->(alt)is a vector hop over the part-spec embeddings, finding parts that are engineering-interchangeable even if their part numbers share nothing. - Sourcing + judgment:
(alt)<-[:SUPPLIES]-(alt_s) WHERE alt_s <> swalks back into the graph to confirm the alternative comes from a different supplier, thenllm_scoregrades drop-in viability inline.
The traversal and the similarity hop read the same rows in the same store, so there is no set-intersection glue and no vector-DB sync job.
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 bill-of-materials graph (Cypher) — but Neo4j has no native
# embedding hop, so SIMILAR_TO does not exist. It can do the impact traversal,
# and it can list the parts a failed supplier feeds, but nothing more:
# (Neo4j Cypher)
MATCH (s:Supplier {id:'SUP-9'})-[:SUPPLIES]->(p:Part)-[:USED_IN]->(prod:Product)
RETURN prod.name, p.id # -> at-risk products + missing part ids
# 2) A separate vector DB (Pinecone/Weaviate/pgvector) holds the part-spec
# embeddings. You query it out-of-band for each missing part:
neighbors = index.query(vector=embed(part_spec), top_k=10) # -> [alt part ids]
# 3) Neither system knows the other's data, so application/orchestration code
# (LangChain) has to join back into Neo4j to find WHO supplies each alternative
# and drop any still coming from the failed supplier:
for alt_id in neighbors:
alt_supplier = neo4j.run("MATCH (as)-[:SUPPLIES]->(:Part {id:$id}) RETURN as", id=alt_id)
if alt_supplier.id == 'SUP-9': continue # app-side filter
# 4) A separate LLM API call scores substitution viability for each survivor:
for alt in survivors: score = openai.rank(prompt, alt) # N more network calls
# Then keep Neo4j and the vector DB in sync forever with a CDC/ETL job.
Four systems, several network round trips per question, and a synchronization job. In SynapCores it was one Cypher query against one store — the bill-of-materials edges and the part-spec vectors are the same data. That is the whole argument of GraphRAG vs Neo4j.
Step 4: One more — single-source risk across the catalog
// "Which parts are single-sourced but DO have a spec-similar alternative
// from another supplier we could qualify as a backup?"
MATCH (p:ScPart)-[:SIMILAR_TO > 0.8]->(alt:ScPart)<-[:SUPPLIES]-(alt_s:ScSupplier)
MATCH (p)<-[:SUPPLIES]-(cur_s:ScSupplier)
WHERE alt_s <> cur_s
RETURN p.name AS part, cur_s.name AS current_supplier,
alt.name AS backup_option, alt_s.name AS backup_supplier
ORDER BY part;
Cleanup (Optional)
MATCH (n:ScPart) DETACH DELETE n;
MATCH (n:ScSupplier) DETACH DELETE n;
MATCH (n:ScProduct) DETACH DELETE n;
Use it from your app or agent
- In production: replace the demo vectors with
EMBED('part spec sheet text')when you MERGE parts (384-dim), and addCREATE VECTOR INDEXfor large catalogs. The Cypher queries above don't change. - REST/SDK: every query here is a
POST /v1/graph/matchwith{"sql": "<cypher>"}, orclient.graph.cypher(...)in the Python SDK — one call, one auth token. Wire it to a supplier-status feed and the impact query becomes a real-time alert. - Why one engine: the bill-of-materials edges and the part-spec embeddings are the
same rows in the same store, so there is no vector-DB sync job and no app-side
join back to suppliers. Add the LLM viability step with
llm_scorewithout leaving the query.
Key Concepts Learned
- Supply-chain GraphRAG = impact traversal + spec-similarity + LLM judgment. Vector-only search finds similar parts but can't tell you what breaks or who still supplies the alternative — those are edges in a graph.
[:SIMILAR_TO > t]is an inline vector hop — spec-interchangeable parts surface as a Cypher edge, with a tunable threshold for precision/recall.- Walking back into the graph (
(alt)<-[:SUPPLIES]-(alt_s) WHERE alt_s <> s) turns a bare similarity result into an actionable, differently-sourced substitute. llm_score(prompt, node)grades substitution viability 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 the vectors are one dataset.
Next in this cluster: Knowledge base (hub) · Fraud-ring detection · Customer-360 Q&A · Product recommendations · Pillar: GraphRAG vs Neo4j