Study Notes
Notecore

Knowledge Graph Primitives & Graph RAG with Neo4j

Hands-on construction of a property graph — nodes, edges, labels, and Cypher queries in Neo4j — then wiring it into a Graph RAG pipeline: entity extraction, community detection, and grounding LLM answers in structured graph context.

30 min read·verified Content review 2026·reviewed 2026-08-30

After this you should be able to

  • Explain the core idea in Knowledge Graph Primitives & Graph RAG with Neo4j
  • Apply Knowledge Graph Primitives & Graph RAG with Neo4j in a representative exercise
Neo4j / CypherProperty GraphCommunity Detection

Study Guide · Session Notes

Why vector search and SQL both break on "who is the CEO of the parent company that acquired the startup Tony mentioned?" — and how a labeled property graph, Cypher, and a hybrid Graph RAG pipeline answer it in milliseconds. Includes a revision of the Alpha Fund hybrid tool-calling dashboard.

Graph RAG · Revision Neo4j · Cypher · APOC Graph RAG · LPG model Tool calling · LangChain Neo4j · Cypher
⭐ Trick ⚠ Pitfall 💡 Tip [+] added, not in source [!] source conflict flagged
Section 1 — Notebook Summary

Hand-copyable revision sheet#

Telegraphic. Copy straight into your notebook. Everything here is derived from the Detailed Notes below.

A · Why graphs (the two failures)
  • Multi-hop Q → "CEO of parent co. that acquired startup Tony mentioned in Q3 report"
  • FAISS fails → embeds query as vector; returns disconnected chunks. No direction/lineage.
  • 💡cosine = distance, NOT lineage
  • SQL fails → 5-hop query = 5 JOINs at runtime; scans millions of rows; 1 bad FK → 0 rows
  • ⚠SQL good @ aggregation (sum/min/max), nightmare @ deep multi-hop
  • CEO? Tony ? FAISS — floats apart SQL — tangled JOINs
B · Graph paradigm
  • Store as web → subject → predicate → object (triples)
  • ⭐Relationships precomputed on disk, NOT built at runtime (unlike SQL JOINs)
  • Query → land on node → walk edges in ms → deterministic, 100% accurate
  • Tony Satya rolls the maze, edge by edge
C · LPG model (Neo4j core structure)
PartIs a…Example
NodeNoun / entityCompany, Executive
EdgeVerb / rel. (directional)ACQUIRED, WORKS_FOR
Propertykey:value metadataname:"MSFT"
  • Properties schema-free · on nodes OR edges · every node can differ
  • 💡"nodes = nouns" is a best practice, not a law (e.g. txn ID can be a node)
D · Cypher = "SQL for graphs"
  • ( ) node   [ ] edge   --> direction
  • Pattern → (e:Executive {name:'Tony'})-[:WORKS_FOR]->(c:Company)
  • e = alias · :Executive = label · { } = property
  • MATCH = SELECT (find geometric pattern) · RETURN = output cols
  • ⭐NO JOINs — you describe the shape, engine walks to it
  • Tony WORKS_FOR Alpha
E · MERGE vs CREATE (ingestion)
CREATEMERGE
Actionalways makes new nodefind-or-create (upsert)
MSFT ×2020 dupes → shattered1 node, all link to it
  • ⚠CREATE in ingestion loop = disconnected fragments = traversal breaks
  • MERGE = "gravitational pull" — one MSFT node binds everything
F · Graph RAG architecture (hybrid)
  • 1. FAISS finds the starting node (Tony out of 10k employees)
  • 2. Graph traversal pulls connected entities in N-hop radius (2-hop)
  • 3. Hand subgraph → LLM to build answer
  • ⭐Don't throw away FAISS — vector for entry, graph for relationships
  • FAISS 2-hop fan-out LLM
G · Deployment (Docker + Neo4j)
  • Docker Compose · pin version (never :latest) · infra-as-code
  • Volumes = "hole in the ship" → data persists on local disk after container dies
  • 4 folders: data · logs · import · plugins
  • Ports → 7474 HTTP (human browser) · 7687 Bolt (Python/machine driver)
  • APOC = Awesome Procedures On Cypher → JSON parsing for LLM integration
  • ⚠Never f-string into Cypher → use parameterized injection (Cypher-injection risk)
  • Constraints = uniqueness lock (like SQL primary key): UNIQUE company
