Beyond Flat RAG: Structure-Aware Graph Expansion for Multi-Hop Reasoning with GraphSAGE A working example project called GraphSAGE-GraphRAG demonstrates that structure-aware retrieval using a Neo4j knowledge graph and an offline-trained GraphSAGE GNN can solve multi-hop reasoning that flat vector RAG fails on. In a 16-document SkyLink Airlines flight-operations corpus, flat RAG retrieved four lexically similar chunks (inc-2209, inc-2209-rca, inc-1187, team-groundops) and incorrectly named Team Ground Operations as both the cause of incident INC-2209 and the team to page, while GraphRAG surfaced seed entities N882AA, INC-2209, and Flight 202 by graph structure and reached the capa-turnaround runbook that names the correct team. The project cites the SAGE (Structure Aware Graph Expansion) framework and an arXiv paper at arxiv.org/abs/2602.16964, with code hosted at github.com/h-swathi-shenoy/graphsage-graphrag. When one chunk isn’t enough GraphSAGE walks the chain that flat RAG can’t see GraphSAGE-GraphRAG is a working example of Structure-Aware Retrieval solving the multi-hop retrieval problem that flat vector RAG cannot. While traditional RAG treats document chunks as isolated units ranked purely by semantic similarity, this project builds a knowledge graph in Neo4j and trains a GraphSAGE GNN on it offline, so every node’s embedding absorbs its neighborhood before a single question is ever asked. This approach echoes the ideas behind the excellent SAGE Structure Aware Graph Expansion framework and the research paper it’s built on: https://www.arxiv.org/abs/2602.16964 https://www.arxiv.org/abs/2602.16964 . https://github.com/h-swathi-shenoy/graphsage-graphrag/tree/main https://github.com/h-swathi-shenoy/graphsage-graphrag/tree/main Flat RAG works by embedding every chunk of text independently and retrieving whichever chunks are semantically closest to the question. That works fine when the answer lives inside one document. It breaks down the moment the answer is a chain of facts spread across multiple documents , none of which reference each other in a way that text embeddings can pick up. The Dataset This project ships with a small, self-contained example knowledge base 16 short documents describing the flight operations of a fictional airline, SkyLink Airlines. It’s deliberately small so the multi-hop chain is easy to follow by hand, but it’s structured the way a real operations knowledge base is: a mix of entities flights, aircraft, systems, teams, policies and incidents that reference them indirectly. What’s in the corpus: • Flights & aircraft: Flight 202 ORD → LAX , flown by Aircraft N882AA , which is shared across two other routes on rotation • Systems: ramp-scheduler gate/ground scheduling, 8-slot-per-hour cap , crew-scheduler pilot/crew assignment, FAA duty-time rules • Teams: Team Flight Operations , Team Ground Operations , Team Airport Operations , Team Crew Scheduling each owns a different piece of the puzzle • Policies & changes: SP-118 a staffing-policy change , PL-FAA-7 a safety policy it violated • Incidents: INC-2209 the flight delay and its root-cause analysis INC-2209-rca , plus an unrelated earlier incident INC-1187 for contrast • Runbooks: capa-turnaround , capa-crewhold the “what to do” documents that operators actually consult Each document is written the way real operational documentation is written: focused on its own system or event, with almost no cross-referencing. The ramp-scheduler doc never mentions Flight 202. The incident report never explains why the scheduler ran out of capacity. That’s the whole point the corpus is designed so that no single chunk answers a root-cause question, forcing retrieval to either get lucky on lexical overlap flat RAG or actually traverse relationships GraphRAG . Lets look at the following concrete example: Flight-delay incident INC-2209 When asked “Which team should be paged for INC-2209, and which team actually caused it?” and F lat vector RAG retrieves the four chunks that lexically resemble the question inc-2209 , inc-2209-rca , inc-1187 , team-groundops because they all contain words like “team,” “incident,” and “caused.” It never retrieves capa-turnaround , the runbook that actually names the correct team to page, because that document shares almost no vocabulary with the question. The result isn’t just incomplete it’s factually wrong: the LLM confidently names Team Ground Operations for both halves of the question, when in reality that team caused the incident but isn’t who gets paged. GraphRAG starts from the same question but asks a different kind of question of the data: not “what sounds similar?” but “what is this incident connected to?” By attaching the question as a virtual node and running it through GraphSAGE, it surfaces seed entities based on graph structure N882AA , INC-2209 , Flight 202 , then walks two hops in Neo4j to reach ramp-scheduler → OWNED BY → Team Airport Operations and, separately, SP-118 → TRIGGERED BY → Team Ground Operations . Neither of those two facts lives in the same document, and neither is reachable by similarity search alone — only by following the relationship edges between them. That’s the core failure mode this diagram illustrates: flat RAG is a similarity search, not a reasoning process. It fails silently with no low-confidence signal whenever the correct answer depends on a connection between two facts that don’t happen to use similar words. Getting Started The full source is on GitHub, and you can reproduce every screenshot in this article on your own machine in about two minutes of actual runtime plus setup : 1. git clone