Agentic AI

FalkorDB for Beginners: How to Build a Knowledge Graph in Python

Master FalkorDB from scratch! Discover how graph databases work, learn essential Cypher queries, and build your first knowledge graph using Python.

Why Your Database Is Failing Your App ?

Picture this.

You build a modern college app. A student types in a simple request:

“I’m learning DSA. Which students know Python, are good at Data Structures, and are actively building a machine learning project with someone who can mentor them?”

Your app crashes, times out, or returns empty results. Why? Because the data is trapped in relational tables. To find that single answer, your database has to run five expensive SQL JOIN operations across students, courses, skills, and projects, choking your server memory in the process.

Your database didn’t fail because the data wasn’t there. It failed because relational databases are built to store isolated rows, not human-like connections.

What If Your Database Could Follow Connections?

Human memory doesn’t work in rows and columns. When you think of a friend, you instantly remember where you met them, the coffee shop nearby, and the song that was playing. You hop naturally from one connected fact to the next.

That is exactly how a graph database thinks.

Instead of forcing you to reconstruct data across fragmented tables, a graph database stores information as Nodes (things like students, subjects, or projects) and Edges (relationships like STUDIES, HAS_SKILL, or WORKS_ON).

When a user asks a complex question, the database doesn’t search for a matching row. It simply walks down the chain of connections:

Student 
   ├── STUDIES ──────→ DSA
   ├── HAS_SKILL ────→ Python
   └── WORKS_ON ─────→ ML Project
                            │
                            └── REQUIRES ───→ Machine Learning
                                                     ▲
                                                     │ HAS_SKILL
                                                     │
                                                   Rahul

Your question changes from a rigid search into a natural traversal: “Follow this web of relationships and tell me who is standing on the other side.”

What We Are Building Today

In this hands-on tutorial, we are going to master this exact way of thinking using FalkorDB—one of the fastest graph databases in the world, built on top of Redis.

We are skipping the boring walls of dry terminology. Instead, we are going to build a real system from absolute zero:

  • We’ll start with just three tiny pieces of data.

  • We’ll make common mistakes (like accidentally creating duplicate records).

  • We’ll write our very first Cypher query.

  • We’ll scale up to a complete college knowledge graph containing students, subjects, clubs, skills, and projects.

  • We’ll use it to answer multi-hop questions that are a nightmare to write in ordinary SQL.

And we are doing all of this before touching an LLM.

Why? Because in Part 2 of this series, we are going to build an Agentic RAG system and give an AI agent the power to query this exact graph in natural language. But to make an AI smart, you first have to understand the underlying infrastructure yourself.

Let’s dive in.

Before FalkorDB: Why Do We Need a Graph at All?

SQL was built for spreadsheets, not real life. If you ask a relational database “Find me a student who studies DSA, knows Python, and has a friend working on an AI project,” you aren’t querying data—you are performing open-heart surgery with a chainsaw. You write five messy JOIN statements, cross your fingers, and pray your server doesn’t time out.

Data isn’t isolated in rows; it’s a living web of connections. Here is why traditional databases choke the moment your questions get real—and why graph thinking changes everything.

Imagine your college holds these simple facts:

  • Aarav studies DSA.

  • Aarav knows Python.

  • Rahul studies DSA.

  • Rahul knows Python.

  • Rahul knows Machine Learning.

  • Rahul works on the Campus Chatbot.

Could you store this in SQL? Absolutely. Relational databases are fantastic for structured tables. You would create separate, neat little boxes:

  • students

  • subjects

  • skills

  • projects

  • student_skills

  • student_subjects

  • student_projects

On day one, everything looks fine. But watch what happens as soon as your questions start tracking relationships instead of isolated rows:

  1. Who studies the same subjects as Aarav?

  2. Who studies the same subjects as Aarav and also knows Python?

  3. Who knows the exact skills required by the Campus Chatbot project?

  4. Which of Aarav’s friends knows Python?

  5. What is the exact prerequisite chain from DSA to Operating Systems?

Look closely at what these questions are actually asking.

They don’t care about a single row. They don’t want a static lookup. They are asking a fundamental question: “What is connected to what, and how do I walk across those connections?”

In SQL, answering multi-hop relationship questions forces you to chain complex joins that become unreadable and slow at scale. In a graph, you don’t look up records—you simply follow the path.

Stop Thinking in Tables for a Minute

Traditional databases are built around entities isolated in rows. Graph databases are built around how things interact.

Every graph comes down to just two core ingredients:

  • Things (People, places, skills, or subjects)

  • Connections (How those things relate to one another)

In graph terminology, we call them:

  • THING → Node

  • CONNECTION → Relationship

A Simple Graph :-

Suppose we have three distinct pieces of data:

  1. Aarav (A student)

  2. DSA (A subject)

  3. Python (A skill)

In a traditional database, these sit in three separate, lonely tables. In a graph, we draw them as independent floating circles:

(Aarav)
(DSA)
(Python)

Now, let’s wire them together using directional arrows:

(Aarav)- [:STUDIES] → (DSA)
(Aarav)- [:HAS_SKILL] → (Python)

Read those lines out loud:

  • Aarav studies DSA.

  • Aarav has the skill Python.

Congratulations. You just built your first knowledge graph.

Why Graphs Beat Tables When Scale Explodes :

This sounds trivial when you’re looking at a single student. But watch what happens when your application grows from 3 data points to 10,000 students.

In a real college network, a single student can simultaneously:

  • Study multiple subjects

  • Master multiple technical skills

  • Join multiple campus clubs

  • Collaborate on active software projects

  • Maintain friend networks

  • Share complex study interests

  • Navigate course prerequisite chains

In a relational SQL setup, answering multi-layered questions requires writing massive, painful JOIN statements across 6 different mapping tables.

In a graph database, the interesting information stops living inside the nodes—it lives inside the network. The power doesn’t come from storing what a student is; it comes from instantly traversing the invisible web of relationships that bind your entire dataset together.

The Core Graph Terms You Need to Know