H · Alpha Fund revision (hybrid tool calling)
  • Hybrid engine → BM25 (lexical, exact IDs) + FAISS (semantic)
  • bind_tools() → LangChain routes to SQL tool OR vector tool
  • ⭐LLM picks tool from the docstring — no docstring, no routing
  • Agent loop → while True: call tool → observe → repeat until done
  • 💡LLM reads all tool names+descriptions at once, doesn't for-loop them
  • ⚠50 tools = overwhelming → hallucination. Fix: classification/router LLM or mini vector-DB of tools → top-5
  • temp=0 for deterministic output · GPT-4o-mini fallback model

Section 2 — Detailed Study Notes

The complete reference#

Start here — the problem that motivates everything A user types one sentence into your finance assistant: "Who is the CEO of the parent company that recently acquired the startup Tony mentioned in his Q3 compliance report?" Your beautiful vector search returns a pile of true-but-unrelated snippets — one about an acquisition, one that mentions Tony, one about a Q3 report — and the model confidently stitches them into nonsense. Your SQL fallback needs five JOINs and hallucinates one foreign key, so it returns zero rows. Nothing you've built so far can actually follow the chain. That chain — that lineage — is exactly what a knowledge graph stores, and it's why this material exists.

01Revision — the Alpha Fund hybrid tool-calling dashboard #

Before graphs, a recap of the Week-4/5 project so the pieces connect. The Alpha Fund Intelligence dashboard is a Streamlit app that answers questions about financial data by picking the right retrieval tool for each query.

A hybrid engine is a search system that runs two complementary retrieval methods and combines their strengths.

It pairs BM25 — the "find the exact word" method (lexical search, ideal for precise things like a transaction ID) — with FAISS — the "find the thing that means the same" method (semantic/contextual search over embeddings). BM25 catches exact matches; FAISS catches meaning.

◆ Analogy

BM25 is Ctrl-F — it finds the literal string you typed. FAISS is a librarian who read every book — ask for "companies worried about margins" and she brings back the right passages even if none contain that phrase. A good assistant keeps both on staff.

How the tool calling actually works#

Tool calling is letting the LLM decide which of several functions to run, based on the user's question.

LangChain's bind_tools() attaches your functions to the model. Here there are two: query_sp500_financials (runs SQL against an S&P 500 financials table) and search_earnings_transcript (does a similarity search over 2024 earnings-call transcripts stored in FAISS). The LLM reads the request and routes to one, the other, or both.

User query "compare NVDA vs AAPL…" Orchestrator bind_tools() · while True query_sp500_financials() SQL → S&P 500 table search_earnings_transcript() FAISS → transcript chunks docstring picks →
The orchestrator reads each tool's docstring, routes the query, and loops until it has enough to answer.
⭐ Trick — the docstring IS the router

The LLM decides which tool to use by reading each function's docstring written in plain English. No docstring → LangChain tool calling "will not work properly." The best practice is a clear, plain-language docstring plus schema hints (column names/types), because that plain text is the semantic signal the model routes on.

The orchestration loop#

The run_agent function is a while True loop implementing agentic reasoning: the LLM calls a tool, observes the result, appends it to the message list, then decides whether it needs another tool. When ai_message.tool_calls comes back empty (None), it stops and emits the final HumanMessage/answer. A single-tool question iterates ~twice (call, then confirm done); a "boss fight" query that hits both tools iterates more.

💡 Tip — the model doesn't loop over 50 tools

A common misconception: that the LLM checks tools one-by-one in a for-loop. It doesn't. When you bind N tools, the framework passes the list of names + descriptions to the model, which evaluates them simultaneously and decides which are relevant. "Reading a tool and using it are different things" — it reads all descriptions at once, then executes the chosen ones.

⚠ Pitfall — 50 tools overwhelm the context window

Too many tools crowd the model's context and cause hallucination / wrong-tool picks. The fix is a filtering layer before the main LLM: either a small classification/routing agent that only holds tool names+descriptions and maps a query to a tool, or a mini vector DB of tool descriptions from which you retrieve the top ~5 relevant tools dynamically. This scales even to 1,000 tools.

Other mechanics worth remembering: temperature=0 for deterministic, context-faithful answers; a fallback model (GPT-4o-mini) if the chosen OpenRouter model has no endpoint; the FAISS index built with all-MiniLM-L6-v2 embeddings and a recursive splitter (chunk_size=600, overlap=80); and executemany to bulk-insert SQL rows in one shot.

