← Back to list

Build a Movie Recommender Using a Knowledge Graph (Neo4j + FastAPI + Streamlit)

How I ditched collaborative filtering and used graph traversal to recommend movies — and why it actually makes more sense.

SHIVA KUMAR BILLANA · 2026-05-14 12:06 · 4 claps · 5.8 min read
#neo4j-graph-database #knowledge-graph #fastapi #database #python
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval 🎬 · Film & Television 📊 · Economic Policy

Build a Movie Recommender Using a Knowledge Graph (Neo4j + FastAPI + Streamlit)

How I ditched collaborative filtering and used graph traversal to recommend movies — and why it actually makes more sense.

Visualizing the data in graph

Visualizing the data in graph

Why Not Just Use Machine Learning?

When most people think “recommendation system,” they picture matrix factorization, collaborative filtering, or sentence embeddings. Those approaches are powerful — but they come with real drawbacks:

  • They need large amounts of user interaction data to work well
  • They’re often black boxes — you can’t easily explain why a movie was recommended
  • They’re complex to set up for a small project

There’s a simpler, more intuitive approach that gets overlooked: Knowledge Graphs.

The insight is straightforward. If you liked Inception, you might like other films by Christopher Nolan, or other sci-fi thrillers, or other Warner Bros. productions. These are explicit, structured relationships — and graph databases are built exactly for this.

In this tutorial, we’ll build CineGraph: a movie recommendation system that uses a Neo4j knowledge graph, a FastAPI backend, and a Streamlit frontend. No ML training required.

What We’re Building

CSV Dataset → Neo4j AuraDB (Knowledge Graph)
                      ↓
             FastAPI Recommendation API
                      ↓
           Streamlit Frontend (User Interface)

Users search for a movie, pick it from a dropdown, and get recommendations across three signals: genre, director, and production company — with a combined score that weighs them intelligently.

Tech stack:

  • Neo4j AuraDB (free tier) — graph database
  • FastAPI — backend API
  • Streamlit — frontend UI
  • Python — everything else
  • TMDB 5000 dataset — movie data

Part 1: The Knowledge Graph

What Is a Knowledge Graph?

A knowledge graph stores data as nodes (entities) and relationships (connections between them). Instead of rows and columns, you have a network of connected facts.

For CineGraph, our graph looks like this:

(Movie)-[:DIRECTED_BY]->(Director)
(Movie)-[:HAS_GENRE]->(Genre)
(Movie)-[:PRODUCED_BY]->(Company)

When you ask “find movies similar to Inception,” the graph traversal literally follows these edges — finding movies that share directors, genres, or companies with Inception.

Setting Up Neo4j AuraDB

Neo4j offers a free cloud-hosted tier called AuraDB. Sign up at neo4j.com/cloud/aura, create a free instance, and save your credentials.

You’ll get:

  • NEO4J_URI — something like neo4j+s://xxxx.databases.neo4j.io
  • NEO4J_USERNAME — usually neo4j
  • NEO4J_PASSWORD — your instance password

Connecting to Neo4j

# database.py
from neo4j import GraphDatabase
from dotenv import load_dotenv
import os
load_dotenv()
uri = os.getenv("NEO4J_URI")
username = os.getenv("NEO4J_USERNAME")
password = os.getenv("NEO4J_PASSWORD")
driver = GraphDatabase.driver(uri, auth=(username, password))
def get_driver():
    return driver

Loading the Data

Download the TMDB 5000 Movies dataset from Kaggle. Then write a script to parse it and insert nodes and relationships into Neo4j.

# data_insertion.py (simplified)
import pandas as pd
import json
from database import get_driver
driver = get_driver()
df = pd.read_csv("tmdb_5000_movies.csv")
with driver.session() as session:
    for _, row in df.iterrows():
        movie_name = row["title"]
        genres = json.loads(row["genres"])
        companies = json.loads(row["production_companies"])
        # Create Movie node
        session.run("MERGE (m:Movie {name: $name})", name=movie_name)
        # Create Genre nodes and relationships
        for genre in genres:
            session.run("""
                MERGE (g:Genre {name: $name})
                MERGE (m:Movie {name: $movie})-[:HAS_GENRE]->(g)
            """, name=genre["name"], movie=movie_name)
        # Repeat for directors and companies...