Before writing a single line of Cypher, you need to master the fundamental vocabulary. Every graph database—including FalkorDB—is built on these exact six building blocks. Let’s cover the first one right now.

1. Node (The “Thing”)

A node represents any physical or abstract entity—a person, a subject, a tool, a club, or a project.

  • Examples: Aarav, DSA, Python, Robotics Club, Campus Chatbot

In FalkorDB, nodes are categorized using labels and described using properties. Look at this exact node syntax:

Cypher:
(:Student {
    name: "Aarav",
    year: 2,
    branch: "CSE"
})

Breaking that down:

  • :Student is the Label. It answers the question: What is this node?

  • name, year, and branch are Properties. They answer the question: What do we know about it?

Mentally, you can picture it like this hierarchy:

Student
   │
   └── Aarav
       ├── year: 2
       └── branch: CSE
  • Label → What is this? (Category)

  • Property → What do we know about it? (Key-value data)

Why Directional Arrows Matter More Than You Think

A collection of isolated nodes is just a digital junkyard. What turns data into a living network is the arrow.

Take this single line of Cypher code:

$$\text{(:Student \{name: “Aarav”\})} \rightarrow \text{[:STUDIES]} \rightarrow \text{(:Subject \{name: “DSA”\})}$$

It looks simple, but it packs four distinct engineering pieces into one readable sentence:

  • (Aarav) → Starting node (Who is acting?)

  • [:STUDIES] → Relationship type (What is happening?)

  • -> → Direction (Where is the action heading?)

  • (DSA) → Destination node (Who is receiving it?)

Together, they form a clear, directional sentence: Aarav studies DSA.

The Direction Trap

The arrow isn’t decorative—it dictates semantic reality.

If you accidentally flip the pointer:

$$\text{(DSA)} \rightarrow \text{[:STUDIES]} \rightarrow \text{(Aarav)}$$

You are explicitly telling the database that DSA studies Aarav.

Unless your coursework has achieved sentience, that is pure nonsense.

Rule of Thumb: Always design your relationship arrows to mirror real-world logic. If the source acts on the target, point the arrow straight from cause to effect.

Wait — Relationships Can Have Data Too?

Most databases force you to store relationship details awkwardly in separate mapping tables. Graph databases let you put data directly on the connection itself.

Suppose Aarav studies DSA. That is a connection. But we also know two extra facts:

  • Semester: 3

  • Grade: B+

Where should this information live?

  • It doesn’t belong on Aarav (his grade in DSA isn’t a permanent personal trait).

  • It doesn’t belong on DSA (the subject doesn’t own the grade).

It belongs precisely to the relationship between them.

In Cypher, we attach properties right inside the edge brackets:

Cypher:
(s)-[:STUDIES {
   semester: 3,
    grade: "B+"
}]->(sub)

Visually, the edge acts as a data-bearing bridge:

             semester: 3
             grade: B+
                  │
                  ▼
(Aarav) ───── STUDIES ─────→ (DSA)

This changes everything. A relationship is never just a dead line—it is a first-class object that can carry its own attributes.

For instance, looking at a club membership:

Cypher:
(Aarav)-[:MEMBER_OF {
    since: 2023,
    role: "Core"
}]->(Coding Club)

Instantly, the graph knows not just that Aarav is in the club, but when he joined and what his responsibilities are. When your connections carry their own context, complex application logic becomes effortless.

So What Exactly Is FalkorDB?

Forget heavy database bloat and painful cluster configurations.

At its core, FalkorDB is Redis injected with graph superpowers.

You already know Redis as the lightning-fast, in-memory key-value store used for caching. FalkorDB takes that exact high-speed engine and layers a purpose-built graph database right on top of it. Same speed, same simplicity—except instead of storing isolated keys and values, you are storing fully connected networks.

How the Stack Actually Works ?

You don’t need to become a Redis infrastructure guru to master graphs. Your day-to-day architecture flows through a clean, predictable pipeline:

Your Python Application
          │
          ▼
     FalkorDB Client
          │
          ▼
      FalkorDB Server
          │
          ▼
     Graph Namespace
          │
     ┌────┼────┐
     ▼    ▼    ▼
   Nodes  Edges  Properties

FalkorDB lets you model connected facts instantly and query them using Cypher (the industry-standard graph query language).

That lightning-fast combination is why graphs are dominating modern tech stacks, powering everything from:

  • AI Memory & GraphRAG: Giving large language models persistent, structured long-term memory.

  • Agentic Applications: Allowing autonomous AI agents to traverse multi-step relationships.

  • Recommendation Engines: Finding instant peer connections and shared interests.

  • Fraud Detection & Dependency Mapping: Catching complex hidden loops in real time.

Why Not Just Use SQL? (When Tables Fail)

If SQL has been powering the internet for decades, why bother with graph databases at all?

Let’s clear the air: SQL is phenomenal. If you want to know “Which students study DSA?”, a simple SQL query handles it effortlessly.

The trouble starts the moment your questions get messy and interconnected. Try answering this in SQL:

“Which students study DSA, also know Python, and work on active projects requiring Machine Learning?”

In a traditional relational database, answering that requires chaining together 4 or 5 separate mapping tables using painful, performance-heavy JOIN statements.

In a graph database, the question mirrors the actual data structure:

Student
   │
   ├── STUDIES ──→ DSA
   │
   ├── HAS_SKILL ──→ Python
   │
   └── WORKS_ON ──→ Project
                         │
                         └── REQUIRES ──→ Machine Learning

Graphs aren’t magically “better” than SQL. That’s the wrong takeaway.

The golden rule of database architecture is simple: Different data shapes favor different models.

If your data is tabular and rigid, use SQL. But if your application revolves around deep, multi-layered relationships—like AI memory, fraud detection, and Agentic RAG—graphs win every single time.

Your First FalkorDB Graph

Enough theory. Let’s write code.

We aren’t building a 20-node monster yet, and there are zero AI agents or RAG pipelines in sight. We just need to spin up FalkorDB and make our first connection work with a single relationship: Aarav → STUDIES → DSA.

