Neo4j — графовая база данных и язык Cypher
Neo4j — самая популярная графовая база данных. Вместо таблиц и джойнов она хранит данные как узлы, соединённые связями, поэтому запросы к отношениям «друг друга», «рекомендации» и «маршруты» выполняются в разы быстрее, чем в SQL.
1. Что такое графовая БД
Графовая база данных хранит сущности (узлы) и отношения между ними (связи). Это самый естественный способ описать социальные сети, маршруты, рекомендации и сети доверия.
- Узлы — сущности с метками и свойствами (человек, город, фильм).
- Связи — отношения между узлами с типом и свойствами («дружит», «живёт в»).
- Свойства — пары ключ-значение, хранящиеся на узлах и связях.
Графы против реляционных таблиц
| Критерий | Реляционная БД | Графовая БД (Neo4j) |
|---|---|---|
| Модель | таблицы и JOIN | узлы и связи |
| Глубина запроса | JOIN по каждому уровню | проход по связям любой глубины |
| Где сильна | финансы, учёт, отчётность | рекомендации, соцсети, графы влияния |
| Схема | жёсткая | гибкая (label property graph) |
2. Установка Neo4j и Neo4j Browser
Скачай Neo4j Community Edition с официального сайта neo4j.com или запусти официальный Docker-образ:
docker run -d --name neo4j \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/yourpassword \
neo4j:5- Порт 7474 — Neo4j Browser (веб-интерфейс).
- Порт 7687 — драйверный протокол Bolt для приложений.
Открой http://localhost:7474, войди как neo4j и выполни первый запрос:
RETURN 'Hello, Neo4j!' AS greeting;neo4j нужно сменить при первом входе в Neo4j Browser. Если забыть новый пароль, его не получится сбросить без перезапуска сервера с выключенной авторизацией.3. Знакомство с Cypher
Cypher — декларативный язык запросов Neo4j. Он намеренно похож на SQL по духу, но описывает не таблицы, а образцы графа: узлы в круглых скобках, связи в квадратных.
(n)— узел.(n:Person)— узел с меткой Person.-[:KNOWS]->— направленная связь типа KNOWS.(a)-[:KNOWS]->(b)— связь от a к b.
MATCH (p:Person)-[:KNOWS]->(f)
WHERE p.name = 'Анна'
RETURN f.name;4. Создание узлов и связей: CREATE
Узел создаётся командой CREATE. Метка пишется с большой буквы, свойства — внутри фигурных скобок:
CREATE (a:Person {name: 'Анна', age: 28});
CREATE (b:Person {name: 'Борис', age: 31});Создать два узла и связь между ними можно одной командой:
CREATE (a:Person {name: 'Анна'})-[:KNOWS]->(b:Person {name: 'Борис'});CREATE всегда создаёт новый узел — даже если такой уже существует. Чтобы не плодить дубликаты, используют MERGE, который создаёт только при отсутствии:
MERGE (a:Person {name: 'Анна'})
ON CREATE SET a.created_at = datetime();
MERGE (b:Person {name: 'Борис'});CREATE с теми же свойствами создаст второй узел-копию. Для идемпотентной вставки всегда используй MERGE, а не CREATE.5. Чтение данных: MATCH и RETURN
Основной способ поиска — MATCH. Он находит все образцы графа, совпадающие с шаблоном, а RETURN возвращает нужные поля:
MATCH (p:Person)
RETURN p.name, p.age;С фильтрами по свойствам и сортировкой:
MATCH (p:Person)
WHERE p.age > 25
RETURN p.name, p.age
ORDER BY p.age DESC
LIMIT 5;Агрегации в Cypher выглядят схоже с SQL:
MATCH (f:City)
RETURN COUNT(*) AS cities,
AVG(f.population) AS avg_pop;MATCH без явного WHERE — это полный обход графа, такой же дорогой, как Scan в DynamoDB. На реальных данных всегда фильтруй по проиндексированным свойствам или метке.6. Связи, типы и свойства связей
Связь — полноценный объект со своим типом и свойствами. Можно хранить на связи «сколько раз дружили», «с какого года знакомы»:
MERGE (a:Person {name: 'Анна'});
MERGE (b:Person {name: 'Борис'});
MERGE (a)-[r:KNOWS {since: 2015}]-(b);Связи бывают направленными и ненаправленными. Запрос без стрелки находит связь в обе стороны:
MATCH (a)-[:KNOWS]-(b)
RETURN a.name, b.name;Типы связей
| Тип связи | Пример | Направление |
|---|---|---|
| FOLLOWS | подписчик — блогер | односторонняя |
| KNOWS / FRIEND_OF | дружба | двусторонняя (ненаправленная) |
| PURCHASED | покупатель — товар | односторонняя |
| MENTIONS | пост — тег | односторонняя |
7. Индексы и ограничения
Индексы ускоряют поиск по свойствам узлов. В Neo4j 5 индексы и ограничения создаются командами CREATE INDEX и CREATE CONSTRAINT:
CREATE INDEX person_name_idx FOR (p:Person) ON (p.name);
CREATE CONSTRAINT person_name_unique FOR (p:Person)
REQUIRE p.name IS UNIQUE;| Объект | Что делает | Синтаксис Neo4j |
|---|---|---|
| Индекс | ускоряет MATCH по свойству | CREATE INDEX ... ON (n:Label) (prop) |
| Ограничение уникальности | запрещает дубли значений | REQUIRE ... IS UNIQUE |
| Ограничение существования | требует атрибут у узла | REQUIRE ... IS NOT NULL |
| Ограничение на метку | одна метка на узел | REQUIRE ... IS NODE KEY |
8. Пример: «Друзья в социальной сети»
Построим мини-соцсеть и найдём друзей друзей — классическую задачу графовых баз.
MERGE (a:Person {name: 'Анна'});
MERGE (b:Person {name: 'Борис'});
MERGE (c:Person {name: 'Вика'});
MERGE (d:Person {name: 'Денис'});
MERGE (a)-[:KNOWS {since: 2015}]-(b);
MERGE (a)-[:KNOWS {since: 2018}]-(c);
MERGE (b)-[:KNOWS {since: 2019}]-(d);
MERGE (c)-[:KNOWS {since: 2020}]-(d);Друзья Анны:
MATCH (a:Person {name: 'Анна'})-[:KNOWS]-(f)
RETURN f.name;Друзья друзей (кроме самой Анны и её прямых друзей):
MATCH (a:Person {name: 'Анна'})-[:KNOWS]-(f)-[:KNOWS]-(ff)
WHERE ff <> a AND NOT (a)-[:KNOWS]-(ff)
RETURN DISTINCT ff.name AS suggestion;Общие друзья пары людей — ещё одна типичная операция:
MATCH (a:Person {name: 'Анна'})-[:KNOWS]-(common)-[:KNOWS]-(b:Person {name: 'Борис'})
RETURN common.name;9. Neo4j из Python
Официальный драйвер для Python устанавливается через pip:
pip install neo4jПодключение и выполнение запросов:
from neo4j import GraphDatabase
driver = GraphDatabase.driver(
"bolt://localhost:7687",
auth=("neo4j", "yourpassword"),
)
def add_friends(tx, name, friend):
tx.run(
"MERGE (a:Person {name: $name}) "
"MERGE (b:Person {name: $friend}) "
"MERGE (a)-[:KNOWS]-(b)",
name=name, friend=friend,
)
def find_friends(tx, name):
result = tx.run(
"MATCH (a:Person {name: $name})-[:KNOWS]-(f) "
"RETURN f.name AS friend",
name=name,
)
return [r["friend"] for r in result]
with driver.session() as session:
session.execute_write(add_friends, "Анна", "Борис")
session.execute_write(add_friends, "Анна", "Вика")
for friend in session.execute_read(find_friends, "Анна"):
print(friend)
driver.close()$name вместо конкатенации строк — это защищает и от ошибок экранирования, и от инъекций в Cypher.10. Переменная длина пути и OPTIONAL MATCH
В Cypher можно обходить граф на произвольную глубину. Звездочка * — поиск по путям любой длины, число задаёт диапазон: *1..4 означает от одного до четырёх рёбер.
MATCH (a:Person {name: 'Егор'})-[*1..4]->(b:Person)
RETURN DISTINCT b.name AS связи_до_4_уровней;
MATCH p = shortestPath(
(a:City {name: 'Москва'})-[:ROAD*]-(b:City {name: 'Казань'})
)
RETURN p;OPTIONAL MATCH работает по аналогии с LEFT JOIN: если путь не найден, в результат вместо удаления строки попадает NULL. Это удобно, когда связей может не быть.
11. MERGE и дедупликация данных
MERGE вставляет узел или ребро, только если его нет, — идеален для загрузки данных из внешних источников без дубликатов. Он работает по свойствам из условия соответствия.
MERGE (u:User {email: 'ivan@mail.ru'})
ON CREATE SET u.created_at = datetime()
ON MATCH SET u.last_seen = datetime()
RETURN u;
// Дубликаты: найти и удалить повторные узлы по email
MATCH (u:User)
WITH u.email AS email, collect(u) AS nodes
WHERE size(nodes) > 1
UNWIND tail(nodes) AS dup
MATCH (dup)-[r]-()
DELETE r, dup;12. Алгоритмы графов: GDS
Neo4j Graph Data Science (GDS) — библиотека алгоритмов, работающих прямо в графе: меры центральности, кластеризация, поиск сообществ и предсказание связей.
| Алгоритм | Для чего |
|---|---|
| PageRank | важность узлов по структуре связей |
| Betweenness | узлы-«мостики» между кластерами |
| Louvain | поиск сообществ и групп |
| Node Similarity | похожие узлы для рекомендаций |
CALL gds.pageRank.write({
nodeProjection: 'User',
relationshipProjection: 'FOLLOWS',
writeProperty: 'pagerank'
})
YIELD nodePropertiesWritten;13. Применение: поиск путей и фрод-паттерны
Классический сценарий графовой базы — обнаружение мошеннических схем: кольца карт, общие устройства, нетипичные встречи.
// Карты, которые переводили друг другу в кольце
MATCH (a:Card)-[t:TRANSFER]->(b:Card)
WITH a, b, t
MATCH (a)-[t2:TRANSFER*3..5]->(a)
RETURN a.number, count(t2) AS ring_hits
ORDER BY ring_hits DESC LIMIT 10;- Кольца: цепочка переводов, возвращающихся к исходному узлу.
- Хаб-схема: много карт переводят одному получателю — типичная разводка.
- Общие признаки: один номер телефона/Wi-Fi у разных клиентов.
14. Импорт данных: LOAD CSV и миграция
Граф редко наполняется вручную — данные приходят из файлов или реляционных баз. Наиболее практичный путь — LOAD CSV: файл кладётся в каталог import базы, а Cypher-запрос парсит его и создаёт узлы и связи.
LOAD CSV WITH HEADERS
FROM 'file:///users.csv' AS row
MERGE (u:User {id: row.id})
SET u.name = row.name, u.age = toInteger(row.age);Правило эффективного импорта: используйте MERGE вместо CREATE для сущностей, общих для нескольких файлов, и приводите типы явно — числа из CSV приходят строками. Для больших объёмов утилита neo4j-admin import быстрее, так как пишет данные напрямую в каталоги.
15. Профилирование запросов: EXPLAIN и PROFILE
Медленный графовый запрос почти всегда следствие обхода всей выборки без помощи индексов. Neo4j позволяет посмотреть план выполнения: EXPLAIN показывает план без запуска, а PROFILE выполняет запрос и возвращает фактические затраты по каждому шагу.
PROFILE
MATCH (u:User)-[:FRIEND]->(f:User)
WHERE u.name = 'Анна'
RETURN f.name;Смотрите на показатели: DbHits (число обращений к хранилищу) и количество строк, прошедших через оператор. Если план начинается с NodeByLabelScan по большой метке, добавьте индекс под условие фильтра. Хорошие планы в графовой базе — это «сначала найти стартовый узел, потом идти по связям».
- Индекс по WHERE-полю метки резко сокращает DbHits.
- Избегайте условий по свойствам видов вроде WHERE c.type = 'linkedin' перед MATCH — фильтруйте на входе.
- Для поиска по подстроке используйте дополнительную индексацию, а не contains по всей метке.
16. Рекомендательные системы на графе
Граф — естественная модель для рекомендаций: связи «купил», «друг», «просмотрел» позволяют строить правила вроде «друзья твоих друзей» без тяжёлого машинного обучения. Самый распространённый паттерн — item-to-item по совместным покупкам.
MATCH (u:User {id: '123'})-[:BOUGHT]->(p:Product)
MATCH (p)<-[:BOUGHT]-(other)-[:BOUGHT]->(rec:Product)
WHERE NOT (u)-[:BOUGHT]->(rec)
RETURN rec.name, count(*) AS score
ORDER BY score DESC LIMIT 5;Число совпадающих пользователей даёт сырой скоринг; для более гладких результатов примените библиотеку Graph Data Science: алгоритм Node Similarity посчитает попарную близость товаров, а Label Propagation разобьёт аудиторию на сегменты по поведению.
17. Администрирование: бэкапы и dump
Утилита neo4j-admin покрывает типовые операции сопровождения: отображение хранилища и управление базами. Бэкап делается командой database dump — это согласованный снимок одной базы, который восстанавливается командой database load.
./bin/neo4j-admin database dump neo4j \
--to-path=/backups/
./bin/neo4j-admin database load neo4j \
--from-path=/backups/Для онлайн-копий на продакшене используйте планировщик или облачную реплику (Aura даёт автоматические бэкапы). Периодически делайте «пробное» восстановление в отдельную базу: оно проверяет, что архив не битый и процесс налажен.
| Операция | Команда |
|---|---|
| Создание снимка | neo4j-admin database dump |
| Восстановление | neo4j-admin database load |
| Статистика базы | neo4j-admin dbms info |
18. Транзакции: атомарность и изоляция
Neo4j — ACID-совместимая база: каждая транзакция выполняется атомарно, а изменения становятся видны другим запросам только после фиксации. Благодаря отсутствию жёсткой схемы при записи согласованность обеспечивают индексы и ограничения, поэтому создавайте их заранее, до массовой загрузки данных.
MATCH (u:User {id: '1'})-[r:KNOWS]->(u2:User {id: '2'})
SET r.since = date('2026-01-01')
RETURN r;
// весь запрос — один атомарный шаг: при ошибке откат целиком
CREATE CONSTRAINT user_email IF NOT EXISTS
FOR (u:User) REQUIRE u.email IS UNIQUE;Драйверы открывают транзакции через функцию обратного вызова: если она бросает исключение, состояние откатывается, а успешный выход фиксирует изменения. Так правильно объединять несколько операций — например, «перевести деньги» как списание и начисление в одном шаге.
19. Производительность: память и профилирование
Скорость Neo4j во многом определяется раскладкой памяти: heap обслуживает внутренние структуры JVM, а page cache кэширует страницы файлов базы, ускоряя поиск по индексам и цепочки переходов. Типичный конфиг для сервера с 8 ГБ RAM выглядит так:
# neo4j.conf
server.memory.heap.initial_size=1G
server.memory.heap.max_size=1G
server.memory.pagecache.size=4G
server.default_listen_address=127.0.0.1Если объём индекса превышает размер page cache, каждое взятие страницы превращается в чтение с диска. Поэтому следите за метриками попаданий в кэш и общим соотношением ресурсов:
- :sysinfo в Neo4j Browser — текущий расклад по памяти и диску.
- Префикс PROFILE перед запросом — реальные затраты CPU и ввода-вывода, а не только план.
- CALL dbms.components() — версия, редакция и статус лицензии.
20. Отдельные базы и облако AuraDB
Один инстанс Neo4j может содержать несколько баз данных. Это удобно, чтобы держать окружения dev, staging и analytics в одном сервере, не разворачивая отдельные детали. Управление базами выполняется Cypher-командами.
SHOW DATABASES; -- список всех баз
CREATE DATABASE analytics; -- отдельная рабочая область
USE analytics; -- переключение контекста
SHOW USERS;Для продакшена без хлопот используют управляемый сервис AuraDB: автоматические резервные копии, мониторинг и обновления берёт на себя провайдер. Аутентификация идёт по токену, который передаётся драйверу через переменные окружения, а не хранится в коде приложения.
- Изолируйте базы для нагрузки: счётчики памяти и лимиты задаются на уровне конфигурации.
- В AuraDB экспорт/импорт данных выполняют через Aura console и utilities, а не через файловую систему.
- Для аналитики держите отдельные базы: длинные запросы не «забивают» операционную инфраструктуру.
21. Моделирование: антипаттерны графа
Гибкость графа искушает строить модель «как выйдет», но через месяц граф превращается в труднообслуживаемую сеть. Классические ошибки: дублирование свойств, которые легко вычисляются запросом, и «звёздные» узлы с сотнями тысяч связей.
// перегрузка одного узла — признак проблемы
MATCH (u:User {id: '1'})-[r]->(p)
RETURN count(r) AS rels;
// роль выражайте типом связи, а не свойством
(Seller)-[:SELLS]->(Product)- Не храните вычисляемые значения повторно: счётчики лучше делать запросами с агрегацией.
- Динамические метки (жестокая смена label в данных) усложняют индексы и планы запросов.
- Избегайте узла, к которому сходится весь граф: ищите способы сегментировать связи.
Что дальше
Ты освоил основы Neo4j: модель графа, Cypher, узлы и связи, индексы и подключение из Python. Чтобы сравнить графовый подход с другими NoSQL-моделями, изучи MongoDB (документы) и DynamoDB (key-value). Основы языка запросов для реляционного мира — в уроке SQL. Для веб-приложения поверх Neo4j отлично подходит FastAPI.