GraphRAG vs Neo4j: product recommendations that blend co-purchase and semantics

Build cross-sell recommendations in one database — co-purchase traversal, semantic similarity, and LLM scoring in a single Cypher query — that keep working on brand-new SKUs with zero purchase history, 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: product recommendations that blend co-purchase and semantics will be staged for a preview — nothing runs until you click Run). No instance yet? Install free in ~30s.

Share

GraphRAG vs Neo4j: product recommendations that blend co-purchase and semantics

This is a spoke of a 5-part GraphRAG cluster. It applies the core pattern to cross-sell recommendations; the hub teaches the pattern itself — GraphRAG vs Neo4j: a knowledge base that answers multi-hop questions. Sibling spokes: fraud-ring detection, supply-chain impact analysis, and customer-360 Q&A. For the architecture argument in depth, read the pillar: GraphRAG vs Neo4j.

Objective

"People who bought X also bought Y" is the workhorse of e-commerce cross-sell, and graph traversal does it beautifully — walk from a product to its buyers, then to everything they bought. But co-purchase recommendations have a fatal blind spot: cold start. A SKU you added this morning has zero purchase history, so the co-purchase query returns nothing. Your newest, highest-margin products get the worst recommendations at the exact moment you most want to sell them.

GraphRAG fixes this by adding a semantic channel. Give every product an embedding of its name and description, and [:SIMILAR_TO > 0.8] finds products that are conceptually alike even with no shared buyers. The textbook way to build that blend is a four-system stack: Neo4j for co-purchase traversal, a vector database (Pinecone / Weaviate / pgvector) for embeddings, an orchestration layer (LangChain / LlamaIndex) to intersect and re-rank the two lists, and an LLM API for a cross-sell rationale. Four services and a sync job.

SynapCores collapses that into one query on one engine: products 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 cross-sell fit inline. This recipe builds a small product catalog with real buyers, recommends cross-sells for a viewed product — including a brand-new SKU nobody has bought yet — then shows you exactly what the Neo4j stack version would take.

Step 1: Build the recommendation graph

Products carry an embedding and are linked to the customers who bought them. Embedding values here are 5-dimensional demo vectors so the recipe is fast and deterministic; in production you generate them with EMBED('product name + description') (384-dim by default). Products in the same category point the same direction, so SIMILAR_TO groups them; different categories point elsewhere. Note SKU-102, a brand-new pour-over kettle with no BOUGHT edges — that is our cold-start test.

// Products — each with a semantic embedding of its name + description
// Cluster A (coffee gear): direction ~ [0.80, 0.10, 0.05, -0.05, 0.20]
MERGE (p1:RecProduct {sku: "SKU-100", name: "AeroPress Go Coffee Maker", price: 39.95,
                      embedding: [0.80, 0.10, 0.05, -0.05, 0.20]})
MERGE (p2:RecProduct {sku: "SKU-101", name: "Baratza Encore Burr Grinder", price: 149.00,
                      embedding: [0.81, 0.11, 0.04, -0.06, 0.21]})
MERGE (p3:RecProduct {sku: "SKU-102", name: "Fellow Stagg Gooseneck Pour-Over Kettle", price: 119.00,
                      embedding: [0.79, 0.09, 0.06, -0.04, 0.19]})  // BRAND NEW — no purchases

// Cluster B (yoga / fitness): direction ~ [-0.30, 0.60, 0.20, 0.05, 0.10]
MERGE (p4:RecProduct {sku: "SKU-200", name: "Manduka PRO Yoga Mat", price: 129.00,
                      embedding: [-0.30, 0.60, 0.20, 0.05, 0.10]})
MERGE (p5:RecProduct {sku: "SKU-201", name: "TheraBand Resistance Band Set", price: 24.99,
                      embedding: [-0.29, 0.61, 0.19, 0.06, 0.11]})

// Cluster C (kitchen cutlery): direction ~ [0.05, -0.40, 0.65, 0.10, -0.17]
MERGE (p6:RecProduct {sku: "SKU-300", name: "Wusthof Classic 8\" Chef's Knife", price: 169.95,
                      embedding: [0.05, -0.40, 0.65, 0.10, -0.17]})

