AI Engineer World's Fair 2025

Intro to GraphRAG

Read the talk

Intro to GraphRAG: Build a Talent Assistant with Explicit Retrieval Logic

Build a skills graph in Neo4j, combine exact overlap with semantic similarity, extract people from biographies, and expose the resulting retrieval patterns to a LangGraph agent.

From a talk by Zach Blumenfeld

Before you start: Basic Python and familiarity with embeddings and LLM tool calling will help; the article introduces the Cypher patterns needed to follow the workshop.

From employee data to an assistant

How do you turn employee data into an assistant that can find skills, compare colleagues, and help assemble a team? The workshop begins with a prepared environment: participants receive numbered Post-it notes, connect to one of two Jupyter servers, and clone the workshop repository. Numbers at or below 160 use the first server; numbers at or above 201 use the second. The temporary login convention is lowercase attendee followed by the assigned number for both username and password. The slides and links are shared in the workshop GraphRAG intro Slack channel. These hosted environments last only for the session; the code remains available afterward.

Zach Blumenfeld develops the assistant in three stages: graph basics and Cypher in Neo4j, entity extraction from unstructured text, and a simple LangGraph agent with retrieval tools. This is an introductory workshop; advanced MCP integrations sit outside the main walkthrough. The prepared Jupyter servers already have the dependencies installed, while Neo4j Browser provides a separate view of the graph as it grows.

Six-part agenda covering objectives, setup, graph basics, unstructured data, GraphRAG and agents, and resources and Q&A.Open full source frame
Workshop agenda: graph basics, unstructured data, and GraphRAG with agents.

To follow the notebook setup:

  1. Open a terminal from Jupyter’s plus button and use the README’s clone command for the workshop repository.
  2. Copy ws.env into the Talent workshop directory. It supplies the database connection settings and the workshop OpenAI key.
  3. Connect Neo4j Browser using the URI, username, and password from that environment file.

The notebooks execute queries; Browser makes the returned nodes and relationships easier to inspect.

0:000:31
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

0:00 · section reference included

Put domain logic in the graph

A knowledge assistant already has a user interface, models, an agent, and tools that can reach data sources. Adding a knowledge graph introduces a shared model for information arriving from both documents and PDFs and structured sources such as CSVs or relational tables. The reason to do this is practical: if you know the kinds of questions the assistant must answer, you can encode the relevant domain relationships before retrieval begins.

For the talent assistant, the starting relationship is a person knows a skill. Expose that schema and suitable tools to the agent, and questions about talent search, skills alignment, staffing, team formation, and substitutions can become explicit retrieval operations. The graph makes the definition of a match inspectable. This becomes especially useful when a request is broken into several steps: the agent can select operations whose retrieval logic is already defined.

Neo4j represents this information as a property graph. Nodes identify people, places, or things; relationships connect them; properties describe either. A person’s name is a property, but so could be a number, an array, or an embedding vector. Cypher expresses the connections with patterns that resemble the graph itself:

cypher

MATCH (p:Person)-[:KNOWS]->(s:Skill)
RETURN p.name AS person, s.name AS skill;

Person and Skill are node labels, KNOWS is a relationship type, and p and s are variables referring to the matched nodes. The arrow makes the relationship direction explicit. This is enough syntax to read the workshop’s examples without treating the session as a complete Cypher course.

Graph traversal also works alongside search and analytics. Embeddings represent inputs numerically so that related meanings can be retrieved even when the words differ. Neo4j supports range indexes, uniqueness constraints, text search, Lucene full-text search, and approximate nearest-neighbor vector search. Vector search can provide an entry point into a graph, after which Cypher follows relationships. Graph analytics adds centrality, community detection, paths, and graph embeddings; later, the assistant will use skill communities written back into the database.

How large should that graph be? Blumenfeld recommends keeping the data model small where possible, especially for dynamic query generation. At the same time, data points that need low-latency traversal between them belong in the same graph. Graph boundaries and schema complexity are therefore separate decisions: colocate connected information, then choose labels and properties that keep its model understandable.

9:059:20
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

9:05 · section reference included

Load people, skills, and stable identities

The first notebook is in the Talent workshop directory, rather than the repository’s other workshop. After loading ws.env, it reads a table with three fields: an email address, a name, and a list of skills. The dataframe is organized into batches for loading. A fresh participant database should be empty; Blumenfeld’s already contains data from an earlier run.

Before loading, establish the identities used to match existing nodes: email for a person and name for a skill. The supporting constraints matter both for correctness and for indexed matching during repeated merges. There is one important distinction in Neo4j’s constraint semantics: uniqueness alone permits a missing property; a node key combines uniqueness and existence, while a separate existence constraint can also require the property. The intended person identity here is an email that is both present and unique.

