Agentic AI

Game of Thrones GraphRAG: Master Multi-Hop Querying with FalkorDB

Jon Snow is Daenerys’s nephew — but your Vector RAG can’t figure it out. Here’s how multi-hop GraphRAG with FalkorDB actually works (with real bugs and fixes).

The Question That Broke My Chatbot

Last month, I asked my RAG chatbot a simple question:

“How is Jon Snow related to Daenerys Targaryen?”

It gave me a confident, well-written answer: “Jon Snow and Daenerys Targaryen are both characters in Game of Thrones. They share a complex relationship involving dragons, the Iron Throne, and the Long Night.”

Translation: it had no idea.

It retrieved five text chunks containing “Jon Snow” and “Daenerys Targaryen.” Every chunk looked relevant. Not a single one contained the answer.

Because the answer wasn’t in any chunk. It was between them.

Jon Snow’s father is Rhaegar Targaryen. Rhaegar’s sister is Daenerys. That’s it. Two facts, sitting in two different documents. And my smart, expensive, vector-search-powered chatbot couldn’t connect them.

That’s the moment I fell down the multi-hop rabbit hole — and built an entire Westeros detective project to understand it inside-out.

This article is everything I learned. No jargon. No PhD. Just a curious brain, a graph database, and a lot of broken code.

Grab a drink. Let’s break some things.

Wait… What the Heck Is a ‘Hop’?

Forget graphs. Think of your family.

  • 0 hops: You. Just you. Alone in the world.

  • 1 hop: Your dad. Or your mom. One connection away.

  • 2 hops: Your dad’s sister. You → Dad → Aunt. Two jumps.

  • 3 hops: Your dad’s sister’s husband. You → Dad → Aunt → Uncle.

Every jump from one person to another is a hop.

That’s it. That’s the whole idea.

Now — imagine your family tree isn’t drawn on paper. It lives inside a database that understands relationships. Not “documents that mention people.” Actual connections.

That database is a graph database. And when you ask it a question that needs 2, 3, or 4 jumps to answer — that’s multi-hop querying.

In FalkorDB (the one I used), a single hop looks like this:

cypher:
-- 1 hop: Who is Jon's father?
MATCH (a:Character {name: 'Jon Snow'})-[:FATHER_OF]->(b)
RETURN b.name
-- Result: Rhaegar Targaryen

Two hops?

cypher:
-- 2 hops: Who is Jon's aunt?
MATCH (a:Character {name: 'Jon Snow'})-[:FATHER_OF]->()-[:SIBLING_OF]->(b)
RETURN b.name
-- Result: Daenerys Targaryen

See that? The answer wasn’t in a document. It was along the way.

That’s multi-hop.

Why Your Normal RAG Chatbot Can’t Do This ?

Here’s the painful truth about standard RAG (the “flat” kind everyone builds):

It searches by similarity, not by connection.

When you ask “How is Jon Snow related to Daenerys?”, flat RAG does this:

  1. Convert your question to a vector (a list of numbers)

  2. Find document chunks with similar vectors

  3. Feed those chunks to an LLM

  4. Hope the LLM figures it out

It returns chunks that mention Jon Snow. Chunks that mention Daenerys. Maybe even a chunk that mentions both.

But it will never return the path between them. Because it doesn’t know paths exist.

Look at this comparison:

# FLAT RAG: similarity search
vector_db.similarity_search("How is Jon related to Daenerys?", k=5)
# Returns: 5 chunks mentioning Jon, Daenerys, or Targaryen
# But NO chunk says "Jon is Daenerys's nephew"

# MULTI-HOP GRAPH RAG: path traversal
MATCH path = (a:Character {name: 'Jon Snow'})-[*1..4]-(b:Character {name: 'Daenerys'})
RETURN path
# Returns: Jon → Rhaegar → Daenerys
# The answer IS the path

This one difference changes everything.

Flat RAG is a librarian who finds books mentioning your topic. Multi-hop Graph RAG is a detective who follows the actual trail.

Problem #1: The Path Explosion That Killed My Server

Here’s where things get ugly.

I built my graph. 2,134 characters from Game of Thrones. I asked my first multi-hop question. And my server died.

Not crashed. Not errored. Just… stopped responding.

Here’s why.

Every character in a family tree has multiple connections. Parents, siblings, spouses, children. Average: about 10 connections per person.

