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% повседневных запросов.