LEARN X · ЗА 14 МИН

Cypher (Neo4j)

Cypher за 14 минут: язык запросов графовой БД Neo4j. Узлы, связи, MATCH, WHERE, MERGE, агрегации, пути и индексы — на одной странице.

Cypher — декларативный язык запросов для графовой базы данных Neo4j. Вместо таблиц и JOIN'ов вы описываете граф: узлы (сущности) и связи (отношения) между ними. Главная фишка — паттерны рисуются прямо в коде в виде ASCII-арта: (a)-[:KNOWS]->(b) буквально читается как «a знает b». Ниже — весь язык на одной странице через плотно закомментированный код.

Что такое граф и Cypher

Граф состоит из узлов (nodes), связей (relationships) и свойств (properties) на тех и других. Паттерн в Cypher рисуется как ASCII-схема.

// Это однострочный комментарий
/* А это многострочный
   комментарий */

// УЗЕЛ рисуется круглыми скобками:
(n)                       // безымянный узел
(p:Person)                // узел с меткой (label) Person
(p:Person {name: 'Аня'})  // узел с меткой и свойством

// СВЯЗЬ рисуется квадратными скобками и стрелкой:
// (a)-[:KNOWS]->(b)  читается: a знает b
//      ^^^^^^         тип связи
//            ^^       направление (от a к b)

// ПАТТЕРН — это узлы, соединённые связями:
// (:Person {name:'Аня'})-[:KNOWS]->(:Person {name:'Боря'})

// Свойства — пары ключ:значение, как в JSON.
// Метки начинаются с двоеточия: :Person, :Movie, :City

Узлы

Узлы создаются командой CREATE. Один узел может иметь несколько меток.

// Создать узел с меткой и свойствами
CREATE (n:Person {name: 'Аня', age: 30});

// Несколько меток у одного узла (через двоеточие)
CREATE (n:Person:Developer {name: 'Боря', city: 'Москва'});

// Создать несколько узлов сразу
CREATE
  (a:Person {name: 'Вика'}),
  (b:Person {name: 'Глеб'}),
  (c:City   {name: 'Казань'});

// Свойства бывают разных типов:
CREATE (m:Movie {
  title: 'Матрица',      // строка
  year: 1999,            // целое число
  rating: 8.7,           // дробное число
  isClassic: true,       // логическое
  genres: ['sci-fi', 'action']  // список
});

Связи

Связь всегда имеет тип и направление. У связей тоже могут быть свойства.

// Создать два узла и связь между ними сразу
CREATE (a:Person {name: 'Аня'})-[:KNOWS]->(b:Person {name: 'Боря'});

// Связь со свойствами (например, с какого года знакомы)
CREATE (a:Person {name: 'Вика'})-[:KNOWS {since: 2020}]->(b:Person {name: 'Глеб'});

// Направление имеет значение:
// (a)-[:LIKES]->(b)   стрелка ВПРАВО: a лайкает b
// (a)<-[:LIKES]-(b)   стрелка ВЛЕВО:  b лайкает a
// (a)-[:FRIEND]-(b)   БЕЗ стрелки: связь в любом направлении

// Несколько связей в одном паттерне (цепочка):
CREATE (a:Person {name: 'Дима'})-[:LIVES_IN]->(c:City {name: 'Уфа'})
CREATE (a)-[:WORKS_AT]->(comp:Company {name: 'Acme'});

Сопоставление паттернов (MATCH)

MATCH ищет узлы и связи по паттерну — это аналог SELECT ... FROM ... JOIN.

// Найти все узлы с меткой Person
MATCH (p:Person)
RETURN p;

// Найти конкретный узел по свойству
MATCH (p:Person {name: 'Аня'})
RETURN p;

// Найти связь: кого знает Аня
MATCH (p:Person {name: 'Аня'})-[:KNOWS]->(friend)
RETURN friend.name;

// Сопоставить и узел, и связь, и переменную связи
MATCH (a:Person)-[r:KNOWS]->(b:Person)
RETURN a.name, r.since, b.name;

// Связь без направления (в обе стороны)
MATCH (a:Person {name: 'Аня'})-[:FRIEND]-(b)
RETURN b.name;

// Несколько паттернов через запятую
MATCH (p:Person)-[:LIVES_IN]->(c:City), (p)-[:WORKS_AT]->(comp:Company)
RETURN p.name, c.name, comp.name;

Фильтрация (WHERE)

WHERE задаёт условия. Поддерживает сравнения, логику, диапазоны, регулярки.

// Сравнения
MATCH (p:Person)
WHERE p.age > 25 AND p.age <= 40
RETURN p.name;

