Where we left off

Every piece is now on the table: extraction (Lessons 3-4), a graph (Lesson 5), and traversal (Lesson 6). This lesson assembles them into the thing this whole course has been building toward: given a question, find a starting entity mentioned in it, traverse outward to gather connected facts, and hand those facts to Gemini to generate a grounded answer. This is Graph RAG's version of Naive RAG's retrieve-then-generate loop, with traversal standing in for similarity search.

Why "start from an entity in the question" works

You might assume the graph needs to be searched exhaustively, checking every node to see if it's relevant. It doesn't, and this is worth understanding precisely: a question names its own starting point. "Who recalibrated the sensor that Dev flagged as drifting in the greenhouse?" mentions "sensor" and "greenhouse" directly. Pick either one as the starting node (this lesson uses "humidity sensor," found by a simple keyword match against the graph's node names, a technique Lesson 10 replaces with something sturdier), then traverse outward. Two hops from "humidity sensor" reaches "greenhouse" (one hop), "Mia," "Dev," and "multimeter" (via the reverse edge back from the sensor and onward), which is precisely the set of facts the question needs, without ever having to inspect the parts of the graph about Priya's book club or Mia's woodworking log.

The code, piece by piece

def gather_facts(graph: Graph, start: str, max_hops: int = 2) -> list[str]:
facts = []
frontier = {start}
for _ in range(max_hops):
next_frontier = set()
for node in frontier:
for relation, other in graph.get(node, []):
facts.append(f"{node} {relation} {other}")
next_frontier.add(other)
frontier = next_frontier
return facts

This is a slightly more general version of Lesson 6's two_hop: instead of a fixed two nested loops, it walks outward one "frontier" (the set of nodes reached so far) at a time, for max_hops rounds. Each fact gets turned into a plain sentence (f"{node} {relation} {other}"), because that's a format Gemini can read directly in a prompt, same idea as turning retrieved chunks into prompt text in naive_rag.

context = "\n".join(facts)
prompt = f"""Answer the question using only the facts below...
Facts:
{context}
Question: {query}"""

Structurally identical to naive_rag's generation step, the only difference is what fills in context: traversed graph facts instead of retrieved chunks of prose. Generation itself doesn't need to know or care which kind of retrieval produced its input.

Checkpoint

  • Graph RAG's retrieve-then-generate loop: find a starting entity in the question, traverse outward to gather connected facts, generate from those facts, exactly parallel to Naive RAG's embed-retrieve-generate loop.
  • A question names its own starting point; traversal doesn't need to search the whole graph, only walk outward from there.
  • Generation doesn't care whether its input came from similarity search or graph traversal, it just needs facts formatted as readable text.

Change the starting entity from "humidity sensor" to "Dev" and rerun. Does traversal still reach Mia and the multimeter within two hops? If not, how many hops does it actually take, and what does that tell you about how much the choice of starting entity matters?

If anything here still feels unclear, ask before moving to Lesson 8.