1. Spin Up FalkorDB with Docker

Create a file named docker-compose.yml in your project folder:

YAML:
services:
  falkordb:
    image: falkordb/falkordb-server:latest
    container_name: falkordb_student
    ports:
      - "6390:6379"
    volumes:
      - falkordb_data:/var/lib/falkordb/data
    restart: unless-stopped

volumes:
  falkordb_data:

Run it in your terminal:

docker-compose up -d

Why port 6390:6379? We map your computer’s 6390 to the container’s default Redis port 6379. This prevents conflicts if you already have a local Redis instance running.

2. Connect via Python

Install the official Python client:

pip install falkordb

Fire up Python and connect to your database server:

from falkordb import FalkorDB

# 1. Connect to the server running on port 6390
client = FalkorDB(host="localhost", port=6390)

# 2. Select an isolated graph namespace
graph = client.select_graph("student_network")

Understanding the Core Objects:

  • client: Your active connection to the FalkorDB server.

  • graph: Your specific isolated database namespace. FalkorDB lets you run multiple independent graphs on one server without data bleeding across them:

    FalkorDB Server
          ├── student_network  (Our active graph)
          ├── user_alice_memory
          └── production_rag
    

3. Verify the Connection

Before building nodes, test the pipeline with a basic query:

result = graph.query("RETURN 1 AS number")
print(result.result_set)
# Expected Output: [[1]]
$$\text{Python Script} \longrightarrow \text{FalkorDB Client} \longrightarrow \text{student_network} \longrightarrow \text{Cypher Query} \longrightarrow \text{Result}$$

Congratulations. You just sent your first Cypher query to FalkorDB. The database is live, your environment is clean, and you’re ready to start dropping actual entities into the network.

Building Your First Graph: Nodes, Labels, and Relationships

You don’t need a hundred lines of SQL to map the real world. You just need to create two points and draw a line between them.

Here is how you write your first actual Cypher commands in FalkorDB to create a node, build a second one, and wire them together.

Step 1: Create Your First Node

Run this command in Python or your terminal client:

Cypher:
CREATE (:Student {
    name: "Aarav"
})

What just happened? FalkorDB created an isolated point in memory—a node categorized under the label Student carrying a single property:

(Student)
    │
    └── name = Aarav

Now, let’s create a subject node right beside it:

Cypher:
CREATE (:Subject {
    name: "DSA"
})

At this exact moment, your database contains two independent floating nodes (Aarav and DSA). They are sitting in the same space, but they are not connected. Right now, that is just a list disguised as a database. Let’s fix that.

Step 2: Bridge the Gap with a Relationship

To connect Aarav to his course, we use a pattern-matching command combined with a creation rule:

Cypher:
MATCH (s:Student {name: "Aarav"})
MATCH (sub:Subject {name: "DSA"})
CREATE (s)-[:STUDIES]->(sub)

Read that block line by line:

  1. MATCH (s:Student {name: "Aarav"})

    Find the Student node named Aarav and temporarily label that variable s.

  2. MATCH (sub:Subject {name: "DSA"})

    Find the Subject node named DSA and temporarily label it sub.

  3. CREATE (s)-[:STUDIES]->(sub)

    Build a directed relationship of type STUDIES pointing from Aarav directly to DSA.

Look at the structure you just generated:

┌──────────────┐
│ Aarav        │
│ Student      │
└──────┬───────┘
       │
    STUDIES
       │
       ▼
┌──────────────┐
│ DSA          │
│ Subject      │
└──────────────┘

That is your first complete, functioning graph. You didn’t write foreign keys, you didn’t configure a mapping table—you simply defined two entities and told the database how they relate.

MATCH — The Only Graph Command You Will Ever Need

Forget complex SQL joins. If you want to pull data from a graph database, you only need to master one master keyword: MATCH.

Think of MATCH as a visual searchlight. You aren’t writing procedural steps; you are drawing a picture of the pattern you want the database to find.

1. Finding a Single Node

Suppose you want to find our student Aarav. You write:

Cypher:
MATCH (s:Student {name: "Aarav"})
RETURN s

2. Finding All Nodes of a Category

If you want to pull every single student in your database regardless of their name, you drop the specific property filter:

Cypher:
MATCH (s:Student)
RETURN s

Translation: Find every node labeled Student.

3. Finding Connected Patterns (Relationships)

This is where the magic happens. If you want to find which subjects your students are studying, you don’t look at separate tables—you draw the connection right inside your query:

Cypher:
MATCH (s:Student)-[:STUDIES]->(sub:Subject)
RETURN sub

Translation: Find a Student node, follow an outgoing STUDIES arrow, and land on a Subject node. Return whatever is on the other side.

In SQL, you tell the computer how to fetch data by stitching tables together step-by-step.

In Cypher and FalkorDB, you describe the exact shape of the data you’re looking for. If the shape exists in the graph, the database hands it to you instantly.

Master Cypher in 60 Seconds: Read Queries Like Plain English

If you can read a sentence, you can read Cypher. While SQL forces you to mentally juggle three different tables and a messy tangle of foreign keys, graph queries literally draw a picture of the path you want to walk.

Here is the exact blueprint to reading graph queries instantly.

How to Read Cypher Like a Sentence

Take this exact query:

Cypher:
MATCH (s:Student {name: "Aarav"})
      -[:STUDIES]->
      (sub:Subject)
RETURN sub.name

Don’t memorize syntax rules. Just read it piece by piece:

  • MATCH → Find a pattern in the graph.

  • (s:Student {name: "Aarav"}) → Find a node labeled Student named Aarav, and temporarily call it s.

  • -[:STUDIES]-> → Follow the outgoing STUDIES relationship arrow.

  • (sub:Subject) → Arrive at the connected Subject node, calling it sub.

  • RETURN sub.name → Give me the name of that subject.

Put it together: Find Aarav, follow what he studies, and return the subject names. That is all Cypher is—visual storytelling for databases.