// Логические операторы: AND, OR, NOT
MATCH (p:Person)
WHERE p.city = 'Москва' OR p.city = 'Казань'
RETURN p.name;

// Проверка наличия свойства
MATCH (p:Person)
WHERE p.email IS NOT NULL
RETURN p.name;

// Списки и диапазоны: IN
MATCH (p:Person)
WHERE p.city IN ['Уфа', 'Казань', 'Самара']
RETURN p.name;

// Строковые предикаты
MATCH (p:Person)
WHERE p.name STARTS WITH 'А'
   OR p.name ENDS WITH 'я'
   OR p.name CONTAINS 'ор'
RETURN p.name;

// Регулярное выражение (=~)
MATCH (p:Person)
WHERE p.name =~ '(?i)а.*'   // (?i) — без учёта регистра
RETURN p.name;

// Фильтр по существованию паттерна
MATCH (p:Person)
WHERE EXISTS { (p)-[:KNOWS]->(:Person {name: 'Боря'}) }
RETURN p.name;

Возврат данных (RETURN)

RETURN формирует результат: свойства, выражения, алиасы, уникальные значения.

// Вернуть отдельные свойства
MATCH (p:Person)
RETURN p.name, p.age;

// Алиасы через AS
MATCH (p:Person)
RETURN p.name AS имя, p.age AS возраст;

// Выражения в RETURN
MATCH (p:Person)
RETURN p.name, p.age + 1 AS черезГод;

// Уникальные значения — DISTINCT
MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN DISTINCT c.name;

// Вернуть весь узел целиком
MATCH (p:Person {name: 'Аня'})
RETURN p;

// Метки и id узла
MATCH (p:Person)
RETURN p.name, labels(p), elementId(p);

Создание и изменение (CREATE, MERGE, SET, DELETE)

MERGE — «найди или создай»: не плодит дубликаты. SET меняет свойства, DELETE удаляет.

// MERGE: создаст узел, только если такого ещё нет
MERGE (p:Person {name: 'Аня'});

// MERGE со сценариями: что делать при создании и при совпадении
MERGE (p:Person {name: 'Боря'})
ON CREATE SET p.created = timestamp(), p.visits = 1
ON MATCH  SET p.visits = p.visits + 1;

// SET — задать или обновить свойства
MATCH (p:Person {name: 'Аня'})
SET p.age = 31, p.city = 'Питер';

// SET += обновляет map, не затирая остальное
MATCH (p:Person {name: 'Аня'})
SET p += {email: '[email protected]', active: true};

// Добавить метку через SET
MATCH (p:Person {name: 'Аня'})
SET p:Admin;

// REMOVE — убрать свойство или метку
MATCH (p:Person {name: 'Аня'})
REMOVE p.email, p:Admin;

// DELETE — удалить связь или узел (узел без связей!)
MATCH (a:Person {name: 'Вика'})-[r:KNOWS]->(b)
DELETE r;

// DETACH DELETE — удалить узел ВМЕСТЕ со всеми его связями
MATCH (p:Person {name: 'Глеб'})
DETACH DELETE p;

Агрегации (count, collect, sum, avg)

Агрегатные функции группируют данные. Группировка — неявная: по всем не-агрегатным полям в RETURN.

// Посчитать узлы
MATCH (p:Person)
RETURN count(p) AS всегоЛюдей;

// count(DISTINCT ...) — уникальные
MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN count(DISTINCT c) AS городов;

// Группировка: сколько людей в каждом городе
MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN c.name AS город, count(p) AS людей;

// collect — собрать значения в список
MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN c.name, collect(p.name) AS жители;

// sum, avg, min, max
MATCH (p:Person)
RETURN avg(p.age) AS среднийВозраст,
       min(p.age) AS младший,
       max(p.age) AS старший,
       sum(p.age) AS суммаЛет;

// Фильтрация по агрегату — через WITH (см. ниже)
MATCH (p:Person)-[:LIVES_IN]->(c:City)
WITH c, count(p) AS n
WHERE n >= 2
RETURN c.name, n;

Сортировка и лимит (ORDER BY, LIMIT, SKIP)

// Сортировка по возрастанию (по умолчанию)
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age;

// По убыванию — DESC
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age DESC;

// Несколько ключей сортировки
MATCH (p:Person)
RETURN p.name, p.city, p.age
ORDER BY p.city ASC, p.age DESC;

// LIMIT — ограничить число строк (топ-5 по возрасту)
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age DESC
LIMIT 5;

// SKIP + LIMIT — пагинация (вторая страница по 10)
MATCH (p:Person)
RETURN p.name
ORDER BY p.name
SKIP 10 LIMIT 10;

Пути и обходы (пути переменной длины, shortestPath)

Сила графов — в обходе связей. Звёздочка * задаёт путь переменной длины.