The load operation follows the same pattern for every input row. Merge the person by email, update the name, unpack the skills list, merge each skill by name, and merge the connection:

cypher

UNWIND $rows AS row
MERGE (p:Person {email: row.email})
SET p.name = row.name
WITH p, row
UNWIND row.skills AS skill_name
MERGE (s:Skill {name: skill_name})
MERGE (p)-[:KNOWS]->(s);

$rows is a batch of records with email, name, and skills fields. Matching the person on email rather than on every attribute allows a name update to affect the existing identity. Matching skills by name lets many people connect to the same skill node.

Browser can now inspect people, skills, or complete paths. Returning a path exposes the connections directly:

cypher

MATCH path = (:Person)-[:KNOWS]->(:Skill)
RETURN path
LIMIT 25;

The demonstration shows skills including API design, Tableau, and Flask attached to people. KNOWS is not a built-in Neo4j concept: it is the domain’s chosen relationship name, and HAS could have served the same purpose. That choice becomes part of the model’s vocabulary when an LLM reads the schema and generates queries.

19:3419:49
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

19:34 · section reference included

Expand from Lucy to a local skills neighborhood

Start with an organization-wide aggregation: how many distinct people know each skill?

cypher

MATCH (p:Person)-[:KNOWS]->(s:Skill)
RETURN s.name AS skill, count(DISTINCT p) AS people
ORDER BY people DESC;

This ranks skills by the people connected to them. It answers a different question from semantic search: popularity comes from recorded relationships, not proximity between embeddings.

Next, choose Lucy and ask who shares her skills. The first traversal goes from Lucy to a skill and then back to another person who knows it. Extend that path once more to retrieve every skill those other people know:

cypher

MATCH path =
  (lucy:Person {name: $person_name})-[:KNOWS]->(:Skill)
  <-[:KNOWS]-(other:Person)-[:KNOWS]->(:Skill)
WHERE other <> lucy
RETURN path;

Set $person_name to Lucy’s stored name. The progression is Lucy → her skills → people sharing those skills → those people’s skills. It produces a larger neighborhood than the initial shared-skill query, because the final skills need not be ones Lucy knows.

The distinction is useful for team formation: a similar colleague can also bring capabilities beyond the original person’s skill set. In the displayed neighborhood, Scrum appears central because many of the connected people know it. The graph gives a concrete, symbolic definition of the group being examined, and that definition can be changed one traversal at a time.

The result shape controls how Browser can display it. Return nodes or a path to obtain a graph view; return only scalar values such as names and the result is a table. Browser’s table and graph tabs cannot reconstruct a path that the query did not return.

Neo4j Browser graph view with connected green nodes and a results overview listing Person, Skill and KNOWS.Open full source frame
Neo4j Browser displays a graph result with person and skill nodes.

DISTINCT is a separate concern. It removes duplicate returned values, which can arise when several traversal routes reach the same result. It may be unnecessary for the particular path query shown, but becomes important when a multi-hop query projects repeated people or skill names into a list.

26:4326:58
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

26:43 · section reference included

Decide how much Cypher the model should write

An audience question introduces the next design decision: how well do LLMs write these queries? Blumenfeld’s answer depends on schema complexity. Simple aggregations and carefully prompted paths over small schemas can work well. For more complicated traversal logic, he recommends limiting what the model must invent:

  • Expert tools: Put a tested traversal in a Python function or an MCP tool and let the model choose it.
  • Restricted patterns: Let the model select or fill in a small set of permitted query patterns.
  • Dynamic generation: Supply enough schema and prompt context for the model to compose the query when the task warrants that flexibility.

He also mentions Gemma-derived Text2Cypher models available on Hugging Face. Neo4j’s April 2025 announcement describes that release family; the workshop itself uses OpenAI models.

30:5631:05
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

30:56 · section reference included

Count shared skills, then decide whether to store the result

The Lucy traversal becomes a reusable similarity query when the starting person is parameterized and the shared skills are counted. DISTINCT now ensures that repeated routes do not produce repeated projected values:

cypher

MATCH (person:Person {email: $email})-[:KNOWS]->(skill:Skill)
      <-[:KNOWS]-(other:Person)
WHERE other <> person
RETURN other.email AS email,
       other.name AS name,
       count(DISTINCT skill) AS shared_skills,
       collect(DISTINCT skill.name) AS skills
ORDER BY shared_skills DESC;