Your First Useful Query: Traversing the Graph

Let’s run that exact logic to answer a real question: What does Aarav study?

Cypher:
MATCH (s:Student {name: "Aarav"})
      -[:STUDIES]->
      (sub:Subject)
RETURN sub.name AS subject

Visualize how FalkorDB executes this under the hood:

  1. Find Aarav (The starting node).

  2. Follow STUDIES (The relationship hop).

  3. Reach DSA (The destination).

  4. Return the name $\rightarrow$ DSA.

Now, suppose we add a second course (DBMS) to Aarav’s profile:

             ┌──→ DSA
             │
Aarav ───────┤
             │
             └──→ DBMS

You don’t need to rewrite your query or add complex loops. Running the exact same query automatically traverses both branches and returns:

  • DSA

  • DBMS

This is the core superpower of graph traversal: You describe the shape of the relationship pattern you want, and the database handles the heavy lifting of finding every connected record instantly.

Why Blind CREATE Will Destroy Your Database (And How MERGE Saves It)

Databases don’t care if you make mistakes—until your nightly cron job duplicates 100,000 users and crashes your production server.

Here is how you write bulletproof Cypher commands that actually prevent chaos, filter data cleanly, and talk properly to your backend code.

The Dangerous Trap of CREATE

Run this command once:

Cypher:
CREATE (:Student {
    id: "S01",
    name: "Aarav"
})

Now run it four more times.

What happens? You don’t get a single Aarav. You get five separate Aarav nodes sitting in your database.

CREATE does exactly what it says: it forces a brand-new node into existence every single time. It doesn’t check if that student already exists. If your data ingestion pipeline runs blindly every night, you are building a factory for duplicate data.

MERGE: The Duplicate Killer

To fix this, we use MERGE. It acts as an intelligent upsert (Find-or-Create):

Cypher:
MERGE (s:Student {
    id: "S01"
})
SET s.name = "Aarav",
    s.year = 2,
    s.branch = "CSE"

How It Works Under the Hood?

  1. Check: Does a node with id: "S01" already exist?

  2. Found? If Yes, it grabs the existing node. If No, it creates a new one.

  3. Set: It updates the properties (SET) on that node safely.

⚠️ Pro-Tip: Always merge on a stable, unique identifier like an id or email. Never merge blindly on mutable fields like a person’s name—because names change, but unique IDs don’t.

SET: Modifying What Exists

Suppose Aarav moves up to his third year. We update his properties using MATCH combined with SET:

Cypher:
MATCH (s:Student {id: "S01"})
SET s.year = 3

You can update multiple fields in one go:

Cypher:
MATCH (s:Student {id: "S01"})
SET s.year = 3,
    s.branch = "CSE"

WHERE: Precision Filtering

To pull every CSE student in year 2 or above, add a filter clause:

Cypher:
MATCH (s:Student)
WHERE s.year >= 2
  AND s.branch = "CSE"
RETURN s.name, s.year

Stop thinking of Cypher as “SQL with weird symbols.” Instead, think in graph patterns first, then apply your logical filters using WHERE.

RETURN: Don’t Dump the Universe

Never return raw nodes (RETURN s) in production application code. Always specify the exact fields your API or frontend needs, and use aliases (AS) for clean formatting:

Cypher:
RETURN 
    s.name AS student, 
    s.year AS year, 
    s.branch AS branch

Clean output structures save your backend from parsing headaches—especially when you start letting AI agents generate these queries automatically in Part 2.

Building Your First Real Knowledge Graph with Python and Cypher

Enough with isolated nodes. Real data is messy, multi-layered, and deeply interconnected.

Here is how you scale a tiny 2-node graph into a fully functional 12-node network using Python loops, proper identifiers, and relationship properties.

The Blueprint: What We Are Building

To simulate a real college network, we need three core entity types:

  • Students (5): S01 (Aarav, Year 2, CSE), S02 (Payal, Year 2, CSE), S03 (Rahul, Year 3, IT), S04 (Sneha, Year 1, CSE), S05 (Karan, Year 3, ECE)

  • Subjects (4): CS201 (DBMS), CS202 (DSA), CS301 (Operating Systems), CS302 (Computer Networks)

  • Clubs (3): Coding Club (2018), Robotics Club (2019), Debate Society (2015)

Loading Nodes with Python & MERGE

Instead of writing manual queries, we loop through our data. But look closely at how we write the query:

students = [
    ("S01", "Aarav", 2, "CSE"),
    ("S02", "Payal", 2, "CSE"),
    ("S03", "Rahul", 3, "IT"),
    ("S04", "Sneha", 1, "CSE"),
    ("S05", "Karan", 3, "ECE"),
]

for sid, name, year, branch in students:
    graph.query(f"""
        MERGE (s:Student {{id: '{sid}'}})
        SET s.name = '{name}',
            s.year = {year},
            s.branch = '{branch}'
    """)

  • for sid, name, year, branch in students: Loops through each student in your data list, extracting their ID, name, year, and branch.

  • graph.query(...) Sends a database command to FalkorDB to execute the Cypher query.

  • MERGE (s:Student {id: '{sid}'}) Checks the database for an existing student node with that ID. If it exists, it selects it; if it does not exist, it creates a new node with the label Student. This prevents duplicate entries.

  • SET s.name = ... , s.year = ... , s.branch = ... Assigns or updates the properties (attributes) on that student node with their current name, year, and branch values.

Why This Design Pattern Matters:

  • The Unique Identifier (id: '{sid}'): We use a stable key like S01 inside MERGE so running this script 100 times never creates duplicate nodes.

  • The SET Clause: We isolate mutable properties (name, year, branch) inside SET so they update cleanly if student details change.

We apply the exact same logic for subjects, using stable course codes (CS202) instead of brittle names (DSA):

subjects = [
    ("CS201", "DBMS", 4),
    ("CS202", "DSA", 4),
    ("CS301", "Operating Systems", 3),
    ("CS302", "Computer Networks", 3),
]