// Customers
MERGE (c1:RecCustomer {id: "CUST-1", name: "Alice Nguyen"})
MERGE (c2:RecCustomer {id: "CUST-2", name: "Bob Martinez"})
MERGE (c3:RecCustomer {id: "CUST-3", name: "Carol Adeyemi"})

// Purchase history: customers BOUGHT products
MERGE (c1)-[:BOUGHT]->(p1)   // Alice: AeroPress + grinder
MERGE (c1)-[:BOUGHT]->(p2)
MERGE (c2)-[:BOUGHT]->(p1)   // Bob:   AeroPress + grinder
MERGE (c2)-[:BOUGHT]->(p2)
MERGE (c3)-[:BOUGHT]->(p1)   // Carol: AeroPress + chef's knife
MERGE (c3)-[:BOUGHT]->(p6);

Step 2: The vector half — cold-start recommendations

[:SIMILAR_TO > 0.8] walks from a viewed product to the products whose embeddings are near it. This is the "vector database" part — except it's a Cypher edge, not a separate service — and crucially it does not need any purchase history.

// "A shopper is viewing the AeroPress. What products are semantically similar?"
MATCH (seed:RecProduct {sku: "SKU-100"})-[:SIMILAR_TO > 0.8]->(rec:RecProduct)
RETURN rec.sku AS sku, rec.name AS name, rec.price AS price;

Expected: SKU-101 (Baratza grinder) and SKU-102 (Fellow Stagg kettle) — the two other coffee-gear products. SKU-102 comes back even though nobody has bought it — that is the cold-start win a co-purchase query cannot deliver. The yoga and cutlery products point in different directions, so they stay below the 0.8 threshold and don't appear. The seed (SKU-100) is excluded from its own SIMILAR_TO set, so you get related products, not the one being viewed.

Step 3: GraphRAG — co-purchase + semantics + LLM rationale

Now the payoff, in two clean moves. First the structural channel (co-purchase), then the semantic channel with an inline llm_score rationale — the GraphRAG combine. Ship both; a real cross-sell rail blends them.

(a) Co-purchase (structural): walk from the seed to its buyers, then to everything they also bought, and rank by how often it co-occurs.

// "People who bought the AeroPress also bought..."
MATCH (seed:RecProduct {sku: "SKU-100"})<-[:BOUGHT]-(:RecCustomer)-[:BOUGHT]->(co:RecProduct)
WHERE co <> seed
RETURN co.sku AS sku, co.name AS name, count(*) AS bought_together
ORDER BY bought_together DESC;

