DEV Community

Prathamesh Sable
Prathamesh Sable

Posted on

Building a Flexible Recommendation Engine with Neo4j

How to build one recommendation system that can work with different types of items instead of creating a new algorithm every time.

Building Smarter Recommendations (AI Generated)

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

The Recommendation Pipeline (AI Generated)

A recommendation system can be divided into these stages:

  1. Request – What is the user asking for?

  2. Context – What information should influence the recommendation?

  3. Candidate Generation – Which items might be relevant?

  4. Evidence – Why is each item connected to the request?

  5. Features – How strong are those signals?

  6. Filtering – Which items should be removed?

  7. Scoring – How relevant is each valid item?

  8. 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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Together, these create a candidate list:


A B C D E F G H

Enter fullscreen mode Exit fullscreen mode

Candidate Generation (AI Generated)

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Use a clearer process:


Graph connection

↓

Evidence

↓

Feature

↓

Score contribution

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Evidence to Score (AI Generated)

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

A preference means the item can still be recommended, but some items should rank higher.

For example:


A newer item

↓

Gets a higher score

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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[];

}

Enter fullscreen mode Exit fullscreen mode

The first version may use Neo4j:


GraphCandidateGenerator

Enter fullscreen mode Exit fullscreen mode

Later, you can add other ways to find candidates:


SimilarityCandidateGenerator

VectorCandidateGenerator

PopularityCandidateGenerator

CollaborativeCandidateGenerator

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

One Engine, Many Item Types (AI Generated)

For example, the same engine could recommend:


Article

Video

Workshop

Mentor

Community

Tool

Enter fullscreen mode Exit fullscreen mode

The process stays the same:


Find candidates

↓

Collect evidence

↓

Apply filters

↓

Score

↓

Rank

Enter fullscreen mode Exit fullscreen mode

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:

  1. The new data in the graph

  2. Meaningful relationships

  3. Configuration that explains those relationships

  4. 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

Enter fullscreen mode Exit fullscreen mode

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[];

}

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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"

}

]

}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Or:


Graph

↓

Find candidates

↓

Embedding similarity

↓

Expand candidate list

↓

Rule-based or ML ranking

Enter fullscreen mode Exit fullscreen mode

The main idea is:

AI should be one part of the recommendation system, not the entire architecture.


19. Common Mistakes to Avoid

Avoid assuming:

  1. The shortest graph path is always the best recommendation.

  2. More graph connections always mean better relevance.

  3. Every new item type needs a new recommendation engine.

  4. Every preference should be a hard filter.

  5. The graph query should calculate everything.

  6. The API only needs to return a score.

  7. Arbitrary Cypher queries should be generated dynamically.

Instead, keep these jobs separate:


Graph retrieval

↓

Evidence

↓

Filtering

↓

Scoring

↓

Ranking

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Step 2: Separate Candidate Generation


Graph traversal

↓

Candidate generation

Enter fullscreen mode Exit fullscreen mode

Step 3: Separate Evidence


Graph paths

↓

Evidence

Enter fullscreen mode Exit fullscreen mode

Step 4: Create Features


Evidence

↓

Features

Enter fullscreen mode Exit fullscreen mode

Step 5: Separate Filtering, Scoring, and Ranking

Create:


CandidateFilter

ScoringEngine

RankingEngine

Enter fullscreen mode Exit fullscreen mode

Step 6: Make Important Settings Configurable

Move these into configuration:


Relationship meaning

Feature weights

Scoring profiles

Enter fullscreen mode Exit fullscreen mode

Step 7: Add More Retrieval Methods When Needed

For example:


Similarity search

Vector search

Collaborative signals

Popularity

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

This separation makes the system easier to grow.

When you add a new type of item, you should not need to build:


NewItemRecommendationEngine

Enter fullscreen mode Exit fullscreen mode

Instead:

  1. Add the item to the graph.

  2. Create meaningful relationships.

  3. Define what those relationships mean.

  4. 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)