for code, name, credits in subjects:
    graph.query(f"""
        MERGE (sub:Subject {{code: '{code}'}})
        SET sub.name = '{name}',
            sub.credits = {credits}
    """)

3. Storing Data Directly on Relationships (Edges)

Right now, we have 12 isolated nodes (5 students + 4 subjects + 3 clubs). A pile of parts is not an engine. Connections are the whole point.

Suppose Aarav studies DBMS (Grade A) and DSA (Grade B+). In SQL, grades live in separate mapping tables. In FalkorDB, the relationship itself carries data.

studies = [
    ("S01", "CS201", 3, "A"),
    ("S01", "CS202", 3, "B+"),
    ("S02", "CS201", 3, "A+"),
    ("S03", "CS301", 5, "A"),
    ("S04", "CS202", 1, "B"),
    ("S05", "CS302", 5, "A"),
]

for sid, code, sem, grade in studies:
    graph.query(f"""
        MATCH (s:Student {{id: '{sid}'}})
        MATCH (sub:Subject {{code: '{code}'}})
        MERGE (s)-[:STUDIES {{
            semester: {sem},
            grade: '{grade}'
        }}]->(sub)
    """)

Look at that relationship payload:

$$\text{(Aarav)} \xrightarrow[\text{grade = B+}]{[\text{:STUDIES}, \text{semester = 3}]} \text{(DSA)}$$

The grade doesn’t belong strictly to Aarav, nor does it belong strictly to DSA—it belongs to their interaction. That is the core superpower of graph modeling.

Why Subject-to-Subject Connections Change Everything

You can build a web of students and clubs all day long, but a true knowledge graph wakes up the moment your core concepts start talking directly to each other.

Up until now, our arrows only pointed from people to things (Student -> Subject or Student -> Club). But what happens when an academic subject has a dependency on another subject?

In the real world, you can’t just dive into Operating Systems without mastering Data Structures and Algorithms first. The curriculum itself forms a web of logic.

Connecting Subjects to Subjects:

We can map course prerequisites directly inside FalkorDB:

  • DSA is a prerequisite for Operating Systems.

  • DBMS is a prerequisite for Computer Networks.

Instead of creating massive relational lookup tables, we write a quick Python ingestion script using Cypher’s MERGE and an arrow relationship:

prereqs = [
    ("CS202", "CS301"), # DSA -> Operating Systems
    ("CS201", "CS302")  # DBMS -> Computer Networks
]

for pre, post in prereqs:
    graph.query(f"""
        MATCH (a:Subject {{code: '{pre}'}})
        MATCH (b:Subject {{code: '{post}'}})
        MERGE (a)-[:PREREQUISITE_FOR]->(b)
    """)

Suddenly, your graph bridges multiple dimensions of meaning:

  • Student -> Subject

  • Student -> Club

  • Subject -> Subject

This is the exact moment your data stops looking like static rows and starts behaving like an intelligent network.

Verifying Your First Mini-Graph

Before writing complex multi-hop queries, let’s check the health of our database. Run this quick script to count your nodes and relationships:

node_count = graph.query("MATCH (n) RETURN count(n)").result_set[0][0]
rel_count = graph.query("MATCH ()-[r]->() RETURN count(r)").result_set[0][0]

print(f"{node_count} nodes, {rel_count} relationships")

Expected Output:

12 nodes, 13 relationships

Take a breath and look at what you just built from scratch:

  • 5 Students (Aarav, Payal, Rahul, Sneha, Karan)

  • 4 Subjects (DBMS, DSA, OS, Networks)

  • 3 Clubs (Coding, Robotics, Debate)

  • 6 STUDIES edges

  • 5 MEMBER_OF edges

  • 2 PREREQUISITE_FOR edges

It’s intentionally tiny. A 12-node graph is small enough to fit inside your head all at once, yet robust enough to teach the exact traversal patterns you’ll use on massive enterprise datasets.

NEURAL NINJAS FalkorDB Visual Simulator
Ready
Student
Subject
Club
Skill
Project
MATCH (s:Student {name: "Aarav"}) -[:STUDIES]->(sub:Subject) RETURN sub.name AS subject, sub.credits AS credits
subjectcredits
DBMS4
DSA4
Click any node on the canvas to inspect its properties.

Query Pattern #1: One-Hop Traversal (What Does Aarav Study?)

Now we transition from building to querying. Let’s ask a simple question: What subjects does Aarav study?

In Cypher, we don’t write multi-line table JOIN statements. We simply sketch out the path we want to walk:

Cypher:
MATCH (s:Student {name: "Aarav"})
      -[:STUDIES]->
      (sub:Subject)
RETURN sub.name AS subject,
       sub.credits AS credits

How your brain (and FalkorDB) executes this:

  1. Find Aarav in the student pool.

  2. Follow the STUDIES arrow outward.

  3. Land on the subjects at the other end (DBMS and DSA).

Result:

DBMS    4
DSA     4

No foreign keys, no table joins—just a clean, direct visual path.

Query Pattern #2: Shared Neighbor Traversal (Finding Study Buddies)

Let’s crank up the difficulty. Suppose Payal studies DBMS. Who else in the college network studies that exact same subject?

Graphically, you are looking for a V-shape or an inverted triangle:

$$\text{Payal} \longrightarrow \text{DBMS} \longleftarrow \text{Other Student}$$

Here is how you write that pattern in Cypher:

Cypher:
MATCH (me:Student {name: "Payal"})
      -[:STUDIES]->
      (sub)
      <-[:STUDIES]-
      (other:Student)
WHERE other.name <> "Payal"
RETURN other.name AS student,
       sub.name AS common_subject

Cracking the Cypher Shape:

  • (me:Student {name: "Payal"}) -[:STUDIES]-> (sub) → Starts at Payal, labels this starting point as me and finds her subject.  “-[:STUDIES]->” follows a directed relationship labeled STUDIES pointing away from Payal. This means “Payal studies this subject.”

  • <-[:STUDIES]- (other:Student) → Walks backward up another incoming STUDIES arrow to find a different student sharing that same subject node. This means “Another student also studies this exact same subject.”

  • WHERE other.name <> "Payal" → Prevents Payal from matching with herself. <> means “not equal to”. In the query, other.name <> "Payal" ensures the database finds other students whose name is not Payal, preventing Payal from being matched with herself.