Now do the math:

  • 1 hop: 10 paths

  • 2 hops: 100 paths

  • 3 hops: 1,000 paths

  • 4 hops: 10,000 paths

  • 5 hops: 100,000 paths

  • 6 hops: 1,000,000+ paths

At depth 6, my database was trying to find a million possible paths between Jon Snow and Daenerys.

Real numbers from my project:

Jon ↔ Daenerys, depth=2: 2 paths
Jon ↔ Daenerys, depth=4: 11 paths
Jon ↔ Daenerys, depth=6: 187 paths

Wait — 187, not a million? Because my graph is small. But on a real enterprise graph (millions of nodes, dense connections), that number explodes astronomically.

This is called path explosion. And it’s the #1 killer of multi-hop systems.

How I Fixed It:

Two simple constraints:

  1. Depth limit — Never go beyond 4 hops. Period.

  2. Beam search — At each level, keep only the top-k paths. Throw the rest away.

Here’s the fix in code:

def traverse(seed, target, max_depth=4, beam_width=10):
    query = f"""
        MATCH path = (a:Character {{name: $seed}})-[*1..{max_depth}]-(b:Character {{name: $target}})
        RETURN path LIMIT 1000
    """
    result = graph.query(query, {"seed": seed, "target": target})
    
    # Keep only top-k paths by score
    scored = [(path, score_path(path)) for path in result.result_set]
    scored.sort(key=lambda x: x[1], reverse=True)
    return [p for p, s in scored[:beam_width]]

Before: 187 paths explored, 187 returned. Slow. Chaotic.
After: 187 paths explored, 10 returned. Fast. Focused.

Depth limit + beam search. That’s the seatbelt.

Problem #2: Shortest Path Is Not Always the Best Path

Here’s something that took me embarrassingly long to figure out.

I asked: “How is Arya Stark related to Tyrion Lannister?”

The shortest path my graph found was:

Arya → Hot Pie → Cook → Tyrion

Hot Pie. A random baker. Met Arya once. Worked at a castle. Technically connected to Tyrion.

Is this the answer a human wants?

Absolutely not.

The actual answer is: “Arya is Tyrion’s sister-in-law. Tyrion married Sansa, Arya’s sister.”

But that path is longer. And a naive traversal returns the shortest one first.

The Fix: Score Paths, Don’t Just Find Them

Not all hops are equal. A father connection is stronger than a “met once” connection.

So I gave every relationship type a weight, and scored paths:

EDGE_WEIGHTS = {
    "FATHER_OF": 1.0,     # strong
    "MARRIED_TO": 0.9,
    "SIBLING_OF": 0.8,
    "MET_ONCE": 0.1,      # weak
}

def score_path(path, decay_factor=0.8):
    score = 1.0
    depth = len(path) - 1
    score *= decay_factor ** depth       # longer = lower score
    for edge in path:
        score *= EDGE_WEIGHTS.get(edge.type, 0.5)
    return score

Now look at what happens:

  • Path A: Arya → Hot Pie → Cook → Tyrion (3 hops, weak edges)
    Score: 0.8³ × 0.1 × 0.1 = 0.0005

  • Path B: Arya → Sansa → Tyrion (2 hops, strong edges)
    Score: 0.8² × 0.8 × 0.9 = 0.46

Path B wins by 900x. And Path B is the right answer.

Lesson: Shortest ≠ best. Score your paths, rank them, return the top ones.

Problem #3: Targaryens Broke My Graph (Cycle Detection)

Targaryens married their siblings. For 300 years.

You know what that does to a family tree?

It creates cycles.

Look at this:

Aerys II → married → Rhaella (his own sister)
Rhaella → mother of → Daenerys
Daenerys → descendant of → Aerys II

Wait, that loops back. Aerys married Rhaella. Rhaella is Daenerys’s mother. Daenerys descends from Aerys. So Aerys is both Daenerys’s father and her uncle. Because he married his sister.

Cycles. Everywhere.

A naive traversal follows these cycles forever. Jon → Lyanna → Rhaegar → Jon → Lyanna → Rhaegar →…

Infinite loop. Server dead. Again.

The Fix: Visited-Set Tracking

Simple rule: If you’ve already visited a node in this path, stop.

def detect_cycle(nodes):
    """True if any node appears twice in the path."""
    return len(nodes) != len(set(nodes))

