Foundational Workshop 2 — Neo4j: A Node, Properties, a Relationship

Guided practice15 min
Duration
25 min
Module
1/7
Prerequisites
the lab is running (etat shows nœuds : 872), Neo4j Browser open and connected
You will build
two nodes of your own, Alice and Bob, linked by an arrow, then linked to the real course graph, before erasing everything cleanly
Deliverable
the table of step 8 (Alice, her two relationships, their targets)

How to read this page. Ten steps, one query at a time. For each: the query to type, what Neo4j Browser displays, and what to look at. Type each query yourself. The "To understand better" blocks are optional. If the lab is not started or if etat does not say nœuds : 872, go back to the guided practice, In Brief section (kit: https://github.com/hrhouma2/aiopsatlas-recherche-graphes-labo-fr).

Objective

In the guided practice, you counted 872 nodes and 3,712 relationships loaded by a script, and you looked at bubbles and arrows that someone else had drawn. Here, you draw your own. An empty node first, to see that a node is just that; then you give it a name, an age, a city; then a second node, and an arrow between the two; then an arrow toward a course from the real dataset. At the end, the etat counter goes up to 874, and you bring it back to 872 by deleting what you created, with the error you need to have seen once to understand what a relationship is.

The Vocabulary in One Image

A node is a bubble. A label says what kind of bubble it is (Personne, Cours, Ville): it is the equivalent of the table name. A property is a value written inside the bubble (nom: "Alice", age: 30): a column. A relationship is an arrow between two bubbles; it has a type in capital letters (CONNAIT, AIME), a direction, and can carry its own properties. In SQL, the arrow would be a foreign key or a join table; here it is a first-class object, which you create, read, and delete like a node.

Neo4jClassic SQL databaseIn this practice
noderowAlice, Bob
labeltablePersonne
propertycolumnnom, age, ville
relationshipforeign key / join tableCONNAIT, AIME
relationship typename of the join tableCONNAIT
relationship propertycolumn of the join tabledepuis: 2024

Two Cypher writing rules to remember: a bubble is written between parentheses (p:Personne), an arrow between square brackets with a dash and an arrowhead -[:CONNAIT]->. The letter before the colon (p, a, r) is a temporary name you choose to reuse the object within the same query.

Where to Type, and How to Read the Response

Open http://localhost:7474, connect (neo4j / aiopsatlas2026, URL localhost:7687). The editor is the neo4j$ line at the top. You send with Ctrl + Enter (Cmd + Enter on macOS) or the ▶ button on the right. Each query opens a frame under the editor, the most recent at the top.

A frame displays two things. At the top, a green summary line: Created 1 node, added 1 label, Set 1 property, Deleted 2 nodes… It is the one that tells you what the query changed. Below, if the query has a RETURN, the result in three views: Graph (the bubbles and arrows), Table (the values in columns), Raw (the raw JSON). A query without RETURN only displays the summary and Completed after … ms.

Step 1 — Create a Node, Just a Label

cypher
CREATE (p:Personne) RETURN p

What the query asks: create a bubble carrying the Personne label, call it p for the duration of the query, and show it to me.

Summary: Created 1 node, added 1 label, then Started streaming 1 record after 1 ms and completed after 2 ms.

What to look at: Graph view, a single bubble, and on the right in Results overview: Nodes (1) with the Personne (1) chip. Click the bubble: the panel lists no property. It is a minimal node: a label, nothing else. SQL equivalent: INSERT INTO personne DEFAULT VALUES, in a table that would have no columns.

To understand better
  • Even more minimal, CREATE (n) without a label, works too. Neo4j accepts it, but you could only find it again by going through every bubble in the graph: a row without a table. Do not do it; a label, always.
  • Personne is a new label in this graph, which only knew five (Competence, Cours, Etudiant, Professeur, Ville). You did not have to declare anything: the first bubble that carries a label creates it. Like the Elasticsearch mapping, in foundational workshop 1.
  • RETURN p is not mandatory. Without it, the query creates the bubble and only displays the summary. It is there so that you can see it.

Step 2 — Give It a Value

cypher
MATCH (p:Personne) SET p.nom = 'Alice' RETURN p

What the query asks: find the Personne bubbles (there is only one), write nom = 'Alice' inside it, show it.

Summary: Set 1 property.

What to look at: the bubble now carries Alice as its caption (Browser displays the first text property by default). Click it: nom: Alice. SQL equivalent: UPDATE personne SET nom = 'Alice'.

To understand better
  • MATCH before SET. To modify a bubble, you first have to find it. MATCH (p:Personne) is the SELECT … FROM personne; SET is the UPDATE. Cypher chains the two in the same sentence.
  • Texts go between single quotes 'Alice' (double quotes work too). Numbers are written bare: 30, not '30'.
  • A property exists only if it has a value. There is no empty nom column for Bob later on: he simply will not have a ville property.

Step 3 — Several Values at Once

cypher
MATCH (p:Personne {nom: 'Alice'}) SET p.age = 30, p.ville = 'Montréal' RETURN p

What the query asks: find the Personne bubble whose nom is Alice, write two more values.

Summary: Set 2 properties.

What to look at: the bubble, when clicked, shows nom: Alice, age: 30, ville: Montréal. The {nom: 'Alice'} between braces is a filter: "the one whose nom is Alice", the WHERE nom = 'Alice' of SQL. It changes nothing here (a single Personne), but from the next step on it becomes indispensable.

Step 4 — Create a Node with Its Values in One Go

cypher
CREATE (b:Personne {nom: 'Bob', age: 25}) RETURN b

What the query asks: create a Personne bubble that immediately has nom and age.

Summary: Created 1 node, set 2 properties, added 1 label.

Now look at your two bubbles as a table:

cypher
MATCH (p:Personne) RETURN p.nom, p.age, p.ville

Table view:

text
p.nom    p.age  p.ville
"Alice"  30     "Montréal"
"Bob"    25     null

What to look at: null for Bob's city: he does not have this property. In SQL it would be an empty cell in the ville column; here, the cell does not exist at all. SQL equivalent: SELECT nom, age, ville FROM personne.

cypher
MATCH (a:Personne {nom: 'Alice'}), (b:Personne {nom: 'Bob'})
CREATE (a)-[:CONNAIT]->(b)

What the query asks: find Alice, call her a; find Bob, call him b; create an arrow of type CONNAIT that starts from a and lands on b.

Summary: Created 1 relationship, then Completed after … ms. No result: there is no RETURN.

Look at the arrow:

cypher
MATCH (a:Personne)-[r]->(b:Personne) RETURN a, r, b

What the query asks: all arrows, whatever their type ([r] without :TYPE), that go from a Personne to a Personne.

What to look at: Graph view, two bubbles and one arrow with CONNAIT written on it, in the direction Alice → Bob. On the right, Results overview: Nodes (2), Relationships (1) with the CONNAIT (1) chip. SQL equivalent: INSERT INTO connait (personne_source, personne_cible) VALUES (id_alice, id_bob), in a join table you would have had to create beforehand.

To understand better
  • A relationship links two bubbles that already exist. Hence the MATCH on the first line: without it, CREATE (a)-[:CONNAIT]->(b) would create two new, empty bubbles and an arrow between them. Classic mistake.
  • The type is in CAPITALS by convention, with _: CONNAIT, A_SUIVI, ENSEIGNE. It is not mandatory, it is what everyone does.
  • A relationship always has a direction. (a)-[:CONNAIT]->(b): the arrowhead is on b. There is no undirected relationship in Neo4j; when reading, however, you can ignore the direction (step 7).

Step 6 — A Value on the Arrow

cypher
MATCH (:Personne {nom: 'Alice'})-[r:CONNAIT]->(:Personne {nom: 'Bob'})
SET r.depuis = 2024
RETURN r

What the query asks: find the CONNAIT arrow that goes from Alice to Bob, call it r, write depuis = 2024 on it.

Summary: Set 1 property.

Read everything as a table:

cypher
MATCH (a:Personne)-[r:CONNAIT]->(b:Personne)
RETURN a.nom, type(r), r.depuis, b.nom
text
a.nom    type(r)    r.depuis  b.nom
"Alice"  "CONNAIT"  2024      "Bob"

What to look at: type(r) gives the arrow's type; r.depuis its property, exactly like p.nom for a bubble. A relationship is handled like a node: you find it, you name it, you read from and write to it. Bubbles without a temporary name, (:Personne {…}), are those you do not need afterward: you can omit the letter.

Step 7 — The Arrow Has a Direction

cypher
MATCH (:Personne {nom: 'Bob'})-[:CONNAIT]->(x) RETURN x.nom

What the query asks: whom does Bob know? The x bubbles at the end of a CONNAIT arrow that starts from Bob.

text
(no changes, no records)

What to look at: zero rows. The only arrow goes from Alice to Bob; none starts from Bob. Now without the arrowhead:

cypher
MATCH (:Personne {nom: 'Bob'})-[:CONNAIT]-(x) RETURN x.nom
text
x.nom
"Alice"

-[:CONNAIT]- without >: "a CONNAIT arrow between Bob and x, in one direction or the other". Alice appears. Remember: when writing, the direction is mandatory; when reading, you choose whether to take it into account or not. This is what makes "who is linked to whom" so short to write in Cypher.

cypher
MATCH (a:Personne {nom: 'Alice'}), (c:Cours {id: 'C0001'})
CREATE (a)-[:AIME]->(c)

What the query asks: find Alice, find course C0001 from the dataset, create an AIME arrow from one to the other.

Summary: Created 1 relationship.

Alice is now linked to a bubble that existed before you. Look at everything that starts from her:

cypher
MATCH (a:Personne {nom: 'Alice'})-[r]->(x)
RETURN a.nom, type(r), labels(x)[0] AS type_cible, coalesce(x.nom, x.titre) AS cible
text
a.nom    type(r)    type_cible  cible
"Alice"  "AIME"     "Cours"     "Docker expliqué simplement"
"Alice"  "CONNAIT"  "Personne"  "Bob"

What to look at: two rows, two arrow types, two target types. labels(x)[0] gives the label of the bubble at the end; coalesce(x.nom, x.titre) takes nom if it exists, otherwise titre (Bob has a nom, the course has a titre). This is your deliverable table: keep it. In SQL, it would have taken one join per target table and a UNION; here, a single MATCH line.

Switch to Graph view on MATCH (a:Personne {nom: 'Alice'})-[r]->(x) RETURN a, r, x: Alice in the center, Bob on one side, the course on the other, in two colors (one per label).

Step 9 — Count

cypher
MATCH (n) RETURN count(n) AS noeuds
text
noeuds
874

872 + your 2 bubbles. By label:

cypher
MATCH (n) RETURN labels(n)[0] AS label, count(*) ORDER BY label
text
label         count(*)
"Competence"  22
"Cours"       504
"Etudiant"    300
"Personne"    2
"Professeur"  30
"Ville"       16

What to look at: a sixth row, Personne 2. In a terminal, .\labo.ps1 etat or ./labo.sh etat now shows ✔ Neo4j répond — nœuds : 874. The script and Neo4j Browser count the same thing by two paths: it is the cross-check from the guided practice, applied to what you created yourself.

Step 10 — Clean Up, and Meet the Error That Explains Everything

First try the naive deletion:

cypher
MATCH (p:Personne) DELETE p
text
Neo.ClientError.Schema.ConstraintValidationFailed
Cannot delete node<874>, because it still has relationships. To delete this node, you must first delete its relationships.

What to look at: a red frame, nothing deleted. Neo4j refuses to erase a bubble that still has arrows: an arrow with no bubble at its end would make no sense. The number node<874> is the internal identifier of the first bubble it tried to delete; on your machine it differs. It is the equivalent of a foreign key violation in SQL, with one difference: here, the remedy fits in one word.

cypher
MATCH (p:Personne) DETACH DELETE p

What the query asks: find the Personne bubbles, detach them (delete their arrows), then delete them.

Summary: Deleted 2 nodes, deleted 2 relationships.

Two nodes, two relationships: CONNAIT and AIME. Course C0001 was not touched; only the arrow that led to it disappeared. Verify:

cypher
MATCH (n) RETURN count(n) AS noeuds
text
noeuds
872

MATCH (n) RETURN labels(n)[0] AS label, count(*) ORDER BY label again returns five rows, without Personne, and etat says nœuds : 872.

To understand better
  • DETACH DELETE is the normal move to delete a node in Neo4j; DELETE alone is only used for a node you know is isolated, or to delete a relationship: MATCH ()-[r:AIME]->() DELETE r erases the arrow and keeps the bubbles.
  • Module 6 will have you write MERGE instead of CREATE: "create if it does not exist, otherwise reuse". This is what allows the charger-graphe script to be replayed without duplicates. With CREATE, rerunning step 4 would have manufactured a second Bob.
  • Nothing you did has remained. The graph is back to its reference state, exactly as after DELETE pratique-mini in foundational workshop 1. Make it a habit: what you create to learn, you delete at the end.

Final Check

  • You created a node with a single label and saw that it has no property.
  • You gave it nom, then age and ville, with SET, by first finding it with MATCH.
  • You created Bob with his values in one query, and saw null where a property is missing.
  • You created CONNAIT between two existing bubbles, put depuis on it, and read type(r) and r.depuis in a table.
  • You can say why (Bob)-[:CONNAIT]->(x) returns nothing and (Bob)-[:CONNAIT]-(x) returns Alice.
  • You linked Alice to course C0001 and kept the two-row table of step 8 as the deliverable.
  • You saw the error Cannot delete node … because it still has relationships, then DETACH DELETE returned Deleted 2 nodes, deleted 2 relationships.
  • MATCH (n) RETURN count(n) and etat say 872.

If Something Goes Wrong

Show the frequent cases
  • Invalid input, expected … in red → syntax error. The three causes seen in class: a parenthesis ( closed by a bracket ], an arrowhead forgotten when writing (-[:CONNAIT]- in a CREATE is refused: Only directed relationships are supported in CREATE), a missing quote around a text.
  • Variable `r` not defined (or a, p…) → you are using a letter in SET or RETURN that no MATCH or CREATE in the same query has introduced. Example: SET r.depuis = 2024 while the arrow was written -[:CONNAIT]-> without the r. Add the letter where the object appears: -[r:CONNAIT]->.
  • Step 5 created extra empty bubbles (Created 2 nodes, created 1 relationship, and Results overview shows * (4)) → you typed CREATE (a)-[:CONNAIT]->(b) without the MATCH right before, or with a semicolon between the two lines. a and b were unknown: Neo4j does not protest, it invents them, with no label or property. Clean up: MATCH (n) WHERE size(labels(n)) = 0 DETACH DELETE n, then redo step 5 as a single query.
  • Two Alices or two Bobs (MATCH (p:Personne) RETURN count(p) returns more than 2) → you reran a CREATE. MATCH (p:Personne) DETACH DELETE p and start again at step 1; module 6 will teach you MERGE, which avoids this.
  • Cannot delete node … because it still has relationships → this is step 10, intended. DETACH DELETE.
  • The bubble shows a number or <id> instead of Alice → Browser does not yet know which property to show. Click the Personne chip under the graph and choose nom as the caption; or read in Table view.
  • etat still says nœuds : 872 after your creations → you are connected to another database or another Neo4j (a neo4j installed locally on port 7687?). In Browser, the top banner must say neo4j://localhost:7687 and Database: neo4j; otherwise, close the other Neo4j or change its port.
  • Neo.ClientError.Security.Unauthorized → password refused. It is aiopsatlas2026; if you changed it and lost it, ./labo.sh reinitialiser then demarrer and charger-graphe (the kit data is reloaded identically).