Expected: SKU-101 (Baratza grinder) with bought_together = 2 (Alice and Bob), then SKU-300 (Wusthof chef's knife) with bought_together = 1 (Carol). Strong structural signal — but notice the brand-new kettle SKU-102 is absent: it has no buyers, so co-purchase is blind to it.

(b) Semantic + llm_score rationale (the GraphRAG combine): hop to the seed's semantic neighbors and let the LLM grade each as a cross-sell inline.

// "Score the semantically similar products as cross-sells for an AeroPress viewer."
MATCH (seed:RecProduct {sku: "SKU-100"})-[:SIMILAR_TO > 0.8]->(rec:RecProduct)
WITH rec, llm_score("rate 0-1 how good a cross-sell this is for someone viewing the seed product", rec) AS fit
WHERE fit > 0.4
RETURN rec.sku AS sku, rec.name AS name, rec.price AS price, fit
ORDER BY fit DESC;

Expected: both SKU-101 (grinder) and SKU-102 (kettle) surface with a high fit (~0.8–0.9) — beans, grinder, kettle and press are one coffee ritual. The new SKU-102 gets recommended on day one, ranked by an LLM rationale, with no purchase history at all. Vector-only search could find the similar product, but it could not fold in "and two customers who bought the seed also bought the grinder" — that lives in the graph.

What's happening

Two recommendation channels, one query language, complementary blind spots:

  • Co-purchase (structural) is precise once a product has sales — it captures real, observed buying behavior ("also bought"). But it starts empty: a new SKU with no BOUGHT edges returns nothing, so your freshest inventory gets no cross-sell placement exactly when it needs the push.
  • Semantic similarity (vector) never has a cold-start gap — SKU-102 is recommendable the moment it's inserted with an embedding, before its first sale. On its own it can't see behavior, only meaning.

GraphRAG runs both and lets llm_score arbitrate — co-purchase carries proven winners, similarity backfills cold-start and long-tail SKUs. In SynapCores that's the same graph, the same query. On the classic stack it's two different databases whose results an application has to reconcile by hand.

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

Neo4j does the co-purchase traversal well. But it has no native embedding hop, so the cold-start half lives in a separate vector DB, and your app has to blend the two lists and call an LLM for the rationale. This block is a reference comparison, not runnable SynapCores code:

# 1) Neo4j holds co-purchase structure (Cypher) — but there is no SIMILAR_TO,
#    so it can only recommend products that already have buyers:
#    (Neo4j Cypher)
MATCH (seed:Product {sku:'SKU-100'})<-[:BOUGHT]-(:Customer)-[:BOUGHT]->(co:Product)
WHERE co <> seed
RETURN co.sku, count(*) AS bought_together ORDER BY bought_together DESC
#    -> misses SKU-102 entirely (no purchase history = cold start)

# 2) A separate vector DB (Pinecone/Weaviate/pgvector) holds the embeddings.
#    You query it out-of-band for semantic neighbors — the cold-start channel:
index.query(vector=embed("AeroPress Go Coffee Maker"), top_k=10)   # -> [skus incl. SKU-102]

# 3) Application/orchestration code (LangChain) merges the co-purchase list and the
#    similarity list, because neither system can see the other's data:
recs = blend(copurchase_skus, similar_skus)   # de-dupe, weight, rank

# 4) A separate LLM API call generates the cross-sell rationale per candidate:
for sku in recs: fit = openai.rank(prompt, sku)   # N more network calls

# Then keep Neo4j and the vector DB in sync forever with a CDC/ETL job on every
# new SKU and every new order.

Four systems, a blend step in application code, N LLM round trips, and a sync job. In SynapCores it was one Cypher query against one store — the purchase graph and the product embeddings are the same data. That is the whole argument of GraphRAG vs Neo4j.

Step 4: One more — cross-sell a brand-new SKU from its semantic twins' buyers

// "The new pour-over kettle has no sales yet. What has sold to buyers of its
//  closest semantic matches? (cold-start cross-sell via similarity + behavior)"
MATCH (new:RecProduct {sku: "SKU-102"})-[:SIMILAR_TO > 0.8]->(twin:RecProduct)<-[:BOUGHT]-(cust:RecCustomer)
MATCH (cust)-[:BOUGHT]->(also:RecProduct)
WHERE also <> new
RETURN also.sku AS sku, also.name AS name, count(*) AS signal
ORDER BY signal DESC;

Cleanup (Optional)

MATCH (n:RecProduct) DETACH DELETE n;
MATCH (n:RecCustomer) DETACH DELETE n;

Use it from your app or agent

  • In production: replace the demo vectors with EMBED('product name + description') when you MERGE products (384-dim), and add CREATE VECTOR INDEX for large catalogs. 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.
  • Why one engine: the purchase edges and the product embeddings are the same rows in the same store, so there is no vector-DB sync job and no app-side blend of two result lists. Add an LLM rationale with llm_score without leaving the query.

Key Concepts Learned

  • Co-purchase recommendations cold-start to empty. A new SKU with no buyers gets no structural recs — exactly when you most want to sell it.
  • [:SIMILAR_TO > t] is an inline vector hop that needs no purchase history, so it recommends brand-new SKUs on day one — the cold-start fix.
  • GraphRAG = structure + semantics + LLM ranking in one query. Co-purchase carries proven winners; similarity backfills cold-start; llm_score arbitrates inline.
  • The Neo4j stack needs four systems, an app-side blend, and a sync job for the same result; here the purchase graph and the embeddings are one dataset.

More in this cluster: Knowledge-base hub · Fraud-ring detection · Supply-chain impact · Customer-360 Q&A · Pillar: GraphRAG vs Neo4j

Tags

graphraggraphcyphersimilar_torecommendationse-commercecross-sellneo4j

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