Graph Neural Networks for Recommendation Systems
Collaborative filtering flattens relationships. Graph models keep them. A practical guide to building and serving GNN recommenders.
Your catalogue has millions of items. Most users have interacted with a handful. Matrix factorisation gives you something to ship, then cold-start and long-tail items quietly degrade the experience. Product asks why recommendations feel generic. Eng points at sparsity.
Recommendation problems are relational. Users touch items. Items sit in categories. Users resemble other users. Interactions arrive in time and context. Graph Neural Networks (GNNs) keep that structure instead of flattening everything into a matrix and hoping the signal survives.
Why graphs for recommendations?
Real systems are graphs whether you model them that way or not:
- Users interact with items
- Items belong to categories and carry attributes
- Users are similar to other users
- Items are related to other items
- Interactions happen in sequences and contexts
Collaborative filtering compresses those relationships. Graph approaches preserve them and let message passing move signal across neighbourhoods.
Graph construction
Entities as nodes
User nodes: Behavioural patterns, preference embeddings, sparse profile features you are allowed to use.
Item nodes: Category, brand, price band, content features, temporal properties.
Context nodes (optional): Tags, seasons, events, locations when they materially change relevance.
Relationships as edges
Explicit: Purchases, ratings, follows.
Implicit: Views, clicks, dwell time.
Derived: User-user or item-item similarity edges when they help, used carefully so you do not bake popularity bias deeper into the graph.
Edge attributes
Edges can carry weight, time, and context:
edge = {
"source": user_id,
"target": item_id,
"weight": interaction_strength,
"timestamp": interaction_time,
"context": {
"device": "mobile",
"session_length": 15.2,
"position": 3
}
}
Timestamps matter. A click from last year should not shout as loudly as one from this morning unless your product says otherwise.
Graph neural network architectures
Message passing
GNNs iteratively pass information between connected nodes:
- Message creation: Neighbours prepare messages
- Aggregation: A node collects messages (sum, mean, attention)
- Update: The node embedding updates from the aggregate
for node in graph.nodes:
messages = []
for neighbor in node.neighbors:
message = create_message(neighbor.embedding, edge_features)
messages.append(message)
aggregated = aggregate(messages)
node.embedding = update(node.embedding, aggregated)
Architectures teams actually use
1. Graph Convolutional Networks (GCN)
# Layer k embedding (schematic)
H^(k+1) = σ(D^(-1/2) A D^(-1/2) H^(k) W^(k))
Captures multi-hop neighbourhoods. Computationally approachable. Can over-smooth if you stack too many layers.
2. Graph Attention Networks (GAT)
Learn which neighbours matter. Helpful when degrees vary wildly and you want interpretable attention weights. Costs more than plain GCN.
3. LightGCN
Built for collaborative filtering. Drops some nonlinear transformations. Often trains faster and behaves well on sparse interaction graphs. A strong default starting point for many product recommenders.
4. Sampling-based models (PinSage-style)
For very large graphs, you cannot materialise full neighbourhoods. Random-walk sampling, pooling aggregation, and careful negative mining are how production systems stay within latency budgets. The lesson is less “copy Pinterest” and more “sample deliberately when the graph outgrows full-neighbour training.”
Training graph recommendation models
Loss functions
BPR optimises pairwise ranking of positive vs negative items.
Multi-task mixes click, purchase, and rating objectives when your funnel needs it:
L = α * L_click + β * L_purchase + γ * L_rating
Contrastive losses pull related nodes together and push unrelated ones apart. Useful when labels are noisy and structure is rich.
Negative sampling
Negatives make or break training:
- Random: Fast, often too easy
- Hard: Informative, expensive to mine
- In-batch: Convenient at scale
- Mixed: Usually what you end up with
Handling scale
Neighbour sampling for mini-batches. Graph partitioning across workers. Precomputed embeddings for parts of the graph that change slowly. Serving rarely runs full GNN inference on every request at large scale. You train, materialise embeddings, and retrieve with a vector index.
Advanced techniques worth the complexity
Temporal dynamics
Weight recent edges higher. Model sessions as short paths. If your product is time-sensitive, a static graph will quietly lie.
Multi-modal features
Fuse GNN embeddings with text and image embeddings when catalogue content carries signal collaborative history lacks. Cold items especially benefit.
Context-aware scoring
score = MLP([
user_embedding,
item_embedding,
context_embedding(time_of_day, device, location)
])
Explainability
Path-based and attention-based explanations help support and trust. “Users like you who bought X also bought Y” is not marketing fluff when the path is real.
Cold start
GNNs help when you can connect new users or items through attributes and content edges:
- New users: Attribute similarity and early interactions propagate signal
- New items: Content features and category links bootstrap the node before collaborative data arrives
Still measure cold-start cohorts separately. A global NDCG number will hide the pain.
Production shape
A practical serving path:
Data pipeline
↓
Graph construction and storage
↓
Feature engineering
↓
GNN training (e.g. PyTorch Geometric)
↓
Embedding materialisation (batch)
↓
Vector index
↓
Real-time ranking / re-ranking API
Evaluation
Ranking: NDCG@K, MRR, MAP.
Classification-style: Precision@K, Recall@K, Hit Rate@K.
Product: CTR, conversion, retention, diversity. Pick the ones your business actually cares about and hold them steady across experiments.
A/B testing notes
Recommendations change behaviour, which changes the graph, which changes future recommendations. Stratify carefully. Watch long-term metrics, not only next-day clicks. Popularity bias and feedback loops deserve explicit mitigations.
Common pitfalls
Over-smoothing: Too many layers, embeddings collapse. Prefer shallow stacks or residual connections.
Popularity bias: Message passing can amplify what is already popular. Debias in training or post-processing.
Feedback loops: Explore/exploit and randomisation keep the system from eating its own history.
Compute cost: Full-graph training is expensive. Sample, cache, update incrementally.
Getting started
- Convert your interaction matrix into a bipartite user-item graph
- Train a simple LightGCN or GCN baseline with 2–3 layers
- Compare against your current collaborative filtering baseline on the same splits
- Add features, temporal weighting, and better negatives only when the metrics move
- Serve via materialised embeddings and a vector index
Tools: PyTorch Geometric, DGL, Spektral if you are in TensorFlow-land, plus a graph store if your graph ops need it.
Closing
GNNs are not magic. They are a way to respect structure that matrix methods discard. The complexity pays off when recommendation quality is a product lever and you are willing to invest in training, evaluation, and serving discipline.
If recommendation quality is blocking growth in your product, discuss your use case. For adjacent production AI work on features and evaluation, see AI Product Engineering and evaluation suites.
Written by Syed Sartaj
Founder of Neurocell. Builds production AI for growth-stage and mid-market teams: agents, knowledge systems, and product features that ship and stay reliable.
Keep going