◆ Why it matters

This dashboard is exactly the system that's about to fail on the multi-hop question. Hold onto two ideas: (1) hybrid = the right retriever per query, and (2) the docstring/description is how routing decisions are made. Graph RAG will reuse both — FAISS still finds the entry point, and the graph replaces the "stitch disconnected chunks" step that breaks next.

02The relational blind spot — why FAISS and SQL both break #

Take the motivating query again: "Who is the CEO of the parent company that recently acquired the startup Tony mentioned in his Q3 compliance report?" This is a multi-hop question — the answer isn't in one chunk or one row; you have to hop across several linked facts. Both existing tools collapse on it, for different reasons.

Failure 1 — FAISS has no sense of direction#

When FAISS embeds the query, it breaks it into semantic pieces — CEO, parent company, acquired, startup, Q3 compliance — and pulls back chunks that each resemble one of those pieces: a chunk saying "Microsoft acquired Nuance," another saying "Tony submitted the Q3 report on Friday." All true, all relevant-looking, all disconnected.

⚠ Pitfall — cosine similarity measures distance, not lineage

Vectors capture how alike two things are, never how they're connected. FAISS hands the LLM a pile of facts with no mathematical link saying "this Microsoft is the parent of that startup." There's no directionality — nothing encodes "A acquired B" vs "B acquired A." Pass this multi-hop query to a pure semantic router and the whole system collapses.

FAISS returns nearest chunks — but no edges between them "MSFT acquired Nuance" "Tony filed Q3 report" "Q3 revenue up 12%" ? ?
Three true chunks floating apart. The "?" is the missing link cosine similarity can never supply.

Failure 2 — SQL chokes on deep multi-hop mapping#

SQL is brilliant at aggregation (sum, min, max over rows) but a nightmare at deep relationship mapping. To answer the question, the LLM must generate something like: SELECT name FROM executives JOIN companies … JOIN acquisitions … JOIN companies … JOIN reports … JOIN employees … — five hops, five JOINs.

ProblemWhat goes wrong
Runtime cost5 JOINs computed at query time; on a 5-million-entity DB this scans and cross-references millions of rows across tables — "computationally catastrophic."
Schema fragilityThe LLM must guess the exact schema perfectly. Miss one foreign key or table name and the query returns zero rows — a silent, confident failure.
◆ Why it matters (interview-relevant)

"When would you not use a vector DB or SQL?" is a real interview question. The crisp answer: vectors lose direction/lineage; SQL loses performance and reliability as relationship depth grows. Multi-hop relationship questions are the signature use case for a graph. This is the exact pain point Graph RAG is designed around.

03The graph paradigm — precomputed connectivity#

A knowledge graph stores data as a web of relationships that are computed once and saved to disk, not rebuilt for every query.

You store facts as triples — subject → predicate → object (e.g. Tony — WROTE — Q3 report). The relationships are precomputed and physically stored on the hard drive, so they already exist before any question is asked. This is the opposite of SQL, where the LLM generates a query and the JOINs (the relationships) are assembled at runtime.

◆ Analogy

SQL is asking for directions and having someone calculate the route from scratch every single time. A graph is a subway map already printed on the wall — the connections are drawn once; you just trace the line with your finger. Tracing is instant; recomputing the whole city's roads is not.

When the AI queries the graph it doesn't scan millions of rows. It lands on the node (say, Tony) and walks the edges to neighboring nodes — physically hopping node → edge → node. That walk finishes in milliseconds and the output is deterministic, 100% accurate: it arrives exactly at Executive: Satya Nadella.

⭐ Trick — connectivity is precomputed, so traversal is nearly free

The whole speed advantage comes from when the relationships are built. Because edges live on disk already, the engine never pays JOIN cost at query time — it just walks pointers. The multi-hop Cypher query that would be five brutal SQL JOINs executes in about 5 ms.

SQL — built at RUNTIME GRAPH — precomputed on DISK recomputed every query · scans rows Tony Doc Startup Satya walk edges · ~5 ms · deterministic
Left: SQL rebuilds tangled JOINs every time. Right: the graph's edges already exist; the engine just walks them.

04The Labeled Property Graph (LPG) model#

