Раздел 39 · Neo4j и графовые базы

Модель данных Neo4j: метки, типы, направление

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В уроке про property graph мы назвали четыре блока модели: узлы, связи, метки и свойства. В Neo4j у каждого блока есть свои правила. Сколько меток может быть у узла? Можно ли дать связи два типа? Что делать со связью, у которой в жизни нет направления, вроде дружбы? Разбираем по порядку.

Метки: сколько угодно, можно ни одной

Метка похожа на бейдж на конференции. Человек может носить сразу два бейджа, «участник» и «докладчик», а может прийти вовсе без бейджа. Так же и узел: у него бывает ноль, одна или несколько меток.

Добавим в клуб модератора: она участница и модератор одновременно.

CREATE (m:Person:Moderator {name: 'Майя'})
RETURN m, labels(m);
+--------------------------------------------------------------+
| m                                  | labels(m)               |
+--------------------------------------------------------------+
| (:Person:Moderator {name: "Майя"}) | ["Person", "Moderator"] |
+--------------------------------------------------------------+

1 row
Created 1 node, set 1 property, added 2 labels

Теперь Майя находится и по метке Person, и по метке Moderator:

MATCH (n:Moderator)
RETURN n.name;
+--------+
| n.name |
+--------+
| "Майя" |
+--------+

1 row

Внутри паттерна метки можно комбинировать. Двоеточие подряд или знак & значат «и то, и другое», вертикальная черта значит «или»:

MATCH (n:City|Book)
RETURN labels(n)[0] AS label, count(*) AS total
ORDER BY label;
+----------------+
| label  | total |
+----------------+
| "Book" | 5     |
| "City" | 3     |
+----------------+

2 rows

Соглашение об именах: метки пишут с большой буквы в единственном числе, как имя класса: Person, Book, City. Регистр важен, person и Person для Neo4j разные метки.

Тип связи: всегда ровно один

С типами связей строже. У каждой связи ровно один тип, и он задаётся при создании. Поменять тип у существующей связи нельзя, можно только создать новую связь и удалить старую.

Если двух людей связывает и знакомство, и работа, это две связи разных типов, а не одна связь с двумя типами. Соглашение для типов: глагол заглавными буквами через подчёркивание, KNOWS, LIVES_IN, WORKS_WITH. Так паттерн читается почти как предложение: «участник LIVES_IN город».

Тип связи и её концы можно спросить функциями type(), startNode() и endNode():

MATCH (:Person {name: 'Вера'})-[r]->()
RETURN type(r) AS type, startNode(r).name AS from, endNode(r).name AS to, endNode(r).title AS book
ORDER BY type, to, book;
+---------------------------------------------------------------------+
| type       | from   | to       | book                               |
+---------------------------------------------------------------------+
| "KNOWS"    | "Вера" | "Аня"    | NULL                               |
| "LIVES_IN" | "Вера" | "Казань" | NULL                               |
| "READ"     | "Вера" | NULL     | "Пикник на обочине"                |
| "READ"     | "Вера" | NULL     | "Понедельник начинается в субботу" |
+---------------------------------------------------------------------+

4 rows

Направление: хранится всегда, учитывается по желанию

У каждой связи в Neo4j есть направление: от начального узла к конечному. Создать связь без направления нельзя, база потребует стрелку:

CREATE (a:Person {name: 'Тест'})-[:KNOWS]-(b:Person {name: 'Тест 2'});
42I56: syntax error or access rule violation - invalid relationship direction. Only directed relationships are supported in `CREATE`. (line 1, column 33 (offset: 32))
"CREATE (a:Person {name: 'Тест'})-[:KNOWS]-(b:Person {name: 'Тест 2'})"
                                 ^
  42001: syntax error or access rule violation - invalid syntax

А вот при чтении направление можно не учитывать. В паттерне есть три варианта стрелки:

  • -[:KNOWS]-> только исходящие связи: кого знает Аня;
  • <-[:KNOWS]- только входящие: кто знает Аню;
  • -[:KNOWS]- без наконечника: связь в любую сторону.
MATCH (:Person {name: 'Аня'})-[:KNOWS]->(f)
RETURN f.name AS knows
ORDER BY knows;
+---------+
| knows   |
+---------+
| "Борис" |
| "Глеб"  |
+---------+

2 rows
MATCH (:Person {name: 'Аня'})<-[:KNOWS]-(f)
RETURN f.name AS knownBy;
+---------+
| knownBy |
+---------+
| "Вера"  |
+---------+

1 row
MATCH (:Person {name: 'Аня'})-[:KNOWS]-(f)
RETURN f.name AS anyDirection
ORDER BY anyDirection;
+--------------+
| anyDirection |
+--------------+
| "Борис"      |
| "Вера"       |
| "Глеб"       |
+--------------+

3 rows

Что делать с симметричными связями

Дружба в жизни взаимна, а связь в Neo4j всё равно направлена. Частая ошибка новичка: создать две связи, туда и обратно, «чтобы было честно». Тогда каждое знакомство хранится дважды, а запрос без стрелки находит каждого друга два раза.

Правильный путь: хранить одну связь в любом направлении и читать её без стрелки. Направление при этом можно наполнить смыслом. В клубе KNOWS идёт от того, кто пригласил, к тому, кого пригласили. Аня пригласила Бориса в 2015 году, а Вера позвала Аню в 2019-м.

Есть связи, где направление важно по существу: READ идёт от человека к книге, LIVES_IN от человека к городу. Книга не читает человека, и запрос со стрелкой в обратную сторону честно ничего не найдёт.

Попробуй сам

Запрос находит все знакомства Ани в любую сторону и возвращает сами связи. На картинке видно, какие стрелки выходят из Ани, а какая входит.

Поставь стрелку ->, потом <- и сравни картинки. Затем спроси то же для Глеба: у него и входящих, и исходящих знакомств больше одного.

Проверь себя

Задание

Дальше

Мы уже написали несколько паттернов, не разбирая их устройство. В следующем уроке посмотрим на паттерн как на рисунок: из каких частей он состоит и как Cypher находит в графе всё, что на этот рисунок похоже.

домашка

Домашка