Статьи

Neo4j: Cypher — Построение запроса для страницы профиля фильма

Вчера я провел день в Берлине, проводя семинар в рамках Data Science Retreat, и одним из упражнений, которое мы выполнили, было написание запроса, который извлек бы всю информацию, необходимую для создания страницы IMDB для фильма .

Сканируя страницу, мы видим, что нужно получить некоторые основные метаданные, включая заголовок. Затем нам нужно привлечь актеров, режиссеров, продюсеров и, наконец, рекомендации для некоторых других фильмов, которые зритель хотел бы увидеть.

Я изо всех сил пытался бы написать все это за один раз — это не тривиально. Однако, если мы разберем его, на самом деле есть 5 простых запросов, которые мы, вероятно, сможем написать. Нашим последним шагом будет выяснить, как склеить их все вместе.

Давайте начнем.

Если вы хотите продолжить, откройте браузер Neo4j и введите : воспроизведите фильмы и импортируйте встроенный набор данных.

Мы собираемся создать запрос для домашней страницы The Matrix, поэтому первым шагом будет найти узел, представляющий этот фильм, в базе данных:

match (movie:Movie {title: "The Matrix"})
return movie.title
 
==> +--------------+
==> | movie.title  |
==> +--------------+
==> | "The Matrix" |
==> +--------------+
==> 1 row

Достаточно просто. Теперь давайте вернемся к производителям:

match (movie:Movie {title: "The Matrix"})
optional match (producer)-[:PRODUCED]->(movie)
 
RETURN movie.title, COLLECT(producer.name) AS producers
 
==> +--------------------------------+
==> | movie.title  | producers       |
==> +--------------------------------+
==> | "The Matrix" | ["Joel Silver"] |
==> +--------------------------------+
==> 1 row

Мы ввели здесь функцию COLLECT, так как хотим, чтобы в нашем конечном результате была только одна строка, независимо от количества производителей. COLLECT применяет неявную группу с помощью movie.title и собирает продюсеров для каждого фильма (в данном случае только The Matrix) в массив.

Мы использовали OPTIONAL MATCH * LINK *, потому что мы все еще хотим вернуть строку для запроса, даже если у него нет производителей. В случае отсутствия производителей мы надеемся увидеть пустой массив.

Теперь давайте напишем тот же запрос, чтобы вернуть режиссеров фильма:

match (movie:Movie {title: "The Matrix"})
optional match (director)-[:DIRECTED]->(movie)
 
RETURN movie.title, COLLECT(director.name) AS directors
 
==> +----------------------------------------------------+
==> | movie.title  | directors                           |
==> +----------------------------------------------------+
==> | "The Matrix" | ["Lana Wachowski","Andy Wachowski"] |
==> +----------------------------------------------------+
==> 1 row

Мы действительно хотим выполнить оба этих действия в одном запросе, поэтому мы возвращаем один результат с 3 столбцами. Для этого мы собираемся ввести предложение WITH , которое позволяет нам объединять результаты обходов вместе.

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

match (movie:Movie {title: "The Matrix"})
optional match (producer)-[:PRODUCED]->(movie)
 
with movie, COLLECT(producer.name) AS producers
optional match (director)-[:DIRECTED]->(movie)
 
RETURN movie.title, producers, COLLECT(director.name) AS directors
 
==> +----------------------------------------------------------------------+
==> | movie.title  | producers       | directors                           |
==> +----------------------------------------------------------------------+
==> | "The Matrix" | ["Joel Silver"] | ["Lana Wachowski","Andy Wachowski"] |
==> +----------------------------------------------------------------------+
==> 1 row

Мы можем следовать той же схеме, чтобы вернуть актеров:

match (movie:Movie {title: "The Matrix"})
optional match (producer)-[:PRODUCED]->(movie)
 
with movie, COLLECT(producer.name) AS producers
optional match (director)-[:DIRECTED]->(movie)
 
with movie, producers, COLLECT(director.name) AS directors
optional match (actor)-[:ACTED_IN]->(movie)
 
RETURN movie.title, COLLECT(actor.name) AS actors, producers, directors
 
==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> | movie.title  | actors                                                                                | producers       | directors                           |
==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> | "The Matrix" | ["Hugo Weaving","Laurence Fishburne","Carrie-Anne Moss","Keanu Reeves","Emil Eifrem"] | ["Joel Silver"] | ["Lana Wachowski","Andy Wachowski"] |
==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> 1 row

Все идет нормально. У нас есть все, кроме рекомендаций по другим фильмам, что немного сложнее, поэтому сначала мы напишем это самостоятельно:

match (movie:Movie {title: "The Matrix"})<-[:ACTED_IN]-(actor)-[:ACTED_IN]->(otherMovie)
RETURN otherMovie, COUNT(*) AS score
ORDER BY score DESC
 