The Labeled Property Graph is the data structure Neo4j is built on: a system of nodes, edges, and properties.

Everything in Neo4j reduces to three primitives. Learn these three and the rest is syntax.

PrimitiveGrammar roleWhat it isExamples
NodeNounA physical or conceptual entity. Every node carries a label.:Company, :Executive, :Document
EdgeVerb [!]A directional relationship connecting two nodes.ACQUIRED, WORKS_FOR, MENTIONS
PropertyAdjectiveKey–value metadata. Can sit on a node or an edge. Schema-free.name:"MSFT", market_cap:…
⚠ [!] Wording to keep straight — "edges = verbs", not adjectives

Edges are sometimes loosely called "adjectives" and sometimes "verbs." The internally consistent, correct mapping is: nodes = nouns, edges = verbs, properties = adjectives/metadata. Edges express actions/relationships ("acquired", "works for") — those are verbs. Use this version.

Properties are schema-free: they aren't locked to a fixed column set, and different nodes of the same label can carry different properties. A Company node might hold {name, ticker:"MSFT", market_cap}; a Document node might hold {title:"Q4 transcript"}. Edges can have properties too — e.g. a WORKS_FOR edge can carry {role:"analyst"}.

:Executive name:"Satya" :Company name:"MSFT" WORKS_FOR {role:"CEO"} NODE = noun ( ) EDGE = verb [ ] → direction PROPERTY = key:value
Two nodes, one directional verb-edge, and properties hanging off both node and edge.
💡 Tip — "nodes are nouns" is a guideline, not a hard rule

As a best practice, nouns become nodes. But it can bend to the data: in a financial dataset you might legitimately model a transaction_id as a node. Don't treat "nouns = nodes" as an absolute law; there are slight, scenario-driven differences. [+] A useful default: model something as a node when you'll want to traverse to and from it, not just read it as an attribute.

05Cypher — "SQL for graphs"#

Cypher is Neo4j's query language: it describes the shape of the data you want, and the engine walks the graph to find it.

Think of it as SQL for graphs. Its syntax is visual — the punctuation looks like the thing it represents.

SymbolMeansWhy the shape
( ) parenthesesa nodethey look like circles
[ ] square bracketsan edgewraps the relationship type
--> dash + arrowdirectionwhich way the relationship points

Inside a node you attach a label, an alias (a temporary variable, exactly like a SQL table alias), and properties in curly braces:

(e:Executive {name:'Tony'})
 │   │            │
 │   │            └─ property  (find the node whose name = Tony)
 │   └─ label      :Executive
 └─ alias e  (reuse it later, like a SQL alias)

The same for an edge — alias, type, and optional properties:

-[w:WORKS_FOR {role:'analyst'}]->
   │    │            │
   │    │            └─ edge property
   │    └─ relationship type
   └─ alias w

The two core reading commands: MATCH and RETURN#

MATCH is the Cypher equivalent of SQL's SELECT — it searches the database for a specific geometric pattern. RETURN names the output columns.

Example scenario: "every company mentioned in the Q3 compliance report."

MATCH (d:Document {title:'Q3 compliance report'})-[:MENTIONS]->(c:Company)
RETURN c.company_name
⭐ Trick — no JOINs, you just draw the shape

Notice there are no JOINs. The query literally describes the shape of the pattern — a Document that MENTIONS a Company — and the Neo4j engine walks the precomputed edges to it. This is the entire mental shift from SQL: stop assembling relationships, start describing the picture you want.

The full multi-hop query#

Now the original question becomes one readable pattern. Each hop is another -[edge]->(node) segment:

MATCH (e:Executive {name:'Tony'})-[:WROTE]->(d:Document) -[:MENTIONS]->(startup:Company) -[:ACQUIRED_BY]->(parent:Company)
MATCH (e2:Executive)-[:WORKS_FOR {role:'CEO'}]->(parent)
RETURN e2.name // parent CEO, parent.name
// → "Satya Nadella", "Microsoft"  ·  executes in ~5 ms
◆ Why it matters

Read the right-hand side of that pattern aloud: Tony wrote a document that mentions a startup acquired by a parent company whose CEO is…. The query reads like the English question. That legibility — plus zero JOINs and millisecond traversal — is the payoff of the graph model over the five-JOIN SQL nightmare.

06MERGE vs CREATE — the ingestion trap#