The notebook also examines shared-skill counts across pairs of people. This similarity measure is grounded in exact common skills: its explanation is the list of shared nodes.

If the assistant repeatedly asks for people with similar skill sets, the overlap results can be stored as person-to-person relationships. The notebook uses the locally retrieved dataframe to merge these derived edges and attach overlap information. The graph can then follow one similarity edge instead of recomputing the original person–skill–person traversal on every request.

That shortcut introduces a refresh obligation. An audience member points out that the stored overlap is static, and Blumenfeld confirms that the query must run again to update it.

ApproachRetrievalMaintenance
Live shared-skill traversalCompute overlap from current connectionsNo stored overlap to refresh
Stored similarity relationshipRead a derived person-to-person edgeRecompute after relevant updates

A frequently changing graph may be better served by the live traversal. The database is designed to traverse relationships; materialization is an option, not a prerequisite.

32:1832:35
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

32:18 · section reference included

Use Leiden to make skill communities explicit

The next enrichment operates over the person-to-person similarity graph. A Graph Data Science client creates a projection, and Leiden community detection finds groups of connected people. The objective is to identify communities with relatively dense internal connections and sparse connections between groups. Blumenfeld introduces the result as a hierarchy; algorithmically, Leiden refines communities and recursively aggregates them into condensed graphs. Here, the similarity edges give that clustering a skills-based interpretation.

The notebook writes community IDs back to the graph, checks the communities, and plots a heat map of skill frequency by community. The data is randomly generated, so the displayed groupings should not be read as real organizational structure. With realistic employee data, the same analysis could reveal concentrations of data-engineering, front-end, or machine-learning skills. Running the enrichment cells through G.drop completes this part of the notebook while retaining the written community properties needed by later examples.

Whether this work is worth doing depends on the questions users ask. Recurring questions about employee segments or performance within groups can justify persistent communities; similar reasoning applies to customer segmentation and recommendations. A request to find one person with skills similar to another may need only pairwise matching. The benefit of clustering is an inspectable grouping method that the assistant can describe, rather than asking the model to invent groups on demand.

The heat map explains the composition of those groups. Its cells show how often a skill appears within a community, not a separate set of secondary skills. An audience member suggests using the distribution to name communities, which is exactly the kind of interpretation it supports. Tableau or Swift appear in the discussion of one displayed group; product-manager, front-end, and DevOps communities are examples of what more realistic data might reveal.

35:5236:03
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

35:52 · section reference included

Make the schema readable to an agent

A schema for an agent benefits from ordinary graph modeling discipline and language that expresses the domain clearly. Person → KNOWS → Skill reads naturally and translates directly into a traversal. Blumenfeld favors simpler models for dynamic query generation, while noting that retriever design, data size, and category cardinality still affect the right representation. Avoid turning every category into a label: hundreds or thousands of labels can make the schema harder to work with when properties would suffice.

Schema context can be assembled by retrieving node labels and relationship types into a JSON-like representation. For a reasonably stable model, enrich that representation with descriptions of properties, labels, and relationships, including guidance on where information belongs and how to retrieve it. Include actual patterns such as (p:Person)-[:KNOWS]->(s:Skill) alongside the descriptions. The model then receives both a vocabulary and examples of how its terms connect.

41:2841:41
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

41:28 · section reference included

Embed descriptions, then make similarity traversable

Exact overlap is only one way skills can be related. The next dataset contains skill names, descriptions, and precomputed embeddings. The descriptions are the useful embedding input: short names such as R and AWS provide less context than an explanation of the capability they represent. Blumenfeld describes the workshop’s text-embedding-ada vectors as having 1,536 dimensions. He then loads the vectors and descriptions onto skill nodes and creates the skills vector index.

Searching from the stored Python skill retrieves ten relevant skills. The displayed results include Ruby, Java, pandas, Django, and PyTorch, with Blumenfeld acknowledging that some are more useful matches than others. The next example starts with wording absent from the database: LangChain embeds API coding, and a thresholded vector search returns API design and JavaScript. Semantic search therefore connects a user’s vocabulary to the graph’s existing skill entities.

The notebook then stores semantic similarity as scored relationships between skills. This makes the embedding-derived connections visible and traversable. The displayed groups include Azure, AWS, and cloud architecture; Flask and Django; Tableau, Power BI, and data visualization; Java, Scala, and Kotlin; and Python with pandas. These connections can support clustering and customized retrieval scores as well as visualization.

Persisted edges also allow explicit curation. Blumenfeld proposes removing a Java–Python connection if it is inappropriate for the application. Subsequent retrieval that follows those stored relationships can respect that decision. This changes the graph’s traversal policy; it does not require treating the embedding’s original notion of similarity as the final authority.

