← назад к разделу

К 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 живёт в проде.