When you build the graph, an LLM reads many documents and extracts entities. Suppose it reads 50 news articles and encounters "Microsoft" 20 times. How you write those 20 mentions decides whether your graph is usable.

CREATE always makes a brand-new node; MERGE finds an existing node or creates it only if absent (an upsert).
CREATE ×20 → shattered MERGE ×20 → one hub 8 disconnected "MSFT" nodes MSFT 1 hub node · everything links in
CREATE in a loop scatters duplicate MSFT nodes; MERGE keeps a single hub that all documents and executives attach to.
⚠ Pitfall — CREATE in an ingestion loop shatters your graph

If you CREATE (c:Company {name:'Microsoft'}) for each of the 20 mentions, Neo4j blindly makes 20 separate Microsoft nodes. Your graph fragments into disconnected pieces and traversal fails — there's no single Microsoft to walk to.

⭐ Trick — MERGE is the gravitational hub

MERGE (c:Company {name:'Microsoft'}) first looks for a Microsoft node: if it exists, it binds to it; if not, it creates it. The result is exactly one Microsoft node acting like a "gravitational pull" that connects every document and executive. Rule of thumb: [+] use MERGE for entities that recur (companies, people) and CREATE for genuinely new, unique facts.

07Graph RAG architecture — the hybrid pipeline#

Graph RAG uses vector search to find where to start, then graph traversal to gather the connected context, then hands that subgraph to the LLM.

Crucially, you don't throw away FAISS. It's a hybrid: each tool does the part it's best at.

vector for ENTRY · graph for RELATIONSHIPS 1 · FAISS find START node pin drops on "Tony" · 1 of 10k 2 · TRAVERSE walk N-hop radius 1-hop → 2-hop fan-out 3 · LLM hand subgraph { } LLM build final answer
The doodle, drawn properly: a fuzzy vector cloud pinpoints the entry node, traversal fans out one and two hops, then the ego-subgraph is handed to the LLM.
  1. FAISS finds the starting node. Out of 10,000 employees, vector search pinpoints the exact "Tony" node the question refers to. This is the one thing vectors are great at — fuzzy entry matching.
  2. Graph traversal pulls the neighborhood. Once landed, walk the graph to pull every connected entity within an N-hop radius (here, 2 hops). This gathers the linked companies, documents, and executives the question needs.
  3. Hand the subgraph to the LLM. That small, connected subgraph — not a pile of loose chunks — becomes the context. The LLM reads a coherent web and produces the answer.
◆ Why it matters

This is the synthesis of the whole topic: FAISS solves the entry problem (which of 10k Tonys?), the graph solves the lineage problem (how are these facts connected?), and the LLM solves the language problem (turn the subgraph into a sentence). Each tool does only what it's best at — the exact hybrid philosophy from the Alpha Fund dashboard, now extended with a graph.

08Deployment — Docker, ports, APOC, safe ingestion#

Why Docker at all#

Docker runs Neo4j as an independent, reproducible service so your database doesn't vanish when your Python kernel restarts.

If the database disappears every time you restart Python, "you're building a toy, not an application." The graph DB should run independently of your code, accept secure connections, and persist data. You define it in a docker-compose.yml — infrastructure as code — so every developer gets an identical environment, and you can later ship the same container to an EC2 instance or Kubernetes without "works on my machine" problems.

⚠ Pitfall — never pin to :latest

Always pin the Neo4j image to a specific version. :latest drifts as dependencies change over time, breaking reproducibility across the team. Version-pinning is what guarantees the "identical environment" promise of infra-as-code.

Volumes — the "hole in the ship"#

Containers are ephemeral: if the container crashes, everything inside is deleted. Volume mounts map container folders to your local hard drive so the data survives. A helpful image: you "drill a hole in the Docker ship" so the Neo4j data leaks out onto your laptop and persists even if you destroy the container.

◆ Analogy

A container is a ship; whatever's on deck sinks with it. A volume is a hose running from the deck to a tank on shore — the cargo (your graph data) is stored on land, so scuttling the ship loses nothing. Four folders get mapped ashore: data, logs, import, plugins.

The two ports#

PortProtocolForUsed by
7474HTTPthe Neo4j browser — visual UI to draw graphs and write Cypher by handhumans
7687Bolta binary driver connection, like a DB connector for Pythonmachines / LangChain / scripts

