К property graph нужен язык запросов. В Neo4j это Cypher: если SQL описывает таблицы и JOIN, то Cypher описывает шаблон — какие узлы соединены какими рёбрами. Запрос выглядит как рисунок связи в тексте.
Индекс по метке нужен один раз — он находит стартовый узел. Дальше Cypher идёт по рёбрам от узла к соседям, и звёздочка *1..2 — это ровно два таких шага. Поэтому цена запроса зависит от числа пройденных рёбер, а не от размера базы.
Шаблон связи: рисунок в скобках
Узел — в круглых скобках, ребро — в квадратных, направление — стрелкой:
(p:Person) -[:КУПИЛ]-> (t:Product)
Читается буквально: «узел p с меткой :Person купил узел t с меткой :Product». Буквы p и t — переменные, чтобы дальше на них сослаться.
MATCH и RETURN: найти и вернуть
Чтение начинается с MATCH (найти шаблон) и заканчивается RETURN (что вернуть). «Какие товары купил Иван»:
MATCH (p:Person {имя: 'Иван'}) -[:КУПИЛ]-> (t:Product)
RETURN t.название
Фильтр пишут прямо в узле ({имя: 'Иван'}) или отдельным условием через WHERE — как в SQL; ниже оба способа сразу. «Товары, которые покупают вместе с этим» — обход на два шага: от товара к его покупателям, от них — к другим их покупкам:
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.. — ноль или больше. Тот же обход на чистой Java, без базы:
живой пример
import java.util.*;
public class VarLengthPath {
public static void main(String[] args) {
Map<String, List<String>> edges = Map.of("Анна", List.of("Борис", "Вика"),
"Борис", List.of("Глеб", "Дина"), "Вика", List.of("Егор"));
List<String> layer = List.of("Анна");
for (int hop = 1; hop <= 2; hop++) {
List<String> next = new ArrayList<>();
for (String node : layer) next.addAll(edges.getOrDefault(node, List.of()));
System.out.println("шаг " + hop + ": " + next);
layer = next;
}
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Неделя бесплатно →
В реляционной базе тот же вопрос — громоздкий рекурсивный CTE, в Cypher — одна строка. Отдельно есть поиск кратчайшего пути, классическая «цепочка рукопожатий»:
MATCH путь = shortestPath(
(a:Person {имя: 'Иван'}) -[:ЗНАКОМ*]- (b:Person {имя: 'Пётр'})
)
RETURN путь
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), если это не полный обход по замыслу.
Коротко
- Cypher описывает шаблон: узел в круглых скобках, ребро в квадратных, направление стрелкой.
- Читают через
MATCH…RETURN, фильтруют прямо в узле или черезWHERE. - Стрелку разворачивают (
<-[:КУПИЛ]-) или опускают — тогда обход идёт в обе стороны. - Звёздочка — путь переменной длины:
*1..3— один-три шага,shortestPath— кратчайшая цепочка. MERGEберёт существующее или создаёт; загрузка черезCREATEплодит дубли.- Запрос заземляют на узел с меткой и индексом: индекс работает один раз, дальше идут рёбра.
Что почитать дальше
- Как устроен Neo4j: property graph, узлы, рёбра и обход без индексов — почему шаг по ребру дешевле
JOIN. - Neo4j: моделирование графа, индексы и эксплуатация — какие индексы нужны
MATCH. - Neo4j: где применяют графовую СУБД — от рекомендаций до GraphRAG — где граф выигрывает у таблиц.