Neo4j — графовая база данных, в которой данные хранятся не в таблицах, а в узлах и связях. Узел описывает сущность, связь — отношение между сущностями, а у обеих есть свойства. Язык запросов Cypher читается почти как текст: «найди пользователя, который купил товар, и покажи категории этого товара». Такой подход особенно полезен для рекомендаций, антифрода, управления доступом и анализа зависимостей.
Когда граф действительно нужен
Графовая модель выигрывает, когда основная ценность — в связях и их глубине. Если вы постоянно пишете рекурсивные JOIN, считаете кратчайшие пути или строите рекомендации «друзья друзей», граф упрощает и запрос, и схему. Для обычной отчётности, финансов и табличных выгрузок реляционная база остаётся разумнее.
- Хорошо: социальные связи, каталоги, маршруты, права доступа.
- Плохо: массовые агрегаты по миллионам строк, чистая OLAP-аналитика.
- Важно: связи в Neo4j — первоклассные объекты, а не вычисляемые join.
Запуск в Docker
Для знакомства достаточно Community Edition. Пароль задаётся через переменную окружения при первом старте; смените его сразу и не храните в открытом виде. Порты 7474 и 7687 — это HTTP и Bolt соответственно.
docker run -d --name neo4j \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/ChangeMe123 \
-v neo4j_data:/data -v neo4j_logs:/logs \
neo4j:5
docker logs -f neo4j
docker exec -it neo4j cypher-shell -u neo4j -p ChangeMe123 "RETURN 1;"
Основы Cypher
Узлы обозначаются круглыми скобками, связи — стрелками. В CREATE можно сразу задать метки и свойства. Не создавайте всё подряд через CREATE: при повторном запуске скрипта появятся дубликаты.
CREATE (a:Person {name: 'Анна', city: 'Казань'})
CREATE (b:Person {name: 'Олег', city: 'Москва'})
CREATE (a)-[:KNOWS {since: 2019}]->(b)
CREATE (b)-[:WORKS_AT]->(:Company {name: 'Пример'});
MATCH (p:Person {name: 'Анна'})-[r:KNOWS]->(f)
RETURN f.name AS друг, r.since AS с_года;
MERGE и ограничения
MERGE ищет шаблон и создаёт его только при отсутствии совпадения. Это основа идемпотентной загрузки. Обязательно создайте ограничение уникальности: без него MERGE при параллельной записи может породить дубликаты, а планировщик не сможет использовать индекс.
CREATE CONSTRAINT person_id IF NOT EXISTS
FOR (p:Person) REQUIRE p.id IS UNIQUE;
MERGE (p:Person {id: 42})
ON CREATE SET p.createdAt = datetime()
ON MATCH SET p.lastSeen = datetime()
SET p.name = 'Анна';
MATCH (p:Person) RETURN count(p) AS всего;
Обход путей и переменная длина
Связи можно проходить на несколько шагов: [:KNOWS*1..3] означает от одного до трёх переходов. Ограничивайте глубину и количество результатов, иначе запрос может обойти весь граф. Для кратчайшего пути используйте shortestPath.
MATCH path = shortestPath(
(a:Person {id: 42})-[:KNOWS*..6]-(b:Person {id: 7})
)
RETURN length(path) AS шагов, [n IN nodes(path) | n.name] AS маршрут;
MATCH (a:Person {id: 42})-[:KNOWS*1..2]-(fof)
WHERE fof.id <> 42
RETURN DISTINCT fof.name, count(*) AS общих
ORDER BY общих DESC LIMIT 10;
Индексы и планы запросов
Индекс ускоряет поиск начальной точки. Полнотекстовый индекс нужен для поиска по словам, диапазонный — для чисел и дат. Смотрите план через EXPLAIN, а фактическое выполнение — через PROFILE. Если в плане появился AllNodesScan, скорее всего, индекс не подхватился из-за несовпадения метки или типа свойства.
CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.name);
CREATE FULLTEXT INDEX person_search IF NOT EXISTS
FOR (p:Person) ON EACH [p.name, p.city];
PROFILE MATCH (p:Person {id: 42}) RETURN p;
EXPLAIN MATCH (p:Person) WHERE p.name = 'Анна' RETURN p;
Резервное копирование и восстановление
Для Community Edition используйте offline-дамп: базу нужно остановить, иначе копия будет несогласованной. В Enterprise доступны online-бэкапы. Проверяйте восстановление на отдельном инстансе — непроверенная копия не считается резервной.
docker stop neo4j
docker run --rm -v neo4j_data:/data -v ${PWD}/backup:/backup neo4j:5 \
neo4j-admin database dump neo4j --to-path=/backup
docker start neo4j
docker run --rm -v neo4j_data:/data -v ${PWD}/backup:/backup neo4j:5 \
neo4j-admin database load neo4j --from-path=/backup --overwrite-destination=true
Мониторинг и диагностика
HTTP-эндпоинт отвечает, даже если Bolt занят, поэтому проверяйте оба канала. Внутри базы смотрите активные транзакции и медленные запросы. Для регулярного мониторинга включите метрики Prometheus и следите за размером heap, числом страниц и временем выполнения запросов.
curl -s http://localhost:7474/ | head
docker exec neo4j cypher-shell -u neo4j -p ChangeMe123 "SHOW DATABASES;"
docker exec neo4j cypher-shell -u neo4j -p ChangeMe123 "SHOW TRANSACTIONS YIELD transactionId, currentQuery;"
docker stats neo4j --no-stream
Моделирование: узел или свойство
Главная ошибка новичка — переносить таблицы в граф один в один. Спросите себя, будет ли сущность участвовать в связях и запросах отдельно. Город человека лучше хранить свойством, а компанию — отдельным узлом, если по ней строятся отчёты и связи. Метки задавайте по роли объекта, а не по каждому изменению статуса. Помните, что связи тоже могут иметь свойства: дату начала, вес, источник. Если сомневаетесь, начните с минимальной модели и расширяйте её по мере появления реальных запросов.
- Узел нужен, если у сущности есть собственные связи и атрибуты.
- Свойство уместно для простых значений без отдельной жизни.
- Не кодируйте тип связи в имени свойства — используйте тип связи.
Пакетная загрузка данных
Загружайте данные пачками через параметр rows и UNWIND: так драйвер передаёт один запрос вместо тысяч. Для больших объёмов используйте neo4j-admin import на пустой базе, но помните, что это офлайн-операция. После загрузки обязательно проверьте количество узлов и отсутствие дубликатов.
UNWIND $rows AS row
MERGE (p:Person {id: row.id})
SET p.name = row.name
WITH p, row
UNWIND row.tags AS tag
MERGE (t:Tag {name: tag})
MERGE (p)-[:HAS_TAG]->(t);
Чек-лист перед продакшеном
- Пароль изменён, внешний доступ закрыт файрволом.
- Созданы ограничения уникальности и нужные индексы.
- Запросы проверены через PROFILE, глубина обхода ограничена.
- Настроены дампы, ротация логов и проверка восстановления.
- Определён объём heap и лимиты контейнера.
- Есть план миграции схемы и загрузки данных.
Итог
Neo4j раскрывается там, где связи важнее таблиц. Начните с Docker-контейнера, опишите модель на бумаге, создайте ограничения и загружайте данные через MERGE. Затем измерьте планы запросов и только после этого расширяйте схему. Граф прощает гибкость модели, но не прощает отсутствия индексов и неограниченных обходов.