44:2544:40
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

44:25 · section reference included

Combine exact evidence with semantic expansion

Semantic edges become more useful when combined with exact shared-skill evidence. A custom person-similarity query can weight these contributions separately: recorded overlap provides direct matches, while semantic connections recognize related capabilities. The application can expose this as several operations—find similar skills, then find people—or couple the logic in one query when a particular similarity policy should remain fixed.

An audience member asks how a natural-language request chooses the right entities and retrieval path within a larger ontology. Blumenfeld defers the concrete answer to the agent module, where the tools define those entry points. For now, the notebook’s larger query demonstrates the retrieval policy itself: balance semantic proximity against hard skill overlap to control which people count as similar.

The vector index does not have to live in Neo4j. An existing Postgres deployment or specialized vector database can remain part of the architecture.

Vector locationMain consideration
Neo4j alongside the graphAvoid cross-store synchronization
Existing external storeAvoid an unnecessary migration

Blumenfeld suggests that colocation could theoretically reduce query latency, but supplies no measured comparison. The deployment decision depends on infrastructure-specific cost per performance.

The final graph-basics demonstration controls how far semantic expansion may go. A variable-length Cypher pattern permits zero to two semantic hops between skills, and the query also unions in the direct shared-skill pattern. Matthew knows React; John knows HTML; JavaScript provides a semantic bridge between those skills. Other connections need only one hop. Bounding the traversal makes the breadth of similarity explicit: a person can match through the same skill, a directly related skill, or a related skill reached through an intermediate node.

49:2749:35
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

49:27 · section reference included

Turn biographies into the same graph model

The second module replaces the structured input table with two biographies. It reconnects through the same environment file and checks the database before extraction. At the start of module two, the workshop connection check expects 154 nodes after the preceding setup. The extraction target remains the same simple domain: people with names, emails, and named skills.

Pydantic expresses that target as nested Python objects:

python

from pydantic import BaseModel, Field


class Skill(BaseModel):
    name: str


class Person(BaseModel):
    name: str
    email: str
    skills: list[Skill] = Field(default_factory=list)


class PersonList(BaseModel):
    people: list[Person] = Field(default_factory=list)

A richer model could put proficiency on the KNOWS relationship. In that case, the person would contain relationship objects, each holding both the skill and the relationship’s attributes. The workshop keeps only the list of skills.

A system prompt and GPT-4.1 turn the two documents into JSON for the two people. Once those objects exist, loading them is almost the same operation as before: merge each person on the indexed email, set the name, merge the skills, and merge the KNOWS relationships. Browser then confirms that the Neo4j employee examples and their extracted skills are present in the graph.

For more elaborate ingestion, Blumenfeld points to Neo4j GraphRAG for Python and the Knowledge Graph Builder reference application. These provide directions for workflows involving overlapping document chunks and concurrent or asynchronous processing, beyond a serial loop over short biographies.

Adding the biographies exposes the maintenance cost of earlier enrichment. The newly inserted KNOWS relationships are unweighted, but they can change person-to-person overlaps and community assignments. New skills also need semantic enrichment and corresponding semantic relationships. Blumenfeld confirms that these derived structures should be refreshed as data arrives, and acknowledges that ingestion would ideally precede clustering in the pipeline.

55:1755:26
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

55:17 · section reference included

Keep document structure when context crosses sections

Extracting people and skills is one way to build a graph from text. Another is to preserve the document’s structure. Catalogs and RFPs often contain predictable sections: an introduction, objectives, a proposal, and subsections. Those structural pieces and their metadata can become graph objects, with embeddings attached to the relevant content. Retrieval can then move from an entity in a chunk up through the document hierarchy and down to another section.

This gives the system two sources of grouping. Leiden derives communities from connectivity; a document already supplies a natural hierarchy. That hierarchy can support summaries across sections or documents without requiring every grouping to be discovered algorithmically.

Why put entities, chunks, and documents in the same graph? Consider a legal clause whose expiry date appears elsewhere in the contract. Finding the clause is only the first retrieval step; following the document’s connections can bring back the date needed to interpret it. The accompanying domain-knowledge slide shows a graph and a query connecting chunks to definitions, another way to retrieve context through explicit relationships.

Add Domain Knowledge slide with explanatory bullets, a references list, a colored node-link diagram and a query connecting chunks to definitions.Open full source frame
Adding domain knowledge connects chunks to definitions for more context.