This is the core magic of graph architecture: two entities are connected because they point to the exact same intermediate node. Once you grasp shared-neighbor traversal, relational database joins will feel obsolete.

Question #3: Who Is in the Coding Club?

To find out who belongs to the Coding Club along with their specific roles and join dates, run this Cypher query:

Cypher:
MATCH (s:Student)
      -[m:MEMBER_OF]->
      (c:Club {name: "Coding Club"})
RETURN s.name AS student,
       s.year AS year,
       m.role AS role,
       m.since AS since

Why use [m:MEMBER_OF] instead of [:MEMBER_OF]?

Placing the letter m right before the colon assigns a variable to the relationship. This small detail unlocks access to relationship properties like m.role and m.since.

The resulting output displays:

Aarav   2   Core     2023
Payal   2   Member   2023

The Rule of Thumb: Whenever you need to read or filter data living inside a connection rather than a node, give that relationship a variable.

Question #4: Which Students Belong to Multiple Clubs?

When a student like Payal joins more than one organization—such as both the Coding Club and the Debate Society—tracking them requires a query that aggregates connections:

Cypher:
MATCH (s:Student)-[:MEMBER_OF]->(c:Club)
WITH s,
     COLLECT(DISTINCT c.name) AS clubs,
     COUNT(DISTINCT c) AS club_count
WHERE club_count > 1
RETURN s.name AS student,
       clubs

This query introduces two essential Cypher tools:

  1. COLLECT: Bundles multiple individual rows into a clean, unified list. Instead of returning separate rows for every club, it converts them into a formatted array:

    Payal → ["Coding Club", "Debate Society"]
    
  2. COUNT: Tallies the total number of unique matches found per student (club_count = 2). Combined with the WHERE clause, it filters out anyone with only a single membership, returning exclusively the multi-club active members.

Scaling Up: Adding Skills, Projects, and Multi-Hop Matching

Boring database tables store isolated facts. Knowledge graphs connect them to create intelligence.

Let’s scale our college network from a simple tracking system into a real matching engine by adding Skills and Projects—and using multi-hop graph traversal to answer questions we never explicitly programmed.

Step 1: Injecting Skills & Projects

First, expand the graph schema with 5 core programming and tech skills, alongside 3 active projects:

# 1. Create Skills
for skill in ["Python", "React", "SQL", "Machine Learning", "Docker"]:
    graph.query(f"MERGE (:Skill {{name: '{skill}'}})")

# 2. Create Projects
projects = [
    ("P01", "Library Management System", "active"),
    ("P02", "Campus Chatbot", "planning"),
    ("P03", "Attendance Tracker", "active"),
]

for pid, name, status in projects:
    graph.query(f"""
        MERGE (p:Project {{id: '{pid}'}})
        SET p.name = '{name}', p.status = '{status}'
    """)

Step 2: Wiring Up Student Skills and Project Demands

Next, connect students to their individual skill sets (with proficiency levels) and map out what technologies each project demands:

# Connect Students to Skills
student_skills = [
    ("S01", "Python", "Advanced"), ("S01", "SQL", "Intermediate"), ("S01", "Docker", "Beginner"),
    ("S02", "React", "Advanced"), ("S02", "Python", "Intermediate"),
    ("S03", "Python", "Intermediate"), ("S03", "Machine Learning", "Advanced"),
    ("S05", "Python", "Advanced"), ("S05", "Machine Learning", "Intermediate"),
]

for sid, skill, level in student_skills:
    graph.query(f"""
        MATCH (s:Student {{id: '{sid}'}})
        MATCH (sk:Skill {{name: '{skill}'}})
        MERGE (s)-[:HAS_SKILL {{level: '{level}'}}]->(sk)
    """)

# Connect Projects to Required Skills
project_requirements = [
    ("P01", "Python"), ("P01", "SQL"), ("P01", "React"),
    ("P02", "Python"), ("P02", "Machine Learning"),
    ("P03", "Python"), ("P03", "Docker"),
]

for pid, skill in project_requirements:
    graph.query(f"""
        MATCH (p:Project {{id: '{pid}'}})
        MATCH (sk:Skill {{name: '{skill}'}})
        MERGE (p)-[:REQUIRES]->(sk)
    """)

Now we have built a powerful structural chain:

$$\text{Project} \xrightarrow{\text{REQUIRES}} \text{Skill} \longleftarrow \text{HAS_SKILL} \longleftarrow \text{Student}$$

Step 3: Answering Complex Questions Without Hardcoding

Because the data is linked in a network, we can ask questions that require stepping across multiple relationships.

Query 1: Who is the best match for the Campus Chatbot?

Instead of hardcoding team assignments, let the graph calculate who possesses the exact skills required by project P02:

Cypher:
MATCH (p:Project {name: "Campus Chatbot"})-[:REQUIRES]->(sk:Skill)
MATCH (s:Student)-[:HAS_SKILL]->(sk)
RETURN s.name AS student,
       COLLECT(DISTINCT sk.name) AS matching_skills,
       COUNT(DISTINCT sk) AS match_score
ORDER BY match_score DESC

How it works:

  1. Finds the Campus Chatbot and its required skills (Python, Machine Learning).

  2. Traverses backwards to find students who possess those skills.

  3. Counts matches and ranks students natively (Rahul and Karan rise to the top with 2 matches each).

The Big Takeaway: We never explicitly stored a row saying “Rahul is good for the chatbot.” That answer emerged dynamically through graph traversal.

Query 2: Which skill is in highest demand across all projects?

Cypher:
MATCH (p:Project)-[:REQUIRES]->(sk:Skill)
RETURN sk.name AS skill, COUNT(p) AS projects_needing_it
ORDER BY projects_needing_it DESC