Bolt is the graph analog of using psycopg/SQLite to connect a relational DB to Python. Any LangChain agent or Python script talks to Neo4j over 7687, with auth (user neo4j + password).

APOC — the LLM bridge#

APOC (Awesome Procedures On Cypher) is a Neo4j plugin that adds utilities — critically, JSON parsing needed to integrate the graph with LLMs.

Neo4j is a pure graph database, but LLM integration needs data passed as JSON. APOC handles that string manipulation and can run complex procedures, converting nodes/edges to/from JSON when building the Graph RAG database. You enable it with a plugin setting so Docker auto-downloads and installs it on boot.

Safe ingestion — parameterized Cypher#

⚠ Pitfall — never f-string data into Cypher

Just like SQL injection, Cypher injection is a real vulnerability. Never build queries with Python f-strings. Pass values as isolated parameters (e.g. $author, $startup) — a standard MLOps best practice. The query is fixed; only the parameter map changes.

# parameterized — safe
query = "MERGE (e:Executive {name: $author})"
graph_db.execute_write(query, {"author": "Tony"})

To enforce clean structure, add uniqueness constraints — the graph equivalent of a SQL primary key. Declaring company, executive, and document unique guarantees Microsoft can't be created twice, locking in the MERGE behavior at the schema level. Reads use an execute_read function (like SELECT); writes use execute_write (a DDL-style command that creates data via CREATE/MERGE).

💡 Tip — bonus: multimodal knowledge graphs

Graphs can handle images/diagrams too. Use a vision model (e.g. CLIP or GPT-4o) to parse a diagram into visual components, store each as a node with metadata (filename, plus the vector embedding) and connect them with DESCRIBES edges; keep raw images in object storage (S3/GCS/Azure Blob). Then you can ask "which services are affected if auth fails?" and the system traces dependencies through the graph. [+] CLIP/GPT-4o naming per the session's example.


Interview & Practice

Follow-up questions this material invites#

Tap a question to reveal a 2–4 sentence answer you can read aloud.

Q1Why do vector databases fail on multi-hop relationship questions?▸

Vectors encode similarity, so cosine similarity measures distance, not lineage. FAISS returns individually-relevant chunks but has no mathematical way to know that "this Microsoft" is the parent of "that startup." The LLM receives disconnected facts with no directional link, so it can't reliably chain them.

Q2SQL can express joins — why is it a poor fit for deep multi-hop queries?▸

SQL excels at aggregation but computes JOINs at runtime. A five-hop question means five JOINs scanning and cross-referencing potentially millions of rows — computationally catastrophic on large datasets. It's also fragile: the LLM must guess the schema perfectly, and one wrong foreign key silently returns zero rows.

Q3What are the three primitives of the Labeled Property Graph model?▸

Nodes (nouns/entities with labels, e.g. :Company), edges (verbs — directional relationships like ACQUIRED), and properties (schema-free key–value metadata that can sit on either a node or an edge). Neo4j is built entirely on these three.

Q4In Cypher, what's the difference between MATCH and MERGE?▸

MATCH is read-only — the equivalent of SQL SELECT, finding an existing geometric pattern. MERGE is an upsert: it looks for a node and binds to it if found, or creates it if not. MATCH queries the graph; MERGE safely builds it without duplicates.

Q5Why use MERGE instead of CREATE during ingestion?▸

CREATE always makes a new node, so extracting "Microsoft" 20 times yields 20 disconnected duplicates and a shattered, un-traversable graph. MERGE guarantees a single canonical node that acts as a hub, letting every document and executive connect to the same Microsoft. Pair it with uniqueness constraints to enforce this at the schema level.

Q6Walk through the Graph RAG pipeline end to end.▸

First, FAISS vector search finds the starting node (e.g. the right "Tony" among thousands). Second, graph traversal walks a fixed hop radius (say 2 hops) to pull every connected entity. Third, that coherent subgraph — not loose chunks — is handed to the LLM, which produces the final answer. Vectors handle entry; the graph handles relationships.

Q7What do ports 7474 and 7687 do, and why two?▸

7474 is the HTTP port for the Neo4j browser — a visual UI for humans to draw graphs and write Cypher manually. 7687 is the Bolt port, a binary protocol for machines; any Python script or LangChain agent connects through it, analogous to a database driver like psycopg for Postgres.

