Neo4j — графовая база данных и язык Cypher

Neo4j — самая популярная графовая база данных. Вместо таблиц и джойнов она хранит данные как узлы, соединённые связями, поэтому запросы к отношениям «друг друга», «рекомендации» и «маршруты» выполняются в разы быстрее, чем в SQL.

Уровень: начальный 17 разделов ⏱ 2–3 часа

1. Что такое графовая БД

Графовая база данных хранит сущности (узлы) и отношения между ними (связи). Это самый естественный способ описать социальные сети, маршруты, рекомендации и сети доверия.

Графы против реляционных таблиц

КритерийРеляционная БДГрафовая БД (Neo4j)
Модельтаблицы и JOINузлы и связи
Глубина запросаJOIN по каждому уровнюпроход по связям любой глубины
Где сильнафинансы, учёт, отчётностьрекомендации, соцсети, графы влияния
Схемажёсткаягибкая (label property graph)
Классический пример: найти «друзей друзей». В SQL нужно три JOIN-а на каждый уровень глубины, в Cypher — один шаблон. Чем глубже цепочка, тем больше разница в производительности.

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

Открой http://localhost:7474, войди как neo4j и выполни первый запрос:

RETURN 'Hello, Neo4j!' AS greeting;
Пароль по умолчанию neo4j нужно сменить при первом входе в Neo4j Browser. Если забыть новый пароль, его не получится сбросить без перезапуска сервера с выключенной авторизацией.

3. Знакомство с Cypher

Cypher — декларативный язык запросов Neo4j. Он намеренно похож на SQL по духу, но описывает не таблицы, а образцы графа: узлы в круглых скобках, связи в квадратных.

MATCH (p:Person)-[:KNOWS]->(f)
WHERE p.name = 'Анна'
RETURN f.name;
Запрос читается почти как английское предложение: «Найди персон p, которая знает f, где p зовут Анна, и верни имя f». Именно поэтому Cypher считается самым простым графовым языком для изучения.

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;
Этот запрос находит «рекомендации друзей» без единого JOIN — просто проход по связям. В реляционной схеме аналогичное решение потребовало бы множества вложенных подзапросов.

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. Это удобно, когда связей может не быть.

Глубина пути вроде [*0..] позволяет включить сам узел в результат. Для больших графов ограничивайте глубину и покрывайте запрос индексами, иначе обход превратится в полный перебор.

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;
MERGE без условия соответствует ВСЕМ свойствам в pattern. Если в pattern несколько атрибутов, совпадение ищется по всей строке — так случайно плодятся дубли с разными полями. Явно указывайте «ключ» в условии существования.

12. Алгоритмы графов: GDS

Neo4j Graph Data Science (GDS) — библиотека алгоритмов, работающих прямо в графе: меры центральности, кластеризация, поиск сообществ и предсказание связей.

АлгоритмДля чего
PageRankважность узлов по структуре связей
Betweennessузлы-«мостики» между кластерами
Louvainпоиск сообществ и групп
Node Similarityпохожие узлы для рекомендаций
CALL gds.pageRank.write({
  nodeProjection: 'User',
  relationshipProjection: 'FOLLOWS',
  writeProperty: 'pagerank'
})
YIELD nodePropertiesWritten;
Алгоритмы GDS написаны на Java и работают на графе в памяти. Для больших наборов выделяйте планировщику JVM кучу и запускайте тяжёлые вычисления в фоновых операциях, а не в интерактивном браузере.

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;
Графовые сигналы — подсказка для расследования, а не приговор. Используйте их как скоринговый признак и проверяйте вручную, прежде чем блокировать клиента.

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 быстрее, так как пишет данные напрямую в каталоги.

Перед массовым импортом создайте индексы по ключам: CREATE INDEX FOR (u:User) ON (u.id). Без индекса MERGE вырождается в полный проход по узлам.
CSV в Windows часто сохраняется в кодировке UTF-8 with BOM — удалите BOM перед загрузкой, иначе первый заголовок может быть испорчен.

15. Профилирование запросов: EXPLAIN и PROFILE

Медленный графовый запрос почти всегда следствие обхода всей выборки без помощи индексов. Neo4j позволяет посмотреть план выполнения: EXPLAIN показывает план без запуска, а PROFILE выполняет запрос и возвращает фактические затраты по каждому шагу.

PROFILE
MATCH (u:User)-[:FRIEND]->(f:User)
WHERE u.name = 'Анна'
RETURN f.name;

Смотрите на показатели: DbHits (число обращений к хранилищу) и количество строк, прошедших через оператор. Если план начинается с NodeByLabelScan по большой метке, добавьте индекс под условие фильтра. Хорошие планы в графовой базе — это «сначала найти стартовый узел, потом идти по связям».

После EXPLAIN всегда проверяйте версию базы — в Neo4j 5 доступны новые виды планов, например Relationship Index для поиска по свойствам связи.

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
Останавливайте запись во время дампа вручную либо полагайтесь на транзакционный контроль Neo4j; восстановление поверх существующей базы может потребовать её удаления.

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, каждое взятие страницы превращается в чтение с диска. Поэтому следите за метриками попаданий в кэш и общим соотношением ресурсов:

Heap и page cache — разные ресурсы: экономия на page cache в пользу heap не ускорит чтения, а переполнение heap приводит к частой сборке мусора.
Увеличивайте page cache до размера «горячего» графа: тогда типовые запросы выполняются без обращений к диску и деградация заметна только при холодном старте.

20. Отдельные базы и облако AuraDB

Один инстанс Neo4j может содержать несколько баз данных. Это удобно, чтобы держать окружения dev, staging и analytics в одном сервере, не разворачивая отдельные детали. Управление базами выполняется Cypher-командами.

SHOW DATABASES;                 -- список всех баз
CREATE DATABASE analytics;      -- отдельная рабочая область
USE analytics;                  -- переключение контекста
SHOW USERS;

Для продакшена без хлопот используют управляемый сервис AuraDB: автоматические резервные копии, мониторинг и обновления берёт на себя провайдер. Аутентификация идёт по токену, который передаётся драйверу через переменные окружения, а не хранится в коде приложения.

Помните про контекст: половина ошибок «выполнено не там» — следствие вызова без USE после переключения между базами.

21. Моделирование: антипаттерны графа

Гибкость графа искушает строить модель «как выйдет», но через месяц граф превращается в труднообслуживаемую сеть. Классические ошибки: дублирование свойств, которые легко вычисляются запросом, и «звёздные» узлы с сотнями тысяч связей.

// перегрузка одного узла — признак проблемы
MATCH (u:User {id: '1'})-[r]->(p)
RETURN count(r) AS rels;

// роль выражайте типом связи, а не свойством
(Seller)-[:SELLS]->(Product)
Перед масштабированием прогоните типовые запросы на выборке в 1000 узлов и посмотрите на стоимость через PROFILE.
Каждая метка добавляет стоимость индексов и обхода: осмысленные, предсказуемые метки экономят время и память при росте графа.

Что дальше

Ты освоил основы Neo4j: модель графа, Cypher, узлы и связи, индексы и подключение из Python. Чтобы сравнить графовый подход с другими NoSQL-моделями, изучи MongoDB (документы) и DynamoDB (key-value). Основы языка запросов для реляционного мира — в уроке SQL. Для веб-приложения поверх Neo4j отлично подходит FastAPI.

← Вернуться к курсам