// Путь ровно из 2 связей KNOWS (друзья друзей)
MATCH (me:Person {name: 'Аня'})-[:KNOWS*2]->(fof)
RETURN DISTINCT fof.name;

// Путь длиной от 1 до 3 связей
MATCH (me:Person {name: 'Аня'})-[:KNOWS*1..3]->(reachable)
RETURN DISTINCT reachable.name;

// Любая длина (осторожно на больших графах!)
MATCH (me:Person {name: 'Аня'})-[:KNOWS*]->(anyone)
RETURN DISTINCT anyone.name;

// Сохранить путь в переменную p и измерить длину
MATCH p = (a:Person {name: 'Аня'})-[:KNOWS*1..4]->(b:Person {name: 'Глеб'})
RETURN p, length(p) AS шагов;

// Кратчайший путь между двумя узлами
MATCH (a:Person {name: 'Аня'}), (b:Person {name: 'Глеб'})
MATCH path = shortestPath((a)-[:KNOWS*]-(b))
RETURN path, length(path) AS дистанция;

// Все кратчайшие пути
MATCH (a:Person {name: 'Аня'}), (b:Person {name: 'Глеб'})
MATCH paths = allShortestPaths((a)-[:KNOWS*]-(b))
RETURN paths;

Связывание запросов (WITH)

WITH — это «конвейер»: он передаёт промежуточный результат дальше, позволяя фильтровать и агрегировать поэтапно (аналог подзапросов).

// WITH прокидывает данные между частями запроса
MATCH (p:Person)-[:KNOWS]->(friend)
WITH p, count(friend) AS друзей       // посчитали
WHERE друзей > 3                        // отфильтровали по агрегату
RETURN p.name, друзей
ORDER BY друзей DESC;

// WITH + ORDER BY + LIMIT внутри конвейера
MATCH (p:Person)-[:KNOWS]->(f)
WITH p, count(f) AS popularity
ORDER BY popularity DESC
LIMIT 3
MATCH (p)-[:LIVES_IN]->(c:City)   // продолжаем обход для топ-3
RETURN p.name, popularity, c.name;

// collect через WITH, чтобы потом развернуть UNWIND
MATCH (c:City)<-[:LIVES_IN]-(p:Person)
WITH c, collect(p.name) AS жители
RETURN c.name, жители, size(жители) AS сколько;

// UNWIND — развернуть список обратно в строки
WITH ['Аня', 'Боря', 'Вика'] AS имена
UNWIND имена AS имя
RETURN имя;

Индексы и ограничения (INDEX, CONSTRAINT)

Индексы ускоряют поиск по свойству, ограничения гарантируют целостность данных.

// Индекс на свойство — ускоряет MATCH/WHERE по нему
CREATE INDEX person_name IF NOT EXISTS
FOR (p:Person) ON (p.name);

// Составной индекс по нескольким свойствам
CREATE INDEX person_city_age IF NOT EXISTS
FOR (p:Person) ON (p.city, p.age);

// Ограничение уникальности (как UNIQUE в SQL)
CREATE CONSTRAINT person_email_unique IF NOT EXISTS
FOR (p:Person) REQUIRE p.email IS UNIQUE;

// Ограничение существования свойства (узел обязан его иметь)
CREATE CONSTRAINT person_name_exists IF NOT EXISTS
FOR (p:Person) REQUIRE p.name IS NOT NULL;

// Посмотреть и удалить
SHOW INDEXES;
SHOW CONSTRAINTS;
DROP INDEX person_name IF EXISTS;

Типичный запрос: рекомендации друзей

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

// Рекомендации друзей для Ани
MATCH (me:Person {name: 'Аня'})-[:KNOWS]->(friend)-[:KNOWS]->(suggestion)
WHERE suggestion <> me                              // не я сам(а)
  AND NOT (me)-[:KNOWS]->(suggestion)               // ещё не друзья
WITH suggestion, count(DISTINCT friend) AS общихДрузей
RETURN suggestion.name AS кандидат, общихДрузей
ORDER BY общихДрузей DESC
LIMIT 10;

// Бонус: граф знаний — найти, чем связаны два понятия
MATCH path = shortestPath(
  (a:Concept {name: 'Граф'})-[*..6]-(b:Concept {name: 'Нейросеть'})
)
RETURN [n IN nodes(path) | n.name] AS цепочкаПонятий,
       length(path) AS шагов;

Это весь Cypher на одной странице. Дальше — APOC-процедуры, полнотекстовый поиск, графовые алгоритмы (GDS: PageRank, сообщества) и драйверы для Python/JS. Но узлы, связи, MATCH и WITH покрывают 90% повседневных запросов.

Поддержать проект