Result: Python dominates with 3 project requirements.

Step 4: Multi-Hop Traversal (Finding Friends Who Know Python)

Let’s add a social layer with friendship links:

friendships = [("S01", "S02"), ("S01", "S03"), ("S02", "S04"), ("S03", "S05")]
for a, b in friendships:
    graph.query(f"""
        MATCH (x:Student {{id: '{a}'}})
        MATCH (y:Student {{id: '{b}'}})
        MERGE (x)-[:FRIENDS_WITH]->(y)
    """)

Now, let’s find which of Aarav’s friends know Python using a two-hop path traversal:

Cypher:
MATCH (s:Student {name: "Aarav"})-[:FRIENDS_WITH]->(friend)-[:HAS_SKILL]->(sk:Skill {name: "Python"})
RETURN friend.name AS friend
$$\text{Aarav} \xrightarrow{\text{FRIENDS_WITH}} \text{Friend} \xrightarrow{\text{HAS_SKILL}} \text{Python}$$

In SQL, this requires joining three separate tables. In Cypher, it is a single intuitive sentence.

What happens when your query looks for a connection that simply isn’t there?

In a standard SQL database, a missing relationship wipes your record right out of the results unless you remember to write a verbose LEFT JOIN. In graphs, ignoring missing links will silently ghost your most important data points.

Here is how you handle missing relationships, shape clean profiles for your APIs or LLMs, and avoid accidentally deleting your entire database.

1. OPTIONAL MATCH: What If a Relationship Doesn’t Exist?

Suppose you want a list of every single student and the campus clubs they belong to.

The catch? Sneha isn’t in any club.

If you run a normal MATCH query, Sneha vanishes from your output entirely because the relationship pattern fails to resolve.

To prevent data from disappearing into the void, you use OPTIONAL MATCH:

Cypher:
MATCH (s:Student)
OPTIONAL MATCH (s)-[:MEMBER_OF]->(c:Club)
RETURN s.name, c.name

How It Works Conceptually:

  • MATCH → Student must exist in the graph.

  • OPTIONAL MATCH → The club relationship may or may not exist.

If a student has no club attached, Cypher doesn’t drop them—it safely returns null for the missing fields:

Sneha    null

The SQL Parallel: This is the direct graph equivalent of a relational LEFT JOIN.

2. COLLECT: Turning Scattered Rows Into Structured Profiles

By default, relational queries return multiple flat rows if a student has multiple connections (e.g., Aarav taking two subjects and joining a club yields three separate lines).

When passing graph data into an API or an LLM context window, flat rows are messy. You want clean, nested JSON structures.

COLLECT(DISTINCT ...) aggregates scattered matching nodes into an orderly list:

Cypher:
MATCH (s:Student {name: "Aarav"})
OPTIONAL MATCH (s)-[:STUDIES]->(sub:Subject)
OPTIONAL MATCH (s)-[:MEMBER_OF]->(c:Club)
RETURN 
    s.name AS name,
    s.branch AS branch,
    s.year AS year,
    COLLECT(DISTINCT sub.name) AS subjects,
    COLLECT(DISTINCT c.name) AS clubs

Instead of getting three separate fragmented rows, your query returns a single, structured profile ready for production apps:

Aarav
CSE
2
subjects: ["DBMS", "DSA"]
clubs: ["Coding Club"]

3. Ordinary Operations: ORDER BY and LIMIT

Graphs don’t replace standard data handling. You still need to sort and slice your results.

Cypher:
MATCH (s:Student)
RETURN s.name, s.year
ORDER BY s.year DESC
LIMIT 5

The execution flow is straightforward:

  1. MATCH → Find the student nodes.

  2. RETURN → Extract the fields.

  3. ORDER BY → Sort the results.

  4. LIMIT → Keep only the top N items.

The true power of a graph isn’t in sorting rows; it’s in how you discover those rows using multi-hop patterns before you sort them.

4. DELETE vs. DETACH DELETE: Handle with Extreme Care

Graph databases give you immense power over connected data, which also means they make careless mistakes catastrophic.

If you try to delete a node that still has active relationships attached:

Cypher:
MATCH (s:Student {name: "Aarav"})
DELETE s

FalkorDB will block you. It refuses to leave orphaned relationships dangling in empty space.

To force-delete a node and all its connected edges simultaneously, you use DETACH DELETE:

Cypher:
MATCH (s:Student {name: "Aarav"})
DETACH DELETE s

⚠️ The Production Warning:

DETACH DELETE is a lifesaver during local development. However, never casually execute a global detach delete like this on a live production server:

Cypher:
MATCH (n) DETACH DELETE n

That single command instantly wipes out every single node and relationship in your entire database. Graph databases make connected deletions effortless—so use them with caution.

Rookie Graph Database Mistakes That Will Break Your Pipeline

One bad query can quietly duplicate half your database or crash your AI agent’s memory. Here are the 5 beginner graph mistakes that hurt way more than they look—and how to fix them right now.

Mistake 1: Using CREATE for Repeated Ingestion

  • The Trap: Running CREATE (:Student {id: "S01"}) every time your data loader syncs.

  • The Result: Duplicate nodes pile up instantly. CREATE doesn’t check if data already exists; it just blindly appends new records.

  • The Fix: Always use MERGE for ingestions. It acts as an upsert—finding the node if it exists, or creating it only if it’s missing.

    Cypher:
    MERGE (:Student {id: "S01"})
    

Mistake 2: Choosing Unstable Identifiers

  • The Trap: Merging nodes based on mutable properties like names (MERGE (:Student {name: "Aarav"})).

  • The Result: If a user updates their name or two students share the same name, your graph breaks. Names are for humans, IDs are for databases.

  • The Fix: Use permanent, unique primary keys (like UUIDs or employee IDs) as your anchor, and store names as normal properties.

    Cypher:
    MERGE (:Student {id: "S01"})
    SET s.name = "Aarav"
    

