К property graph нужен язык запросов. В Neo4j это Cypher — декларативный язык, в котором запрос выглядит как рисунок связи в тексте. Если SQL описывает таблицы и JOIN, то Cypher описывает шаблон: какие узлы соединены какими рёбрами.
Шаблон связи: рисунок в скобках
Основная идея Cypher — ASCII-рисунок графа. Узел — в круглых скобках, ребро — в квадратных, направление — стрелкой:
(p:Person) -[:КУПИЛ]-> (t:Product)
Читается буквально: «узел p с меткой :Person, от которого идёт ребро типа КУПИЛ к узлу t с меткой :Product». Круглые скобки — узлы, -[...]-> — направленное ребро, p и t — переменные, чтобы потом на них сослаться.
MATCH и RETURN: найти и вернуть
Запрос на чтение начинается с MATCH (найти шаблон) и заканчивается RETURN (что вернуть). «Какие товары купил Иван»:
MATCH (p:Person {имя: 'Иван'}) -[:КУПИЛ]-> (t:Product)
RETURN t.название
Свойства для фильтра можно писать прямо в узле ({имя: 'Иван'}) или отдельным условием через WHERE — как в SQL:
MATCH (p:Person) -[:КУПИЛ]-> (t:Product)
WHERE p.возраст > 30 AND t.цена < 1000
RETURN p.имя, t.название
Рекомендация «товары, которые покупают вместе с этим» — это обход на два шага: от товара к его покупателям, от них — к другим их покупкам:
MATCH (t:Product {id: 42}) <-[:КУПИЛ]- (:Person) -[:КУПИЛ]-> (other:Product)
WHERE other.id <> 42
RETURN other.название, count(*) AS вместе
ORDER BY вместе DESC LIMIT 5
Обратите внимание на <-[:КУПИЛ]- — стрелка развёрнута, потому что от товара к покупателю связь идёт в обратную сторону. Направление в шаблоне можно указывать как угодно; можно и вовсе опустить (-[:КУПИЛ]-), чтобы обходить в обе стороны.
Путь переменной длины: обход на неизвестную глубину
Главная сила Cypher — обход, когда число шагов заранее неизвестно. Его задают звёздочкой, как * в регулярных выражениях, только для связей. «Все подчинённые руководителя на любую глубину»:
MATCH (boss:Person {имя: 'Анна'}) <-[:ПОДЧИНЯЕТСЯ*]- (сотрудник:Person)
RETURN сотрудник.имя
* означает «одно или больше рёбер ПОДЧИНЯЕТСЯ». Можно ограничить глубину: *1..3 — от одного до трёх шагов, *0.. — ноль или больше. Именно этот запрос в реляционной базе разворачивается в громоздкий рекурсивный CTE, а в Cypher занимает одну строку.
Отдельно есть встроенный поиск кратчайшего пути:
MATCH путь = shortestPath(
(a:Person {имя: 'Иван'}) -[:ЗНАКОМ*]- (b:Person {имя: 'Пётр'})
)
RETURN путь
shortestPath находит кратчайшую цепочку знакомств между двумя людьми — классическая «цепочка рукопожатий».
CREATE и MERGE: создать и обновить
Записывают данные через CREATE (создать узел или ребро) и MERGE (создать, если нет, иначе взять существующий):
CREATE (p:Person {имя: 'Мария', возраст: 28})
MATCH (m:Person {имя: 'Мария'}), (c:Company {имя: 'Acme'})
MERGE (m) -[:РАБОТАЕТ_В {с: 2024}]-> (c)
MERGE важен для идемпотентности: он не создаст дубликат связи, если она уже есть. Это ежедневный инструмент при загрузке данных из внешних источников, когда одну и ту же сущность встречают многократно.
Где спотыкаются начинающие
- Забывают про
MERGEи плодят дубли.CREATEна повторных загрузках создаёт вторую копию узла или ребра. Для «создать, если ещё нет» — толькоMERGE, и обязательно по уникальному ключу с индексом или ограничением уникальности. MATCHбез якоря обходит весь граф. Если в шаблоне нет ни одного узла с меткой и индексируемым свойством, Neo4j начинает с полного сканирования. Всегда «заземляйте» запрос на конкретный узел (по метке + свойству), от которого пойдёт обход.- Пишут
*без границы на большом графе. Путь*без верхнего предела на плотном графе может обойти пол-базы. Ограничивайте глубину (*1..4), если это не осознанный полный обход.
Дальше — моделирование графа и эксплуатация: как проектировать структуру под запросы, какие нужны индексы и как Neo4j живёт в проде.