Модель данных Neo4j: метки, типы, направление
Раскладываем по полочкам конкретную модель Neo4j и принятые в сообществе соглашения по именованию.
В Neo4j данные — это узлы с метками, направленные связи с типами и свойства на тех и других; внутренний
idназначает сама база, но полагаться в данных надо на свои ключи.
Метки узлов
Метка (label) — это категория узла, пишется через двоеточие: :Person, :Movie. По меткам Neo4j группирует узлы и строит индексы. У узла может быть ноль, одна или несколько меток. Например, актёр-режиссёр — это :Person:Director. Соглашение: метки в PascalCase, единственное число — :User, а не :users.
Зачем так строго относиться к соглашениям, если база и так всё примет? Затем, что метка и тип связи в Cypher чувствительны к регистру: :Person и :person — это две разные метки, и запрос по одной не найдёт узлы с другой. Один человек, написавший :User, и другой, написавший :user, незаметно расколют данные надвое. Единый стиль (PascalCase для меток, единственное число) — это не вкусовщина, а защита от молчаливых багов, которые не выдают ошибку, а просто возвращают пустой результат.
Типы связей и направление
У каждой связи ровно один тип и одно направление. Тип пишут в квадратных скобках после двоеточия, направление задаёт стрелка:
(:Person)-[:ACTED_IN]->(:Movie)
(:User)-[:FOLLOWS]->(:User)
(:Account)-[:SENT]->(:Transaction)Соглашение для типов — UPPER_SNAKE_CASE: :ACTED_IN, :LIVES_IN. Направление несёт смысл: (a)-[:FOLLOWS]->(b) значит «a подписан на b», и это не то же самое, что (b)-[:FOLLOWS]->(a). При этом по связи можно ходить и против стрелки — об этом отдельный урок про обходы.
Полезный приём именования — называть тип связи так, чтобы фраза «узел-A ТИП узел-B» читалась как нормальное предложение. (:Person)-[:LIVES_IN]->(:City) читается «человек живёт в городе» — хорошо. (:Person)-[:CITY]->(:City) читается коряво — плохо. Тип связи — это глагол или отношение, а не существительное. Когда модель проходит этот «тест на чтение вслух», запросы на Cypher получаются почти как английский текст, и их легко понимать спустя месяцы.
Между одной парой узлов — сколько угодно связей
В графе ничто не мешает соединить два узла несколькими связями — даже одного типа. Между пользователем и фильмом могут быть и :RATED, и :WATCHED, и :REVIEWED одновременно. А между двумя людьми может быть несколько :SENT_MONEY с разными датами и суммами на свойствах — каждый перевод это отдельное ребро. Это принципиально мощнее реляционной связи через внешний ключ, где «одна строка ссылается на одну строку». В графе связь — это событие или факт, и таких фактов между двумя узлами может накопиться сколько угодно.
Свойства
Свойства — пары ключ-значение и на узлах, и на связях. Значения бывают примитивные (строки, числа, булевы, даты) и массивы примитивов. Вложенных объектов в свойствах нет — это важное ограничение: если вам нужна структура, делайте отдельные узлы и связи.
(:Person {name:'Алиса', born:1990, tags:['admin','beta']})
│
│ [:RATED {stars:5, at:'2024-01-10'}]
▼
(:Movie {title:'Начало', released:2010})Идентификаторы: внутренний id и ваш ключ
Neo4j присваивает каждому узлу и связи внутренний числовой id. Соблазнительно использовать его как ключ — но не надо: эти id могут переназначаться после удалений и не переносятся между базами. Для бизнес-идентификации заведите своё свойство, например {userId: 'u-42'}, и повесьте на него ограничение уникальности (об этом — в разделе про индексы).
Как работает под капотом
Метка — это не просто строка на узле, а ключ для так называемого label scan: Neo4j умеет быстро перебрать все узлы данной метки. А индекс по паре «метка + свойство» (например, :Person(name)) превращает поиск конкретного стартового узла в логарифмический. Сами обходы по связям индексов не требуют — они идут по смежности. Поэтому типичный запрос — это «найти стартовый узел по индексу, дальше идти по рёбрам».
Эта двухфазная природа запроса — сначала точечный вход, потом обход — определяет, где оптимизировать. Узкое место почти никогда не в самом обходе (он дёшев), а в том, как вы находите стартовый узел. Если на :Person(name) нет индекса, Neo4j переберёт всех людей, чтобы найти Алису, — и это будет медленнее самого обхода её друзей. Отсюда практическое правило: ставьте индекс на то свойство, по которому вы входите в граф (обычно бизнес-ключ), и не переживайте об индексах на рёбрах — их там попросту не нужно.
Стоит сказать и про внутренний id чуть подробнее. Он отражает физическую позицию записи в файле хранилища, и именно поэтому он так быстр для прямой адресации — но именно поэтому он и нестабилен. Удалили узел — его слот может быть переиспользован под новый узел с тем же числовым id. Перенесли базу через export/import — нумерация поедет. Поэтому внутренний id — это деталь реализации движка, а не идентификатор вашей предметной области. Для последней всегда заводите собственный ключ.
Частые ошибки
- Брать внутренний id за бизнес-ключ. Он нестабилен; заведите своё уникальное свойство.
- Метки во множественном числе или snake_case. Договорённость — PascalCase, единственное число:
:Movie, а не:movies. - Складывать вложенный JSON в свойство. Свойства плоские; структуру выражают узлами и связями.
- Игнорировать направление.
FOLLOWSв одну сторону ≠ в обратную; продумывайте семантику стрелки.
Итоги
- Метки (PascalCase) категоризируют узлы; типов связей — UPPER_SNAKE_CASE, и у связи всегда есть направление.
- Свойства плоские (примитивы и их массивы); вложенность выражается узлами и рёбрами.
- Внутренний
id— нестабилен; для бизнес-ключа заведите своё уникальное свойство. - Старт запроса ускоряют индексы по «метка+свойство»; обходы идут по смежности без индексов.