Mistake 3: Ignoring Relationship Direction

  • The Trap: Designing an outgoing arrow like (Student)-[:STUDIES]->(Subject), but later querying it backwards as (Subject)-[:STUDIES]->(Student).

  • The Result: Zero search results. Cypher arrows are strictly directional.

  • The Fix: Match your query direction to your schema logic. If a relationship needs to flow both ways during runtime searches, traverse without the directional head (-[:STUDIES]-), but keep your ingestions consistent.

Mistake 4: Returning Everything (RETURN s)

  • The Trap: Pulling entire node objects into your Python API or LLM context window (RETURN s).

  • The Result: Massive memory bloat, sluggish response times, and wasted tokens if you are feeding data into an AI agent.

  • The Fix: Explicitly pull only the specific properties you need.

    Cypher:
    RETURN s.name, s.year, s.branch
    

Mistake 5: Overcomplicating Your Schema on Day One

  • The Trap: Designing 50 different node labels and 100 relationship types before writing a single line of code.

  • The Result: Paralysis by analysis. Your graph becomes too brittle to query efficiently.

  • The Fix: Start lean. Build your foundation using 4 or 5 core entities (e.g., Student, Subject, Skill, Project), then scale out complexity organically only when a new relationship solves a real problem.

The 5 Cypher Patterns That Do 90% of the Heavy Lifting

Stop trying to memorize 50 different database commands. To master graph querying from scratch, you only need these five core traversal patterns.

Pattern 1: One-Hop Traversal (Direct Connection)

Cypher:
MATCH (s:Student)-[:STUDIES]->(sub:Subject)
RETURN sub.name
  • Mental Model: {Student} → {Subject}

  • Use Case: Finding direct attributes or immediate connections.

Pattern 2: Two-Hop Traversal (Chained Path)

Cypher:
MATCH (s:Student)-[:FRIENDS_WITH]->(f)-[:HAS_SKILL]->(sk:Skill)
RETURN f.name, sk.name
  • Mental Model: {Student} → {Friend} → {Skill}

  • Use Case: Stepping across multiple relationships to find indirect data.

Pattern 3: Shared Neighbor (Common Ground)

Cypher:
MATCH (a:Student)-[:STUDIES]->(sub)<-[:STUDIES]-(b:Student)
RETURN a.name, b.name, sub.name
  • Mental Model: {A} → {Subject} ← {B}

  • Use Case: Recommendations, shared classes, common interests, and social links.

Pattern 4: Relationship Properties (Data on Edges)

Cypher:
MATCH (s:Student)-[r:MEMBER_OF]->(c:Club)
RETURN s.name, r.role, r.since
  • Mental Model: {Node} → {Relationship + Data} → {Node}

  • Use Case: Extracting metadata stored directly inside the connection (like roles, grades, or dates).

Pattern 5: Multi-Hop Reasoning (Knowledge Graphs)

Cypher:
MATCH (p:Project)-[:REQUIRES]->(sk:Skill)<-[:HAS_SKILL]-(s:Student)
RETURN p.name, sk.name, s.name
  • Mental Model: {Project} → {Skill} ← {Student}

  • Use Case: Complex matchmaking, dependency routing, and AI reasoning chains where graphs shine brightest.

From a Single Line to a Connected Brain: What We Built (and What’s Next)

You didn’t just learn 15 Cypher commands or a bunch of random syntax snippets today. You adopted a brand-new mental model.

Traditional databases ask: “Where is my data?”

Graph databases ask: “What is connected to what?”

Here is the entire anatomy of what we built:

                         ┌─────────────┐
                         │    Python   │
                         └──────▲──────┘
                                │
                           HAS_SKILL
                                │
┌─────────┐    STUDIES    ┌─────┴─────┐
│  Aarav  ├──────────────►│    DSA    │
└────┬────┘               └─────┬─────┘
     │                           │
     │ MEMBER_OF                 │
     ▼                           │
┌─────────────┐                  │
│Coding Club  │                  │
└─────────────┘                  │
                                 │
                         PREREQUISITE_FOR
                                 │
                                 ▼
                         ┌──────────────┐
                         │      OS      │
                         └──────────────┘

And across our network, we connected students, subjects, clubs, skills, projects, and prerequisites—allowing us to answer multi-hop questions that weren’t even explicitly stored as raw facts.

Why We Kept Part 1 Strictly AI-Free

Everything we built today used zero AI:

  • No LLM

  • No embeddings

  • No vector database

  • No agent

Why? Because if you let an AI generate Cypher queries before you understand how graphs work yourself, debugging becomes an absolute nightmare. When something breaks, you won’t know if the fault lies in your schema, your query, the LLM, or the database.

Now you own the foundation.

What’s Coming in Part 2: Giving the Graph a Brain

In the next part, we take this exact college graph and turn it into a production-grade Agentic RAG pipeline.

We will layer in 30+ complex academic concepts (Normalization, 1NF, 2NF, 3NF, BCNF, Indexing, Query Optimization, Transactions) and connect them to real notes. Then, we’ll let users ask natural language questions like:

“I’m struggling with database normalization. What should I learn before BCNF, and who in our network is strong at the relevant topics?”

Our architecture will shift into a live agentic loop:

Natural Language 
       │
       ▼
     LLM 
       │
       ▼
 Cypher Query 
       │
       ▼
   FalkorDB 
       │
       ▼
 Graph Results 
       │
       ▼
     LLM 
       │
       ▼
 Human Answer

And that is where engineering reality kicks in. We will confront real-world production blockers head-on:

  • What happens when the LLM generates bad Cypher?

  • What do you do when rate limits hit?

  • How do you handle empty graph results or confident hallucinations?

The foundation is built. In Part 2, we write the brain. See you there.

Logic Lama
Neural Ninjas
// Continuing from this article

Your Neural Path

0 0 votes
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
trackback
2 days ago

[…] Part 1, we built a rock-solid college knowledge graph using FalkorDB with 20 nodes and 39 relationships. […]

// STILL BROWSING?
Build along, don't just read.
Get labs & articles matched to what you're into — free, takes 30 seconds.
Start building free