В прошлом уроке мы искали треугольники и компоненты связности обычным Cypher. Для маленького клуба это работает, но настоящие вопросы аналитиков сложнее: кто самый влиятельный в сети, на какие группы она распадается, через кого идёт больше всего путей. Для таких задач у Neo4j есть библиотека Graph Data Science, коротко GDS. Сегодня обзорный урок: как она устроена и что умеют её главные алгоритмы.
Что такое GDS
GDS это плагин: отдельная библиотека, которая добавляет в Neo4j сотни процедур с префиксом gds.. Внутри параллельные реализации классических алгоритмов: центральность, поиск сообществ, пути с весами, похожесть узлов, машинное обучение. Есть бесплатная редакция Community и платная Enterprise с дополнительными возможностями.
В Docker плагин включается одной переменной, сервер сам его установит:
docker run -d --name neo4j-gds \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/secret-password \
-e NEO4J_PLUGINS='["graph-data-science"]' \
neo4j:2026.10.1-community
Песочница уроков GDS не исполняет: выдумывать результаты алгоритмов она не будет. Все листинги ниже сняты с настоящего Neo4j 2026.10.1 с плагином GDS на графе клуба, строки о времени из вывода убраны.
Проекция: граф в памяти
Алгоритмы GDS работают не с базой напрямую, а с её копией в памяти. Сначала граф проецируют: выбирают нужные метки и связи и складывают в именованный граф. Возьмём участников и знакомства без учёта направления:
CALL gds.graph.project('club', 'Person', {KNOWS: {orientation: 'UNDIRECTED'}})
YIELD graphName, nodeCount, relationshipCount;
+-------------------------------------------+
| graphName | nodeCount | relationshipCount |
+-------------------------------------------+
| "club" | 10 | 20 |
+-------------------------------------------+
1 row
Связей двадцать, а не десять: в неориентированной проекции каждое знакомство хранится в обе стороны. Дальше каждый алгоритм вызывается с именем проекции. У алгоритмов есть режимы: stream возвращает результат строками, write записывает его обратно в свойства узлов, mutate дописывает в проекцию, stats возвращает только сводку.
Центральность: кто в центре
Самая простая мера это степень, число связей. Её легко посчитать и обычным Cypher:
MATCH (p:Person)
RETURN p.name AS name, COUNT { (p)-[:KNOWS]-() } AS degree
ORDER BY degree DESC, name;
+------------------+
| name | degree |
+------------------+
| "Аня" | 3 |
| "Борис" | 3 |
| "Глеб" | 3 |
| "Вера" | 2 |
| "Дина" | 2 |
| "Егор" | 2 |
| "Жанна" | 2 |
| "Зоя" | 2 |
| "Илья" | 1 |
| "Яна" | 0 |
+------------------+
10 rows
Степень не видит, кто твои соседи. PageRank, алгоритм, с которого начинался поиск Google, считает узел важным, если с ним связаны важные узлы:
CALL gds.pageRank.stream('club') YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, round(score, 3) AS score
ORDER BY score DESC, name;
+-----------------+
| name | score |
+-----------------+
| "Глеб" | 1.139 |
| "Аня" | 1.107 |
| "Борис" | 1.107 |
| "Зоя" | 1.094 |
| "Жанна" | 1.01 |
| "Егор" | 0.943 |
| "Дина" | 0.868 |
| "Вера" | 0.772 |
| "Илья" | 0.612 |
| "Яна" | 0.15 |
+-----------------+
10 rows
gds.util.asNode(nodeId) превращает внутренний номер узла проекции обратно в узел базы, чтобы взять имя. Глеб впереди Ани и Бориса, хотя знакомств у всех троих по три: PageRank учитывает не только число соседей, но и то, насколько важны они сами.
А betweenness ищет мосты: через кого проходит больше всего кратчайших путей между другими участниками.
CALL gds.betweenness.stream('club') YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, round(score, 2) AS score
ORDER BY score DESC, name LIMIT 4;
+-----------------+
| name | score |
+-----------------+
| "Дина" | 16.0 |
| "Глеб" | 15.0 |
| "Егор" | 15.0 |
| "Жанна" | 12.0 |
+-----------------+
4 rows
Дина первая, хотя связей у неё всего две. Это мы видели на картинке с кратчайшим путём: она мост между двумя половинами клуба.
Сообщества
WCC, слабо связные компоненты, делит граф на части, внутри которых все достижимы друг из друга. То же, что мы искали в прошлом уроке через NOT EXISTS, но для всех сразу:
CALL gds.wcc.stream('club') YIELD nodeId, componentId
WITH componentId, gds.util.asNode(nodeId).name AS name ORDER BY name
WITH componentId, collect(name) AS members
RETURN members, size(members) AS size ORDER BY size DESC;
+---------------------------------------------------------------------------------+
| members | size |
+---------------------------------------------------------------------------------+
| ["Аня", "Борис", "Вера", "Глеб", "Дина", "Егор", "Жанна", "Зоя", "Илья"] | 9 |
| ["Яна"] | 1 |
+---------------------------------------------------------------------------------+
2 rows
Louvain идёт дальше: внутри одной компоненты ищет плотные группы.
CALL gds.louvain.stream('club') YIELD nodeId, communityId
WITH communityId, gds.util.asNode(nodeId).name AS name ORDER BY name
WITH communityId, collect(name) AS members
RETURN members ORDER BY size(members) DESC, members[0];
+----------------------------------+
| members |
+----------------------------------+
| ["Аня", "Борис", "Вера", "Глеб"] |
| ["Жанна", "Зоя", "Илья"] |
| ["Дина", "Егор"] |
| ["Яна"] |
+----------------------------------+
4 rows
Верхнее ядро с треугольниками, нижняя тройка, пара на мосту и Яна отдельно. На маленьком графе это видно и глазами, а на миллионе узлов только алгоритмом.
Когда проекция больше не нужна, её удаляют, чтобы освободить память: CALL gds.graph.drop('club').
Что ещё есть в GDS
- Пути с весами: Дейкстра, A*, все кратчайшие пути из одной вершины. Например, маршрут по времени в пути, а не по числу пересадок.
- Похожесть узлов: Node Similarity находит участников с похожим набором книг, это основа рекомендаций.
- Вложения и машинное обучение: узлы превращают в векторы и учат модели предсказывать недостающие связи.
Попробуй сам
Степень можно посчитать и в песочнице, обычным Cypher. Запрос считает знакомства каждого участника без учёта направления.
Посчитай отдельно входящие и исходящие знакомства: кто чаще приглашает, а кого чаще приглашают? Если у тебя есть Docker, запусти контейнер с GDS и повтори PageRank.
Проверь себя
Задание
Дальше
Мы прошли большую часть Cypher. В следующем уроке сравним его с SQL на одних и тех же вопросах: где запросы похожи, а где графовый путь короче.
домашка