After running this, your graph will have ~4,800 movies connected to 20 genres, 2,350 directors, and 5,000+ companies via 31,000+ relationships.

Part 2: The Scoring Logic

Here’s the core idea behind CineGraph’s recommendations.

When you search for movies similar to Inception, we traverse the graph and count how many connections each candidate movie shares with it:

Signal Weight Reasoning Shared Genre +1 per genre Genre taste is common but not definitive Shared Director +2 Same director is a much stronger signal Shared Company +1 per company Studio style matters, but less so

Director gets double weight because it’s a stronger stylistic signal — you’re far more likely to enjoy another Nolan film than just any other thriller.

Part 3: The Recommendation Queries

This is where Neo4j shines. The query language Cypher lets you express graph traversals in a way that reads almost like English.

Genre-Based Recommendations

MATCH (m:Movie)
WHERE toLower(m.name) = toLower($movie)
MATCH (m)-[:HAS_GENRE]->(g:Genre)<-[:HAS_GENRE]-(other:Movie)
WHERE other <> m
RETURN other.name AS title, count(g) AS score
ORDER BY score DESC LIMIT $limit

Read this as: “Find my movie, then find all other movies that share at least one genre with it, count how many genres they share, and return them sorted by that count.”

Director-Based Recommendations

MATCH (m:Movie)
WHERE toLower(m.name) = toLower($movie)
MATCH (m)-[:DIRECTED_BY]->(d:Director)<-[:DIRECTED_BY]-(other:Movie)
WHERE other <> m
RETURN other.name AS title, count(d) AS score
ORDER BY score DESC LIMIT $limit

Combined Score (The Main Recommender)

MATCH (m:Movie)
WHERE toLower(m.name) = toLower($movie)
OPTIONAL MATCH (m)-[:HAS_GENRE]->(g:Genre)<-[:HAS_GENRE]-(other:Movie)
WHERE other <> m
WITH m, other, count(DISTINCT g) AS genre_score
OPTIONAL MATCH (m)-[:DIRECTED_BY]->(d:Director)<-[:DIRECTED_BY]-(other)
WITH m, other, genre_score, count(DISTINCT d) * 2 AS director_score
OPTIONAL MATCH (m)-[:PRODUCED_BY]->(c:Company)<-[:PRODUCED_BY]-(other)
WITH other, genre_score, director_score, count(DISTINCT c) AS company_score
RETURN other.name AS title,
       genre_score + director_score + company_score AS total_score,
       genre_score,
       director_score,
       company_score
ORDER BY total_score DESC LIMIT $limit

Notice the OPTIONAL MATCH — this ensures movies still appear even if they don't share a director or company, they just score 0 on that signal.

Wrapping Queries in Python

# recommender.py
from database import get_driver
driver = get_driver()
def recommend_by_genre(movie: str, limit: int = 10):
    with driver.session() as session:
        result = session.run("""
            MATCH (m:Movie)
            WHERE toLower(m.name) = toLower($movie)
            MATCH (m)-[:HAS_GENRE]->(g:Genre)<-[:HAS_GENRE]-(other:Movie)
            WHERE other <> m
            RETURN other.name AS title, count(g) AS score
            ORDER BY score DESC LIMIT $limit
        """, movie=movie, limit=limit)
        return [{"title": r["title"], "score": int(r["score"])} for r in result]

Part 4: The FastAPI Backend

FastAPI makes it trivial to expose our recommender functions as a REST API.

# main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from recommender import recommend_by_genre, recommend_by_director, recommend_combined
app = FastAPI(title="CineGraph API")
app.add_middleware(CORSMiddleware, allow_origins=["*"],
                   allow_methods=["*"], allow_headers=["*"])
@app.get("/recommend")
def combined_recommendations(movie: str, limit: int = 10):
    results = recommend_combined(movie, limit)
    if not results:
        raise HTTPException(status_code=404, detail=f"Movie '{movie}' not found")
    return {"movie": movie, "recommendations": results}