Q8What is APOC and why is it needed for LLM integration?▸

APOC (Awesome Procedures On Cypher) is a Neo4j plugin adding utilities like string manipulation and complex procedures. It's essential for LLM work because it handles JSON parsing — you can't pass graph data to an LLM directly, so APOC serializes nodes and edges to and from JSON. It's installed automatically on container boot via a plugin setting.

Q9Why parameterize Cypher queries instead of using f-strings?▸

Building Cypher with Python f-strings opens a Cypher-injection vulnerability, the direct analog of SQL injection — a crafted input could delete or exfiltrate data. Passing values as isolated parameters keeps the query structure fixed and the data separate, which is standard MLOps security practice.


Recap

Key takeaways#

  1. Multi-hop questions break both defaults. Vectors lose direction/lineage; SQL loses performance and reliability as JOIN depth grows.
  2. Graphs precompute connectivity on disk. Store triples (subject→predicate→object); querying is a millisecond walk, not a runtime rebuild.
  3. Three primitives: nodes (nouns), edges (verbs, directional), properties (schema-free key–value on nodes or edges).
  4. Cypher describes shapes, not JOINs. ( ) node, [ ] edge, --> direction; MATCH = SELECT, RETURN = output.
  5. MERGE, not CREATE, for recurring entities — one canonical hub node keeps the graph connected and traversable.
  6. Graph RAG is a hybrid: FAISS for the entry node → traversal for the neighborhood → LLM for the answer. Keep FAISS.
  7. Ship it right: Docker + pinned version, volumes for persistence, 7474 (human) / 7687 (Bolt), APOC for JSON/LLM, parameterized Cypher + uniqueness constraints.
  8. Alpha Fund tie-back: hybrid retrieval + docstring-driven tool routing; guard against too-many-tools with a classifier/router or a mini vector DB of tools.
A curated reference on Knowledge Graph Primitives.
Legend — ⭐ trick · ⚠ pitfall · 💡 tip · [+] added detail · [!] flagged wording conflict. Theme-aware: adapts to light & dark.

Build it · Chapter 05 · Atlas

Build it — Cypher-style queries + community detection#

Neo4j's Cypher is just pattern matching. Here it's a match by label/property and a one-hop walk along a named edge — over the same graph from Chapter 04. Then community detection falls out as connected components (union-find).

File atlas/queries.pyRun python3 -m atlas.queriesDeps Python 3 · stdlib only

atlas/queries.py — a query layer over the graph:

"""Chapter 05 — a mini query layer + community detection over the Graph.

Cypher is just pattern matching: find nodes by label/property, then walk a
named relationship. Communities are connected components — clusters that talk
to each other more than to the rest of the graph.
"""
from atlas.graph import Graph, seed_graph


def match(g, label=None, **props):
    """MATCH (n:Label {prop: value}) — return matching node ids."""
    hits = []
    for id, data in g.nodes.items():
        if label and data.get("label") != label:
            continue
        if all(data.get(k) == v for k, v in props.items()):
            hits.append(id)
    return hits


def walk(g, start, rel):
    """MATCH (start)-[rel]->(x) RETURN x — one hop along a named edge."""
    return [dst for r, dst in g.out[start] if r == rel]


def communities(g):
    """Connected components over the undirected graph = graph communities."""
    parent = {id: id for id in g.nodes}

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    for src in g.nodes:
        for _, dst in g.out[src]:
            parent[find(src)] = find(dst)

    groups = {}
    for id in g.nodes:
        groups.setdefault(find(id), []).append(id)
    return sorted(groups.values(), key=len, reverse=True)


if __name__ == "__main__":
    g = seed_graph()
    print("Companies:", match(g, label="Company"))
    print("Who does Elena work for?", walk(g, "Elena", "works_for"))
    print("Communities:")
    for i, c in enumerate(communities(g), 1):
        print(f"  {i}: {sorted(c)}")

Run it — you'll see:

Companies: ['Microsoft', 'Alpha', 'Nimbus']
Who does Elena work for? ['Nimbus']
Communities:
  1: ['Alpha', 'Elena', 'Microsoft', 'Nimbus', 'Q3 Report', 'Tony']

This is one checkpoint of Atlas — a single project built across notes 04–14. Full code, setup, and the map of all checkpoints: Atlas — Build the notes ↗.