How to build one recommendation system that can work with different types of items instead of creating a new algorithm every time.
Introduction
Recommendation systems often start simple.
You may have a few items and a simple rule to decide which item to show first.
For example:
Show items that are similar to what the user is currently viewing.
But as the product grows, recommendations become more complicated.
The system may need to consider:
What the user is looking for
Past interactions
Current context
Related concepts
Requirements
Availability
Quality
Similarity
Business rules
At this point, a simple "find similar items" query may not be enough.
This is where a graph database like Neo4j can help.
Neo4j stores the connections between different things. Your recommendation engine can then use those connections to find and rank useful items.
The main idea of this article is simple:
Do not build a separate recommendation system for every type of item. Build one flexible recommendation pipeline that uses evidence from the graph.
1. Why Recommendations Become Difficult
A good recommendation usually depends on more than one signal.
For example, an item may be relevant because:
It matches the user's current need.
It is related to something the user interacted with before.
It meets an important requirement.
It is similar to another useful item.
It is currently available.
A graph database is useful because it can store these connections naturally.
However, there is an important point to understand:
Neo4j is not the recommendation algorithm.
Neo4j stores and finds connected information.
Your recommendation engine decides:
Which connections matter
Which items are valid
How relevant each item is
What order the items should appear in
2. The Recommendation Pipeline
A recommendation system can be divided into these stages:
Request – What is the user asking for?
Context – What information should influence the recommendation?
Candidate Generation – Which items might be relevant?
Evidence – Why is each item connected to the request?
Features – How strong are those signals?
Filtering – Which items should be removed?
Scoring – How relevant is each valid item?
Ranking – What order should the items appear in?
In simple terms:
Request
↓
Context
↓
Find possible items
↓
Collect evidence
↓
Remove invalid items
↓
Score items
↓
Rank items
↓
Recommendations
Keeping these steps separate makes the system easier to understand, test, and improve.
3. Use Neo4j as the Connected Data Layer
A simple way to think about the architecture is:
Neo4j
↓
Connected data
↓
Find candidates
↓
Collect evidence
↓
Recommendation engine
↓
Ranked results
Neo4j can store connections between:
Users
Items
Concepts
Requirements
Interactions
Rules
Categories
The exact type of data does not matter.
What matters is that the relationships between things have meaning.
For example:
User
↓ INTERESTED_IN
Topic
↓ RELATED_TO
Article
These connections can help the system understand why an article may be useful.
4. Finding Candidates Is Not the Same as Ranking Them
The first job of the system is to find possible recommendations.
This is called candidate generation.
Imagine the system uses several methods to find items:
User intent
↓
A B C
Item attributes
↓
C D E
Past history
↓
E F
Similarity
↓
B G H
Together, these create a candidate list:
A B C D E F G H
At this stage, the system should not decide which item is best.
Its only job is to find as many useful possibilities as possible.
Later stages will decide:
Which items are valid
Which signals are important
Which items should appear first
This separation is useful because you can add new ways to find candidates without changing the rest of the system.
5. Where Cypher Fits
Cypher is Neo4j's query language.
It can be used to find useful patterns in the graph.
For example:
MATCH (context:Context)-[:RELATES_TO]->(signal:Signal)
MATCH (item:Item)-[:ADDRESSES]->(signal)
RETURN item
In simple terms, this query says:
Find items that address signals related to the current context.
The architecture can look like this:
Application
↓
Candidate Generator
↓
Cypher Query
↓
Neo4j
↓
Candidates + Evidence
The recommendation service should control the queries it uses.
Avoid making automatically generated or unpredictable Cypher queries the main part of the recommendation architecture.
6. Graph Distance Does Not Always Mean Relevance
A common approach is:
Start from the current item
↓
Move through the graph
↓
Find nearby items
↓
Rank by distance
Graph distance can be useful when finding candidates.
But:
A shorter path does not always mean a better recommendation.
For example, an item connected through a very meaningful relationship may be more useful than an item that is technically closer in the graph.
The recommendation system needs to understand:
What does this relationship mean?
Not just:
How close are these two nodes?
7. Turn Graph Connections Into Evidence
Instead of simply saying:
Relationship found
↓
Add 0.25 to the score
Use a clearer process:
Graph connection
↓
Evidence
↓
Feature
↓
Score contribution
For example:
Evidence:
The item is connected to a concept related to the user's request.
Feature:
context_match = 1.0
Score:
context_match × configured weight
This approach makes the system easier to explain.
Instead of only returning a score, you can explain:
This item ranked highly because it strongly matched the user's context and requirements.
8. Give Relationships Clear Meaning
Not every relationship should have the same value.
For example:
SUPPORTS
Strong positive signal
ADDRESSES
Strong relevance signal
REQUIRES
Requirement or eligibility signal
RELATED_TO
General contextual signal
SIMILAR_TO
Similarity signal
The relationship names will depend on your product.
The important idea is:
Keep the meaning of relationships in one central place.
For example:
relationships:
SUPPORTS:
feature: intent_match
weight: 0.35
ADDRESSES:
feature: requirement_match
weight: 0.30
SIMILAR_TO:
feature: similarity
weight: 0.15
RELATED_TO:
feature: contextual_relevance
weight: 0.10
This makes it easier to adjust the system without changing recommendation logic everywhere in the code.
9. Hard Rules and Preferences Are Different
A hard rule means the item cannot be recommended if it does not meet the condition.
For example:
The item is unavailable
↓
Remove it
A preference means the item can still be recommended, but some items should rank higher.
For example:
A newer item
↓
Gets a higher score
These should stay separate.
Otherwise, an item that breaks an important rule could still appear at the top because it performed well in other areas.
10. Start With Simple, Rule-Based Scoring
You do not need machine learning from the beginning.
A simple scoring model may look like this:
score(item) =
intent_match × W1
+ requirement_fit × W2
+ context_match × W3
+ similarity × W4
+ quality × W5
- redundancy × W6
The weights should be configurable.
This gives you:
Predictable results
Easier testing
Easier debugging
Clear explanations
Simple tuning
Machine learning can be added later if needed.
11. Scoring and Ranking Are Not Exactly the Same
Suppose three items have these scores:
A = 0.91
B = 0.88
C = 0.83
Normally, A would appear first.
But the final ranking layer may also consider:
Diversity
Reducing duplicate recommendations
Freshness
Availability
Business rules
For example, the system may avoid showing five nearly identical items at the top.
This is why scoring and ranking should be separate steps.
12. Make Candidate Generation Flexible
A simple interface could look like this:
interface CandidateGenerator {
generate(
context: RecommendationContext
): Candidate[];
}
The first version may use Neo4j:
GraphCandidateGenerator
Later, you can add other ways to find candidates:
SimilarityCandidateGenerator
VectorCandidateGenerator
PopularityCandidateGenerator
CollaborativeCandidateGenerator
The rest of the recommendation pipeline does not need to know where the candidate came from.
Every candidate enters the same process.
13. Build Around "Recommendable Items"
Instead of designing the system around one specific item type, design it around a general idea:
Recommendable Item
For example, the same engine could recommend:
Article
Video
Workshop
Mentor
Community
Tool
The process stays the same:
Find candidates
↓
Collect evidence
↓
Apply filters
↓
Score
↓
Rank
Only the relationships and signals may change depending on the type of item.
This means you do not need a completely new recommendation system for every new item type.
14. Adding New Data Is Not Completely Automatic
You may hope that:
If we add new data to the graph, the recommendation engine will automatically understand how to use it.
This is partly true.
The graph can tell you that a relationship exists.
But the system still needs to know:
Why does this relationship matter for recommendations?
So when adding a new item type, you may need:
The new data in the graph
Meaningful relationships
Configuration that explains those relationships
Relevant features or scoring rules
The goal is not:
Add a new item type → Build a new recommendation algorithm.
The goal is:
Add a new item type → Define its relationships and configuration → Use the existing pipeline.
15. Keep the Main Building Blocks Generic
A recommendation service can use a small set of shared components:
RecommendationRequest
RecommendationContext
Candidate
Evidence
Feature
CandidateFilter
ScoringEngine
RankingEngine
A candidate could contain information about where it came from and why it was selected:
interface Candidate {
id: string;
type: string;
sources: CandidateSource[];
evidence: Evidence[];
features?: Feature[];
}
This creates a common structure for candidates found through:
Neo4j
Similarity search
Vector search
Popularity
Other recommendation methods
16. Return the Reason Behind the Recommendation
A recommendation API should return more than this:
{
"id": "123",
"score": 0.91
}
A better result could include the reason:
{
"id": "123",
"score": 0.91,
"reasons": [
"Strong match with the requested intent",
"Addresses an important requirement",
"Fits the current context"
],
"evidence": [
{
"type": "intent_match"
},
{
"type": "requirement_match"
}
]
}
The exact format can change.
The important idea is:
The system should be able to explain why an item was recommended.
17. A Possible Code Structure
The service could be organized like this:
recommendation/
├── api/
│ └── RecommendationController
│
├── context/
│ └── ContextResolver
│
├── candidates/
│ ├── CandidateGenerator
│ ├── GraphCandidateGenerator
│ └── OtherCandidateGenerators
│
├── evidence/
│ └── EvidenceExtractor
│
├── features/
│ └── FeatureEngine
│
├── filters/
│ ├── CandidateFilter
│ └── ConstraintFilters
│
├── scoring/
│ └── ScoringEngine
│
├── ranking/
│ └── RankingEngine
│
├── graph/
│ ├── QueryRepository
│ └── GraphClient
│
└── configuration/
├── RelationshipSemantics
└── ScoringProfiles
The exact folder names are not important.
What matters is keeping different responsibilities separate.
18. AI Can Be Added Later
This architecture does not require AI or machine learning.
You can start with a simple rule-based system.
Later, AI or machine learning can be added to specific parts of the pipeline.
For example:
Graph
↓
Find candidates
↓
Build features
↓
Machine learning ranking
↓
Final ranking
Or:
Graph
↓
Find candidates
↓
Embedding similarity
↓
Expand candidate list
↓
Rule-based or ML ranking
The main idea is:
AI should be one part of the recommendation system, not the entire architecture.
19. Common Mistakes to Avoid
Avoid assuming:
The shortest graph path is always the best recommendation.
More graph connections always mean better relevance.
Every new item type needs a new recommendation engine.
Every preference should be a hard filter.
The graph query should calculate everything.
The API only needs to return a score.
Arbitrary Cypher queries should be generated dynamically.
Instead, keep these jobs separate:
Graph retrieval
↓
Evidence
↓
Filtering
↓
Scoring
↓
Ranking
20. How to Move Toward This Architecture
If you already have a recommendation system, you do not need to rebuild everything at once.
You can improve it step by step.
Step 1: Create Common Models
Start with:
RecommendationRequest
RecommendationContext
Candidate
Evidence
Step 2: Separate Candidate Generation
Graph traversal
↓
Candidate generation
Step 3: Separate Evidence
Graph paths
↓
Evidence
Step 4: Create Features
Evidence
↓
Features
Step 5: Separate Filtering, Scoring, and Ranking
Create:
CandidateFilter
ScoringEngine
RankingEngine
Step 6: Make Important Settings Configurable
Move these into configuration:
Relationship meaning
Feature weights
Scoring profiles
Step 7: Add More Retrieval Methods When Needed
For example:
Similarity search
Vector search
Collaborative signals
Popularity
Step 8: Try Machine Learning Later
Once you have enough user behavior data, you can test whether a machine learning ranking model performs better than your rule-based approach.
21. The Main Design Idea
The entire architecture can be explained in one sentence:
The graph finds possibilities. Evidence explains why they are relevant. Filters remove invalid items. Scoring decides how useful they are. Ranking decides the final order.
The full flow looks like this:
GRAPH
↓
CANDIDATES
↓
EVIDENCE
↓
FEATURES
↓
FILTERS
↓
SCORE
↓
RANK
↓
RECOMMENDATIONS + REASONS
Final Takeaway
Neo4j is useful for recommendation systems because recommendations often depend on connections and context.
But Neo4j does not automatically create the best recommendation.
Think of the system like this:
Neo4j
=
Connected information
Recommendation Engine
=
Understanding and decision-making
Result
=
Ranked recommendations + reasons
This separation makes the system easier to grow.
When you add a new type of item, you should not need to build:
NewItemRecommendationEngine
Instead:
Add the item to the graph.
Create meaningful relationships.
Define what those relationships mean.
Let the existing recommendation pipeline process the item.
Build one recommendation engine based on evidence, instead of building a separate recommendation algorithm for every item type.





Top comments (0)