@app.get("/movies/search")
def search_movies(q: str):
    from database import get_driver
    driver = get_driver()
    with driver.session() as session:
        result = session.run("""
            MATCH (m:Movie)
            WHERE toLower(m.name) CONTAINS toLower($q)
            RETURN m.name AS title
            ORDER BY m.name LIMIT 10
        """, q=q)
        return {"results": [r["title"] for r in result]}

Run it with:

uvicorn main:app --reload

Your API is now live at http://localhost:8000. Check the auto-generated docs at /docs.

Pydantic Response Models

FastAPI uses Pydantic for response validation — always a good practice:

# models.py
from pydantic import BaseModel
from typing import List
class CombinedRecommendation(BaseModel):
    title: str
    total_score: int
    genre_score: int
    director_score: int
    company_score: int

Part 5: The Streamlit Frontend

# app.py (key parts)
import streamlit as st
import requests, os
API_URL = os.getenv("API_URL", "http://localhost:8000")
st.title("🎬 CineGraph Movie Recommender")
search_query = st.text_input("Search for a movie", placeholder="e.g. Inception, Avatar...")
if search_query:
    res = requests.get(f"{API_URL}/movies/search", params={"q": search_query})
    suggestions = res.json().get("results", [])
    movie_selected = st.selectbox("Select a movie", suggestions)
if movie_selected:
    tab1, tab2, tab3, tab4 = st.tabs([
        "⭐ Combined", "🎭 By Genre", "🎬 By Director", "🏢 By Company"
    ])
    with tab1:
        res = requests.get(f"{API_URL}/recommend", params={"movie": movie_selected})
        for i, rec in enumerate(res.json()["recommendations"], 1):
            st.write(f"**{i}.** {rec['title']}")
            st.divider()

Run it with:

streamlit run app.py

Part 6: Project Setup

Install Dependencies

git clone <your-repo>
cd cinegraph
pip install -r requirements.txt
# requirements.txt
fastapi
uvicorn
neo4j
python-dotenv
streamlit
requests
pydantic

Environment Variables

# .env
NEO4J_URI=neo4j+s://your-instance.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your-password
API_URL=http://localhost:8000

Running the App

You need two terminals:

# Terminal 1 — FastAPI
uvicorn main:app --reload
# Terminal 2 — Streamlit
streamlit run app.py

What We Learned

Knowledge graphs are underrated for recommendation systems. They’re:

  • Explainable — you can always tell the user why a movie was recommended (“3 shared genres, same director”)
  • No training data needed — works immediately with structured data
  • Fast to query — graph traversals are highly optimized in Neo4j
  • Easy to extend — add actors, release year, ratings as new node types and relationships

The tradeoffs:

  • No personalization based on individual user history (though you could add user nodes to the graph)
  • Recommendations are only as good as your metadata quality
  • Cold start isn’t a problem here since recommendations are content-based, not user-based

What’s Next

A few directions to take this further:

  • Add movie metadata — poster images, ratings, release year, plot overview
  • User preference nodes — connect users to genres/directors they like, then traverse from them
  • Semantic similarity layer — use sentence embeddings on plot summaries as a fourth signal
  • Deploy — FastAPI to Railway or Render, Streamlit to Streamlit Cloud

Final Thoughts

This project is a great example of choosing the right tool for the problem. You don’t always need a neural network. Sometimes the data is the structure, and a graph database lets you query that structure directly.

If you found this useful, the full source code is on GitHub. Feel free to fork it, extend it, and share what you build.

Built by Shiva Kumar Billana & Nithin Chandra


메타데이터
post_id
65126e7be3c2
slug
build-a-movie-recommender-using-a-knowledge-graph-neo4j-fastapi-streamlit-65126e7be3c2
url
https://medium.com/@billanashivakumar/build-a-movie-recommender-using-a-knowledge-graph-neo4j-fastapi-streamlit-65126e7be3c2
canonical_url
https://medium.com/@billanashivakumar/build-a-movie-recommender-using-a-knowledge-graph-neo4j-fastapi-streamlit-65126e7be3c2
author_url
https://medium.com/@billanashivakumar
status
ok
fetched_at
2026-06-09 15:37:30