Раздел 39 · Neo4j и графовые базы

Типовые задачи: рекомендации, путь, сообщества

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В уроке про графовые задачи мы перечислили, для чего берут графовые базы. Теперь у нас есть все инструменты, чтобы решить три главные задачи на настоящих запросах: что порекомендовать человеку, как короче всего дойти от одного узла до другого и какие группы образуют узлы графа.

Рекомендации через знакомых

Самый простой рецепт: посоветуй то, что читают твои знакомые, а ты ещё нет. Для Глеба:

MATCH (a:Person {name: 'Глеб'})-[:KNOWS]-(f)-[r:READ]->(b:Book)
WHERE NOT (a)-[:READ]->(b)
RETURN b.title, count(*) AS friends, round(avg(r.rating), 1) AS rating
ORDER BY friends DESC, rating DESC, b.title;
+-----------------------------------------+
| b.title              | friends | rating |
+-----------------------------------------+
| "Двенадцать стульев" | 1       | 5.0    |
| "Пикник на обочине"  | 1       | 4.0    |
| "Обломов"            | 1       | 2.0    |
+-----------------------------------------+

3 rows

Три шага: Глеб, его знакомые, их книги. WHERE NOT убирает прочитанное, count(*) считает, сколько знакомых читали книгу, средняя оценка помогает разбить ничьи.

Рекомендации по похожим вкусам

Второй рецепт сильнее: искать не знакомых, а людей с похожими вкусами, даже если Аня их не знает. Сходство измеряем числом общих книг, а потом берём то, что читали самые похожие:

MATCH (a:Person {name: 'Аня'})-[:READ]->(b:Book)<-[:READ]-(other)
WITH a, other, count(b) AS common
ORDER BY common DESC, other.name
LIMIT 3
MATCH (other)-[:READ]->(rec:Book)
WHERE NOT (a)-[:READ]->(rec)
RETURN rec.title, collect(other.name) AS via, count(*) AS votes
ORDER BY votes DESC, rec.title;
+--------------------------------------------------------+
| rec.title                          | via       | votes |
+--------------------------------------------------------+
| "Двенадцать стульев"               | ["Борис"] | 1     |
| "Понедельник начинается в субботу" | ["Вера"]  | 1     |
+--------------------------------------------------------+

2 rows

Первая часть до LIMIT находит трёх самых похожих читателей. Вторая берёт их книги, которых у Ани нет. Так работает простейшая коллаборативная фильтрация. В настоящих системах сходство считают точнее, с учётом оценок, но форма запроса остаётся той же.

Кратчайший путь

Как Ане выйти на Илью через знакомых? Пути переменной длины из урока 14 нашли бы все цепочки, а нужна одна, самая короткая. Для этого есть функция shortestPath:

MATCH p = shortestPath((a:Person {name: 'Аня'})-[:KNOWS*]-(b:Person {name: 'Илья'}))
RETURN length(p) AS hops, [n IN nodes(p) | n.name] AS chain;
+----------------------------------------------------------------+
| hops | chain                                                   |
+----------------------------------------------------------------+
| 6    | ["Аня", "Глеб", "Дина", "Егор", "Жанна", "Зоя", "Илья"] |
+----------------------------------------------------------------+

1 row

Шесть рукопожатий, почти по классике. Поиск идёт в ширину: база проверяет всех на расстоянии один, потом два и так далее, поэтому звёздочка без верхней границы здесь не опасна.

Если коротких путей несколько, allShortestPaths вернёт все:

MATCH p = allShortestPaths((a:Person {name: 'Вера'})-[:KNOWS*]-(b:Person {name: 'Глеб'}))
RETURN [n IN nodes(p) | n.name] AS chain
ORDER BY chain;
+---------------------------+
| chain                     |
+---------------------------+
| ["Вера", "Аня", "Глеб"]   |
| ["Вера", "Борис", "Глеб"] |
+---------------------------+

2 rows

А если пути нет, ответ пустой. До Яны не добраться никак:

MATCH p = shortestPath((a:Person {name: 'Аня'})-[:KNOWS*]-(b:Person {name: 'Яна'}))
RETURN p;
+---+
| p |
+---+
+---+

0 rows

В Cypher 25 есть и новая запись из стандарта GQL: SHORTEST 1 перед паттерном и + после связи вместо звёздочки. Она умеет больше, например SHORTEST 3 для трёх кратчайших путей:

MATCH p = SHORTEST 1 (a:Person {name: 'Аня'})-[:KNOWS]-+(b:Person {name: 'Илья'})
RETURN length(p) AS hops;
+------+
| hops |
+------+
| 6    |
+------+

1 row

Кратчайший путь здесь считается по числу связей. Если у связей есть вес, например минуты в пути, нужен алгоритм Дейкстры, он есть в библиотеке Graph Data Science, о ней следующий урок.

Сообщества на чистом Cypher

Сообщество это группа, внутри которой связей много, а наружу мало. Серьёзные алгоритмы поиска сообществ живут в GDS, но два полезных сигнала видны и обычными запросами.

Первый сигнал это треугольники: трое, которые все знакомы друг с другом. Чем больше у человека треугольников, тем плотнее его круг:

MATCH (a:Person)-[:KNOWS]-(b:Person)-[:KNOWS]-(c:Person)-[:KNOWS]-(a)
WHERE a.name < b.name < c.name
RETURN a.name, b.name, c.name
ORDER BY b.name, c.name;
+---------------------------+
| a.name | b.name  | c.name |
+---------------------------+
| "Аня"  | "Борис" | "Вера" |
| "Аня"  | "Борис" | "Глеб" |
+---------------------------+

2 rows

Условие a.name < b.name < c.name оставляет каждый треугольник один раз, без перестановок. Оба треугольника в верхней половине клуба: Аня, Борис, Вера и Глеб образуют плотное ядро.

Второй сигнал это связность: кто вообще достижим от человека. Те, до кого от Ани нельзя дойти никакой цепочкой, живут в другой компоненте связности:

MATCH (p:Person)
WHERE NOT EXISTS { MATCH (:Person {name: 'Аня'})-[:KNOWS*1..9]-(p) }
RETURN p.name;
+--------+
| p.name |
+--------+
| "Яна"  |
+--------+

1 row

Только Яна: у неё нет связей вовсе. Сама Аня в ответ не попала, потому что до неё самой можно дойти по кругу через Бориса и Веру.

Попробуй сам

Запрос рисует кратчайшую цепочку знакомств от Ани до Ильи. На картинке видно, что вся цепочка проходит через Глеба и Дину: это мост между двумя половинами клуба.

Удали знакомство Глеба и Дины и выполни запрос снова. Потом найди кратчайший путь от Веры до Зои и посчитай его длину.

Проверь себя

Задание

Дальше

Треугольники и связность на чистом Cypher это только начало. Для настоящих графовых алгоритмов есть библиотека Graph Data Science. В следующем уроке посмотрим, как она устроена и что умеют PageRank, Louvain и их соседи.

домашка

Домашка