==> +---------------------------------------------------------------------------------------------------------------------------+
==> | otherMovie                                                                                                        | score |
==> +---------------------------------------------------------------------------------------------------------------------------+
==> | Node[348]{title:"The Matrix Revolutions",released:2003,tagline:"Everything that has a beginning has an end"}      | 4     |
==> | Node[347]{title:"The Matrix Reloaded",released:2003,tagline:"Free your mind"}                                     | 4     |
==> | Node[490]{title:"Something's Gotta Give",released:2003}                                                           | 1     |
==> | Node[349]{title:"The Devil's Advocate",released:1997,tagline:"Evil has its winning ways"}                         | 1     |
==> | Node[438]{title:"Johnny Mnemonic",released:1995,tagline:"The hottest data on earth. In the coolest head in town"} | 1     |
==> | Node[443]{title:"Cloud Atlas",released:2012,tagline:"Everything is connected"}                                    | 1     |
==> | Node[452]{title:"V for Vendetta",released:2006,tagline:"Freedom! Forever!"}                                       | 1     |
==> | Node[425]{title:"The Replacements",released:2000,tagline:"Pain heals, Chicks dig scars... Glory lasts forever"}   | 1     |
==> +---------------------------------------------------------------------------------------------------------------------------+
==> 8 rows

Наш запрос рекомендаций находит всех актеров в «Матрице», а затем переходит к поиску других фильмов, в которых они снимались, и заказывает эти фильмы в зависимости от того, сколько наших актеров появилось в них. Не удивительно, что другие фильмы Матрицы выходят на первое место.

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

match (movie:Movie {title: "The Matrix"})<-[:ACTED_IN]-(actor)-[:ACTED_IN]->(otherMovie)
 
WITH otherMovie, COUNT(*) AS score
ORDER BY score DESC
 
RETURN COLLECT({movie: otherMovie.title, score: score}) AS otherMovies
 
==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> | recommended                                                                                                                                                                                                                                                                                                                                                  |
==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> | [{movie -> "The Matrix Revolutions", score -> 4},{movie -> "The Matrix Reloaded", score -> 4},{movie -> "Something's Gotta Give", score -> 1},{movie -> "The Devil's Advocate", score -> 1},{movie -> "Johnny Mnemonic", score -> 1},{movie -> "Cloud Atlas", score -> 1},{movie -> "V for Vendetta", score -> 1},{movie -> "The Replacements", score -> 1}] |
==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

Мы ввели предложение WITH по двум причинам:

  1. Чтобы обеспечить порядок фильмов на основе самого высокого балла
  2. Потому что мы не можем выполнить агрегирование внутри агрегации, т.е. COLLECT (COUNT (…)) будет недопустимой операцией в Cypher.

Теперь мы готовы включить этот рекомендательный запрос в наш основной:

match (movie:Movie {title: "The Matrix"})
optional match (producer)-[:PRODUCED]->(movie)
 
with movie, COLLECT(producer.name) AS producers
optional match (director)-[:DIRECTED]->(movie)
 
with movie, producers, COLLECT(director.name) AS directors
optional match (actor)-[:ACTED_IN]->(movie)
 
WITH  movie, COLLECT(actor.name) AS actors, producers, directors
optional match (movie)<-[:ACTED_IN]-(actor)-[:ACTED_IN]->(otherMovie)
 
WITH movie, actors, producers, directors, otherMovie, COUNT(*) AS score
ORDER BY score DESC
 
RETURN movie, actors, producers, directors,  
       COLLECT({movie: otherMovie.title, score: score}) AS recommended
 
==> +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> | movie                                                                           | actors                                                                                | producers       | directors                           | recommended                                                                                                                                                                                                                                                                                                                                                 |
==> +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> | Node[338]{title:"The Matrix",released:1999,tagline:"Welcome to the Real World"} | ["Hugo Weaving","Laurence Fishburne","Carrie-Anne Moss","Keanu Reeves","Emil Eifrem"] | ["Joel Silver"] | ["Lana Wachowski","Andy Wachowski"] | [{movie -> "The Matrix Revolutions", score -> 4},{movie -> "The Matrix Reloaded", score -> 4},{movie -> "Johnny Mnemonic", score -> 1},{movie -> "The Replacements", score -> 1},{movie -> "Cloud Atlas", score -> 1},{movie -> "V for Vendetta", score -> 1},{movie -> "Something's Gotta Give", score -> 1},{movie -> "The Devil's Advocate", score -> 1}] |
==> +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
==> 1 row

Вуаля! 4 разных типа собранных данных и всего один запрос, чтобы сделать все это.

Специалистам по шифрованию с орлиными глазами (Привет, Майкл!) Вы заметите небольшое дублирование в том, как мы обращаемся к актерам дважды: один раз, чтобы найти их, и один раз, чтобы дать рекомендацию к фильму.

Мы могли бы оптимизировать это, собирая актеров один раз, а затем используя предложение UNWIND , но это оптимизация, которая, как мне кажется, слегка затеняет цель запроса, поэтому я оставил это пока так.