# During traversal:
for path in result:
    if detect_cycle(path):
        stats["cycles_detected"] += 1
        continue  # Skip it
    valid_paths.append(path)

Real numbers from my Targaryen query:

  • Aerys II ↔ Daenerys: 34 paths explored

  • 24 were cyclic (pruned)

  • 10 were valid (returned)

Without cycle detection: server hangs.
With it: 10 clean paths in 0.034 seconds.

Lesson: Family trees are graphs, not trees. Cycles are guaranteed. Handle them.

Problem #4: Direction Matters More Than You Think

Here’s a subtle one that broke my brain.

I asked: “Who are Jon Snow’s ancestors?”

The graph returned: Robb, Sansa, Arya, Rickon, Jon Snow…

Wait — Robb and Sansa are Jon’s siblings, not his ancestors. And Jon Snow is himself?!

What went wrong?

I used an undirected traversal. It walked in both directions — up (to parents) and sideways (to siblings). It mixed everything up.

The Fix: Direction-Aware Traversal

There are three modes:

cypher:
-- INCOMING: walk UP (ancestors only)
MATCH (a:Character {name: 'Jon Snow'})<-[:FATHER_OF|MOTHER_OF*1..4]-(b)
RETURN DISTINCT b.name
-- → Rhaegar, Lyanna, Aerys II, Rhaella, Rickard, ...

-- OUTGOING: walk DOWN (descendants only)
MATCH (a:Character {name: 'Jon Snow'})-[:FATHER_OF|MOTHER_OF*1..4]->(b)
RETURN DISTINCT b.name
--(empty — Jon has no children)

-- UNDIRECTED: mixes everything
MATCH (a:Character {name: 'Jon Snow'})-[*1..4]-(b)
RETURN DISTINCT b.name
--25+ nodes including siblings and in-laws

Same node. Same graph. Three different queries. Three different answers.

Lesson: Direction is not optional. In genealogy, relationships flow one way. Walk with intention.

The Complete Multi-Hop Pipeline (Everything Connected)

Here’s the full flow, start to finish. This is what runs behind the scenes every time you ask a multi-hop question:

def multi_hop_query(user_query, max_depth=4, beam_width=10):
# Step 1: Is this single-hop or multi-hop?
query_type = classify_query(user_query)
# → “multi-hop”

# Step 2: Extract the two entities (people) from the query
seed, target = extract_entities(user_query)
# → (“Jon Snow”, “Daenerys Targaryen”)

# Step 3: Traverse the graph with safety nets
result = engine.traverse(
seed=seed,
target=target,
max_depth=max_depth, # prevents explosion
beam_width=beam_width, # keeps top-k paths
detect_cycles=True, # prunes loops
)

# Step 4: Score and rank paths
# (done inside traverse)

# Step 5: Feed ranked paths to an LLM
context = format_paths(result[“paths”])
response = llm.invoke([
{“role”: “system”, “content”: “Explain the relationship based on graph paths.”},
{“role”: “user”, “content”: f”{user_query}\n\nPaths:\n{context}”}
])

return response, result[“stats”]

# Input: “How is Jon Snow related to Daenerys?”
# Output: “Jon Snow is Daenerys’s nephew through her brother Rhaegar.”

That’s the whole thing. Five steps. Every problem from earlier — explosion, ranking, cycles, direction — solved inside step 3.

The LLM doesn’t find the answer. The graph finds it. The LLM just explains it.

What I Learned :

Multi-hop querying in Graph RAG is not magic. It’s path finding with guardrails.

You need:

  • A graph, not a pile of documents

  • Depth limits, so it doesn’t explode

  • Beam search, so it returns the best paths, not the most

  • Cycle detection, so it doesn’t loop forever

  • Direction awareness, so it walks the right way

  • Scoring, so shortest ≠ best

Without these, multi-hop is a ticking time bomb. With them, it’s the most powerful retrieval pattern you’ll use in 2026.

Flat RAG retrieves chunks.
Multi-hop Graph RAG connects dots.

And in a world where the answer is increasingly between documents — not inside them — that difference is everything.

Logic Lama
Neural Ninjas
// Continuing from this article

Your Neural Path

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
// STILL BROWSING?
Build along, don't just read.
Get labs & articles matched to what you're into — free, takes 30 seconds.
Start building free