The audience extends this example to comparing data-protection language across vendors. Once the matching clauses are found, a traversal can also recover dates or perpetuity provisions elsewhere in the documents. Combining the entity and document structures preserves a route from a relevant passage to the surrounding facts that qualify its meaning.

1:01:181:01:34
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

1:01:18 · section reference included

Package the graph patterns as four expert tools

The third module reconnects to Neo4j and assembles four capabilities: retrieve a person’s skills, find similar skills, find similar people, and recommend people from a requested skill set. It begins with explicit graph patterns; schema-guided text-to-Cypher comes later. A direct person-to-skill match supplies the simplest tool.

The next patterns handle user wording that does not exactly match stored skill names. Vector search resolves phrases to skill nodes, and semantic relationships expand the candidates, with score thresholds controlling what is retained. Continuous delivery, cloud native, and security illustrate inputs that can be mapped into the graph’s vocabulary. This answers part of the earlier entry-point question: the tool’s retrieval pattern determines how the user’s words become graph entities.

Person similarity can use several kinds of evidence:

  • Community membership: Retrieve people in the same Leiden skills community and inspect their skills.
  • Exact overlap: Follow the stored similar-skill-set relationship. John Garcia is the example used to inspect overlap counts against other people.
  • Exact plus semantic similarity: Combine shared-skill evidence with related skills, independently weighting the contributions. This yields a floating-point score and can change the ranking.

For recommendations from a requested skill set, the larger query is shown in two parts: search for skills, expand to semantically related skills, then find people who know them and count their matches.

A skills object helps define function arguments and return values. The final tool boundaries preserve the retrieval decisions:

ToolGraph operation
Retrieve a person’s skillsMatch the person’s KNOWS connections
Find similar skillsVector search, then one semantic hop
Find similar peopleWeight exact and semantic skill similarity
Find people from skillsResolve skills, expand, traverse to people, rank

The last tool counts who knows the most matching skills. The model can choose the capability without having to reconstruct its traversal and scoring policy each time.

1:04:401:04:59
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

1:04:40 · section reference included

Let the agent choose and chain the tools

After testing the LLM and collecting the tools, the notebook first shows that tools can be bound directly to a model. It then creates a ReAct agent with LangGraph’s historical create_react_agent entry point, supplying the LLM and the four tools. LangGraph v1 later deprecated this helper in favor of LangChain’s create_agent; the recorded walkthrough uses the earlier API.

A greeting checks that the agent responds, and a notebook utility makes streamed interactions easier to inspect. Asking for Christophe’s skills selects the retrieve-person-skills tool. Asking which skills are similar to Power BI and data visualization selects the similar-skills tool. A question about a person with similar skills selects person similarity. These examples show the division of responsibility: the agent interprets the request and selects a capability; the tool supplies the defined graph retrieval.

A small Gradio chatbot then turns the isolated questions into a conversation. The sequence begins with skills related to Power BI, follows with who knows those skills, and then asks who is similar to those people. Blumenfeld checks the notebook trace as he proceeds: it shows a similar-skills call, a people-from-skills call, and finally person-similarity calls for each returned person. The follow-ups move between retrieval patterns while retaining the subject established by earlier answers.

1:11:111:11:28
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

1:11:11 · section reference included

Extend the assistant with annotated-schema queries

The final implementation example adds dynamic query generation to the expert-tool approach. An annotated schema supplies descriptions to an aggregation-query function containing an LLM. For a request to describe communities, the shown example generates Cypher that combines the person–skill pattern with the Leiden community property. The schema tells the model both what information exists and how the relevant parts connect. This receives a brief walkthrough, with additional examples left in the notebook.

The closing access question returns to the environment’s lifetime. The Jupyter servers are scheduled to shut down quickly, but the code and data remain in GitHub, linked from the deck shared through Slack. Blumenfeld suggests creating a cloud database through the Aura console’s free trial and loading the workshop data there to continue experimenting.

For participants at the event, the meetup announcement is corrected from tomorrow to that evening at 5:00. A separate 1:00 workshop goes deeper into graph analytics and communities, while booth follow-ups cover Neo4j MCP servers, ADK examples, and further knowledge-graph construction. Those extensions build on the working assistant demonstrated here: a small domain graph, explicit retrieval patterns, and an agent that can select among them.

GraphRAG Resources slide with QR codes for the workshop repository, developer guides and a meetup, plus a graph analytics workshop announcement.Open full source frame
GraphRAG resources include the workshop repository and developer guides.
1:15:401:15:53
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

1:15:40 · section reference included

Resources

From the talk

Updates since the talk

Transcript download
Read the transcript and explore slides

Loading transcript and slides…