Schema Diagram and Graph Statistics
Node Statistics
| Node Label | Count |
|---|---|
| Genre | 20 |
| Movie | 87,585 |
| User | 200,948 |
Edge Statistics
| Relationship | Count |
|---|---|
| (:Movie)-[:HAS_GENRE]-(:Genre) | 154,170 |
| (:User)-[:RATED]-(:Movie) | 32,000,204 |
| (:User)-[:TAGGED]-(:Movie) | 2,000,072 |
Disk space: 5.93 GiB
Total import time: 1246.41 Seconds
Data Loading
Overview
Data import is handled in two stages: node creation followed by sequential edge ingestion. We put all the csv files under the data import directory of Neo4j. This directory can be set in the neo4j.conf file using the parameter 'dbms.directories.import'.
For the implementation of MovieLens 32M on Neo4j, we used the original files downloaded from their website. We will read the CSV files unpacked from the downloaded file to complete node and edge imports.
To speed up the import process and future queries, we create B+-tree indices for some node labels.
// Unique User IDs CREATE CONSTRAINT IF NOT EXISTS FOR (u:User) REQUIRE u.userId IS UNIQUE; // Unique Movie IDs CREATE CONSTRAINT IF NOT EXISTS FOR (m:Movie) REQUIRE m.movieId IS UNIQUE; // Unique Genre Names CREATE CONSTRAINT IF NOT EXISTS FOR (g:Genre) REQUIRE g.name IS UNIQUE;
Movie Nodes, Genre Nodes, and HAS_GENRE Edges
CALL apoc.periodic.iterate( "LOAD CSV WITH HEADERS FROM 'file:///movielens/ml-32m/movies.csv' AS row RETURN row", "CREATE (m:Movie {movieId: toInteger(row.movieId)}) SET m.title = row.title WITH m, row UNWIND split(row.genres, '|') AS genreName WITH m, genreName WHERE genreName IS NOT NULL AND trim(genreName) <> '' MERGE (g:Genre {name: genreName}) MERGE (m)-[:HAS_GENRE]->(g)", {batchSize: 10000, parallel: false} )
User Nodes and RATED Edges
CALL apoc.periodic.iterate( "LOAD CSV WITH HEADERS FROM 'file:///movielens/ml-32m/ratings.csv' AS row RETURN row", " MERGE (u:User {userId: toInteger(row.userId)}) WITH u, row MATCH (m:Movie {movieId: toInteger(row.movieId)}) MERGE (u)-[r:RATED]->(m) SET r.rating = toFloat(row.rating), r.timestamp = apoc.date.format(toInteger(row.timestamp), 's', 'yyyy-MM-dd HH:mm:ss') ", {batchSize: 1000, parallel: false} )
TAGGED Edges
CALL apoc.periodic.iterate( "LOAD CSV WITH HEADERS FROM 'file:///movielens/ml-32m/tags.csv' AS row RETURN row", " MERGE (u:User {userId: toInteger(row.userId)}) WITH u, row MATCH (m:Movie {movieId: toInteger(row.movieId)}) CREATE (u)-[t:TAGGED]->(m) SET t.tag = row.tag, t.timestamp = apoc.date.format(toInteger(row.timestamp), 's', 'yyyy-MM-dd HH:mm:ss') ", {batchSize: 1000, parallel: false} )
Queries
Select a query below to view its logic and description.
Q1. Movies Rated by a User
This query lists the movies directly connected to a selected user by outgoing RATED relationships.
MATCH (u:User {userId: 33})-[r:RATED]->(m:Movie) RETURN m.movieId AS movieId, m.title AS title, r.rating AS rating, r.timestamp AS ratingTime ORDER BY rating DESC, title;
Equivalent query in GQL standard:
MATCH (u:ml32m_user {userId: $userId})-[r:ml32m_rated]->(m:ml32m_movie) RETURN m.movieId AS movieId, m.title AS title, r.rating AS rating, r.timestamp AS ratingTime ORDER BY rating DESC, title LIMIT $limit;
Q2. Genres within Two Hops of a User
This query follows a selected user to rated movies and then to their genres, returning distinct genres within two hops.
MATCH (:User {userId: 33})-[:RATED]->(:Movie)-[:HAS_GENRE]->(g:Genre) RETURN DISTINCT g.name AS genre ORDER BY genre;
Equivalent query in GQL standard:
MATCH (:ml32m_user {userId: $userId})-[:ml32m_rated]->(:ml32m_movie)-[:ml32m_has_genre]->(g:ml32m_genre) RETURN DISTINCT g.name AS genre ORDER BY genre LIMIT $limit;
Q3. Users Exactly Two Rating Hops Away
This query finds users exactly two RATED hops away through movies co-rated with a selected user.
MATCH (:User {userId: 33})-[:RATED]->(:Movie)<-[:RATED]-(other:User) WHERE other.userId <> 33 RETURN DISTINCT other.userId AS userId ORDER BY userId;
Equivalent query in GQL standard:
MATCH (:ml32m_user {userId: $userId})-[:ml32m_rated]->(:ml32m_movie)<-[:ml32m_rated]-(other:ml32m_user) WHERE other.userId <> $userId RETURN DISTINCT other.userId AS userId ORDER BY userId LIMIT $limit;
Q4. Highly Rated One-Hop Movies
This query filters a selected user's one-hop movie neighborhood to movies with ratings at or above a threshold.
MATCH (u:User {userId: 33})-[r:RATED]->(m:Movie) WHERE r.rating >= 4.0 RETURN m.movieId AS movieId, m.title AS title, r.rating AS rating ORDER BY rating DESC, title;
Equivalent query in GQL standard:
MATCH (u:ml32m_user {userId: $userId})-[r:ml32m_rated]->(m:ml32m_movie) WHERE r.rating >= $minRating RETURN m.movieId AS movieId, m.title AS title, r.rating AS rating ORDER BY rating DESC, title LIMIT $limit;
Q5. Filter Movies by Title Text
This query filters Movie nodes by title text and movie ID without traversing relationships.
MATCH (m:Movie) WHERE m.title CONTAINS "Love" AND m.movieId >= 99 RETURN m.movieId AS movieId, m.title AS title ORDER BY m.movieId;
Equivalent query in GQL standard:
MATCH (m:ml32m_movie) WHERE m.title CONTAINS $titleText AND m.movieId >= $minMovieId RETURN m.movieId AS movieId, m.title AS title ORDER BY m.movieId LIMIT $limit;
Q6. Filter High-Rating Edges
This query filters RATED relationships by numeric rating and returns their endpoint user and movie.
MATCH (u:User)-[r:RATED]->(m:Movie) WHERE r.rating >= 4.0 RETURN u.userId AS userId, m.movieId AS movieId, m.title AS title, r.rating AS rating ORDER BY rating DESC;
Equivalent query in GQL standard:
MATCH (u:ml32m_user)-[r:ml32m_rated]->(m:ml32m_movie) WHERE r.rating >= $minRating RETURN u.userId AS userId, m.movieId AS movieId, m.title AS title, r.rating AS rating ORDER BY rating DESC LIMIT $limit;
Q7. Check Whether a User Can Reach a Movie
This query tests whether a selected user can reach a selected movie within three undirected RATED hops.
RETURN EXISTS { MATCH (:User {userId: 33})-[:RATED*1..3]-(:Movie {movieId: 66}) } AS reachable;
Equivalent query in GQL standard:
MATCH p = ANY (:ml32m_user {userId: $userId})-[:ml32m_rated]-{1,3}(:ml32m_movie {movieId: $movieId}) RETURN true AS reachable;
Q8. Shortest Co-Rating Path to a Movie
This query finds the shortest undirected RATED path between a selected user and movie.
MATCH (u:User {userId: 33}) MATCH (m:Movie {movieId: 66}) MATCH p = shortestPath((u)-[:RATED*1..4]-(m)) RETURN p, length(p) AS hopCount;
Equivalent query in GQL standard:
MATCH p = SHORTEST (:ml32m_user {userId: $userId})-[:ml32m_rated]-{1,4}(:ml32m_movie {movieId: $movieId}) RETURN p, length(p) AS hopCount;
Q9. Simple Co-Rating Paths up to Three Hops
This query enumerates acyclic RATED paths between a selected user and movie up to three hops.
MATCH p = (:User {userId: 33})-[:RATED*1..3]-(:Movie {movieId: 66}) WHERE all(n IN nodes(p) WHERE single(m IN nodes(p) WHERE m = n)) RETURN p, length(p) AS hopCount ORDER BY hopCount;
Equivalent query in GQL standard:
MATCH p = ACYCLIC (:ml32m_user {userId: $userId})-[:ml32m_rated]-{1,3}(:ml32m_movie {movieId: $movieId}) RETURN p, length(p) AS hopCount ORDER BY hopCount LIMIT $limit;
Q10. Top Weighted Co-Rating Paths
This query ranks bounded RATED paths by the sum of rating values along each path.
MATCH p = (:User {userId: 33})-[rels:RATED*1..3]-(:Movie {movieId: 66}) WHERE all(n IN nodes(p) WHERE single(m IN nodes(p) WHERE m = n)) WITH p, reduce(total = 0.0, r IN relationships(p) | total + coalesce(r.rating, 0.0)) AS ratingWeight RETURN p, length(p) AS hopCount, ratingWeight ORDER BY ratingWeight DESC;
Equivalent query in GQL standard:
MATCH p = ACYCLIC (:ml32m_user {userId: $userId})-[:ml32m_rated]-{1,3}(:ml32m_movie {movieId: $movieId}) WITH p, reduce(total = 0.0, r IN relationships(p) | total + coalesce(r.rating, 0.0)) AS ratingWeight RETURN p, length(p) AS hopCount, ratingWeight ORDER BY ratingWeight DESC LIMIT $limit;
Q11. Three-Movie Triangle by Shared Genres
This query finds three movies where every pair shares at least one genre through HAS_GENRE relationships.
MATCH (a:Movie)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(b:Movie) MATCH (b)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(c:Movie) MATCH (c)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(a) WHERE a.movieId < b.movieId AND b.movieId < c.movieId RETURN DISTINCT a.title AS movieA, b.title AS movieB, c.title AS movieC;
Equivalent query in GQL standard:
MATCH (a:ml32m_movie)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(b:ml32m_movie) MATCH (b)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(c:ml32m_movie) MATCH (c)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(a) WHERE a.movieId < b.movieId AND b.movieId < c.movieId RETURN DISTINCT a.title AS movieA, b.title AS movieB, c.title AS movieC LIMIT $limit;
Q12. Four-Movie Square by Shared Genres
This query finds four movies connected in a cycle where each adjacent pair shares a genre.
MATCH (a:Movie)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(b:Movie) MATCH (b)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(c:Movie) MATCH (c)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(d:Movie) MATCH (d)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(a) WHERE a.movieId < b.movieId AND a.movieId < c.movieId AND a.movieId < d.movieId AND b <> c AND b <> d AND c <> d RETURN DISTINCT a.title AS movieA, b.title AS movieB, c.title AS movieC, d.title AS movieD;
Equivalent query in GQL standard:
MATCH (a:ml32m_movie)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(b:ml32m_movie) MATCH (b)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(c:ml32m_movie) MATCH (c)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(d:ml32m_movie) MATCH (d)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(a) WHERE a.movieId < b.movieId AND a.movieId < c.movieId AND a.movieId < d.movieId AND b <> c AND b <> d AND c <> d RETURN DISTINCT a.title AS movieA, b.title AS movieB, c.title AS movieC, d.title AS movieD LIMIT $limit;
Q13. Users with Many Rated Movies
This query finds user-centered stars by counting how many distinct movies each user rated.
MATCH (u:User)-[:RATED]->(m:Movie) WITH u, count(DISTINCT m) AS ratedMovieCount WHERE ratedMovieCount >= 10 RETURN u.userId AS userId, ratedMovieCount ORDER BY ratedMovieCount DESC;
Equivalent query in GQL standard:
MATCH (u:ml32m_user)-[:ml32m_rated]->(m:ml32m_movie) WITH u, count(DISTINCT m) AS ratedMovieCount WHERE ratedMovieCount >= $minDegree RETURN u.userId AS userId, ratedMovieCount ORDER BY ratedMovieCount DESC LIMIT $limit;
Q14. Four-Movie Clique by Shared Genres
This query finds four movies where every pair shares at least one genre, forming a clique in the derived movie-genre similarity graph.
MATCH (a:Movie)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(b:Movie) MATCH (a)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(c:Movie) MATCH (a)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(d:Movie) MATCH (b)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(c) MATCH (b)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(d) MATCH (c)-[:HAS_GENRE]->(:Genre)<-[:HAS_GENRE]-(d) WHERE a.movieId < b.movieId AND b.movieId < c.movieId AND c.movieId < d.movieId RETURN DISTINCT a.title AS movieA, b.title AS movieB, c.title AS movieC, d.title AS movieD;
Equivalent query in GQL standard:
MATCH (a:ml32m_movie)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(b:ml32m_movie) MATCH (a)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(c:ml32m_movie) MATCH (a)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(d:ml32m_movie) MATCH (b)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(c) MATCH (b)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(d) MATCH (c)-[:ml32m_has_genre]->(:ml32m_genre)<-[:ml32m_has_genre]-(d) WHERE a.movieId < b.movieId AND b.movieId < c.movieId AND c.movieId < d.movieId RETURN DISTINCT a.title AS movieA, b.title AS movieB, c.title AS movieC, d.title AS movieD LIMIT $limit;
Q15. Users Who Rated the Same Movie
This query finds pairs of users connected through a commonly rated movie.
MATCH (u1:User)-[:RATED]->(m:Movie)<-[:RATED]-(u2:User) WHERE u1.userId < u2.userId RETURN u1.userId AS userA, u2.userId AS userB, m.title AS sharedMovie;
Equivalent query in GQL standard:
MATCH (u1:ml32m_user)-[:ml32m_rated]->(m:ml32m_movie)<-[:ml32m_rated]-(u2:ml32m_user) WHERE u1.userId < u2.userId RETURN u1.userId AS userA, u2.userId AS userB, m.title AS sharedMovie LIMIT $limit;
Q16. List users and the movies they rated that belong to the specific genre "Comedy", find top-20 results in descending order of ratings
This query retrieves users and their comedy movie ratings, returning the top 20 highest-rated user-movie pairs.
MATCH (u:User)-[r:RATED]->(m:Movie)-[:HAS_GENRE]->(g:Genre {name: "Comedy"}) RETURN u.userId AS userId, m.title AS movieTitle, g.name AS genre, r.rating AS rating ORDER BY r.rating DESC LIMIT 20;
Equivalent query in GQL standard:
MATCH (u:ml32m_user)-[r:ml32m_rated]->(m:ml32m_movie)-[:ml32m_has_genre]->(g:ml32m_genre {name: "Comedy"}) RETURN u.userId AS userId, m.title AS movieTitle, g.name AS genre, r.rating AS rating ORDER BY r.rating DESC LIMIT 20
Q17. Movie Rating Degree Summary
This query computes global statistics over the number of RATED edges incident to each movie.
MATCH (m:Movie) OPTIONAL MATCH (:User)-[r:RATED]->(m) WITH m, count(r) AS ratingDegree RETURN count(m) AS movieCount, avg(ratingDegree) AS avgRatingDegree, min(ratingDegree) AS minRatingDegree, max(ratingDegree) AS maxRatingDegree;
Equivalent query in GQL standard:
MATCH (m:ml32m_movie) OPTIONAL MATCH (:ml32m_user)-[r:ml32m_rated]->(m) WITH m, count(r) AS ratingDegree RETURN count(m) AS movieCount, avg(ratingDegree) AS avgRatingDegree, min(ratingDegree) AS minRatingDegree, max(ratingDegree) AS maxRatingDegree;
Q18. Find users who have rated movies from at least 3 distinct genres, list the top-10 in descending order of the genre count
This query identifies the top 10 users who have rated movies spanning the most distinct genres (with a minimum of 3 genres).
MATCH (u:User)-[:RATED]->(m:Movie)-[:HAS_GENRE]->(g:Genre) WITH u, COUNT(DISTINCT g.name) AS genreCount WHERE genreCount >= 3 RETURN u.userId AS userId, genreCount ORDER BY genreCount DESC LIMIT 10;
Equivalent query in GQL standard:
MATCH (u:ml32m_user)-[r:ml32m_rated]->(m:ml32m_movie)-[:ml32m_has_genre]->(g:ml32m_genre) WITH u, count(DISTINCT g.name) AS genreCount WHERE genreCount >= 3 RETURN u.userId AS userId, genreCount ORDER BY genreCount DESC LIMIT 10
Q19. Find top-20 movies with the highest number of 5-ratings, return the movies and this rating number
This query identifies and lists the top 20 movies that have received the highest count of perfect 5.0 ratings.
MATCH (:User)-[r:RATED]->(m:Movie) WHERE r.rating = 5.0 RETURN m.title AS movieTitle, COUNT(*) AS fiveStarCount ORDER BY fiveStarCount DESC LIMIT 20;
Equivalent query in GQL standard:
MATCH (:ml32m_user)-[r:ml32m_rated]->(m:ml32m_movie) WHERE r.rating = 5.0 RETURN m.title AS movieTitle, count(*) AS fiveStarCount ORDER BY fiveStarCount DESC LIMIT 20
Q20. Check different rating values and their occurrence frequencies
This query aggregates all ratings in the dataset and returns each rating value along with its total occurrence frequency, sorted by rating value.
MATCH (:User)-[r:RATED]->(:Movie) RETURN r.rating AS rating, COUNT(*) AS frequency ORDER BY rating;
Equivalent query in GQL standard:
MATCH (:ml32m_user)-[r:ml32m_rated]->(:ml32m_movie) RETURN r.rating AS rating, count(*) AS frequency ORDER BY rating
Q21. Find the top-50 most frequent tags, return the tags along with their frequencies
This query ranks and returns the top 50 most frequently used movie tags in descending order of their frequency.
MATCH (:User)-[t:TAGGED]->(:Movie) WHERE t.tag IS NOT NULL RETURN t.tag AS tag, COUNT(*) AS frequency ORDER BY frequency DESC LIMIT 50;
Equivalent query in GQL standard:
MATCH (:User)-[t:ml32m_tagged]->(:Movie) WHERE t.tag IS NOT NULL RETURN t.tag AS tag, count(*) AS frequency ORDER BY frequency DESC LIMIT 50
Q22. User Rating Path Counts by Length
This query counts bounded RATED paths from a selected user grouped by path length.
MATCH p = (:User {userId: 33})-[:RATED*1..3]-(target) RETURN length(p) AS pathLength, count(p) AS pathCount, count(DISTINCT target) AS distinctTargetCount ORDER BY pathLength;
Equivalent query in GQL standard:
MATCH p = (:ml32m_user {userId: $userId})-[:ml32m_rated]-{1,3}(target) RETURN length(p) AS pathLength, count(p) AS pathCount, count(DISTINCT target) AS distinctTargetCount ORDER BY pathLength;
Q23. User Rating Path Counts by Length and Genre
This query groups bounded paths from a selected user by path length and by genre of reached movie endpoints.
MATCH p = (:User {userId: 33})-[:RATED*1..3]-(m:Movie)-[:HAS_GENRE]->(g:Genre) RETURN length(p) - 1 AS pathLength, g.name AS genre, count(p) AS pathCount, count(DISTINCT m) AS distinctMovieCount ORDER BY pathLength, pathCount DESC;
Equivalent query in GQL standard:
MATCH p = (:ml32m_user {userId: $userId})-[:ml32m_rated]-{1,3}(m:ml32m_movie)-[:ml32m_has_genre]->(g:ml32m_genre) RETURN length(p) AS pathLength, g.name AS genre, count(p) AS pathCount, count(DISTINCT m) AS distinctMovieCount ORDER BY pathLength, pathCount DESC LIMIT $limit;
Q24. Find the title of the movies that belong to multiple genres, e.g., "Comedy" and "Romance"
This query finds and lists the titles of the first 10 movies that are classified under both the Comedy and Romance genres.
MATCH (m:Movie)-[:HAS_GENRE]->(g1:Genre {name: "Comedy"}) MATCH (m)-[:HAS_GENRE]->(g2:Genre {name: "Romance"}) RETURN m.title AS movieTitle LIMIT 10;
Equivalent query in GQL standard:
MATCH (m:ml32m_movie)-[:ml32m_has_genre]->(g1:ml32m_genre {name: "Comedy"}) MATCH (m)-[:ml32m_has_genre]->(g2:ml32m_genre {name: "Romance"}) RETURN m.title AS movieTitle LIMIT 10
Q25. Find users who have rated movies in both "Action" and "Romance" genres, list 10 userIds
This query identifies and lists up to 10 unique user IDs who have rated at least one Action movie and at least one Romance movie.
MATCH (u:User)-[:RATED]->(m1:Movie)-[:HAS_GENRE]->(g1:Genre {name: "Action"}) MATCH (u)-[:RATED]->(m2:Movie)-[:HAS_GENRE]->(g2:Genre {name: "Romance"}) RETURN DISTINCT u.userId AS userId LIMIT 10;
Equivalent query in GQL standard:
MATCH (u:ml32m_user)-[r:ml32m_rated]->(m1:ml32m_movie)-[:ml32m_has_genre]->(g1:ml32m_genre {name: "Action"}) MATCH (u)-[r:ml32m_rated]->(m2:ml32m_movie)-[:ml32m_has_genre]->(g2:ml32m_genre {name: "Romance"}) RETURN DISTINCT u.userId AS userId LIMIT 10
Q26. Degree Centrality on the User-Movie Rating Graph
This query projects the bipartite User-Movie rating graph and ranks nodes by degree centrality.
CALL gds.graph.project( 'ML32MRatingGraph', ['User', 'Movie'], {RATED: {type: 'RATED', orientation: 'UNDIRECTED'}} ); CALL gds.degree.stream('ML32MRatingGraph') YIELD nodeId, score RETURN labels(gds.util.asNode(nodeId)) AS labels, coalesce(gds.util.asNode(nodeId).userId, gds.util.asNode(nodeId).movieId) AS id, gds.util.asNode(nodeId).title AS title, score ORDER BY score DESC;
Equivalent query in GQL standard:
MATCH (n:ml32m_user|ml32m_movie) OPTIONAL MATCH (n)-[r:ml32m_rated]-() WITH n, count(r) AS score RETURN coalesce(n.userId, n.movieId) AS id, n.title AS title, score ORDER BY score DESC LIMIT $limit;
Q27. PageRank on the User-Movie Rating Graph
This query runs PageRank over the projected User-Movie rating graph and returns the highest scoring nodes.
CALL gds.graph.project( 'ML32MRatingGraphPageRank', ['User', 'Movie'], {RATED: {type: 'RATED', orientation: 'UNDIRECTED'}} ); CALL gds.pageRank.stream('ML32MRatingGraphPageRank') YIELD nodeId, score RETURN labels(gds.util.asNode(nodeId)) AS labels, coalesce(gds.util.asNode(nodeId).userId, gds.util.asNode(nodeId).movieId) AS id, gds.util.asNode(nodeId).title AS title, score ORDER BY score DESC;
Q28. Weakly Connected Components of Ratings
This query finds weakly connected components in the undirected User-Movie rating graph.
CALL gds.graph.project( 'ML32MRatingGraphWcc', ['User', 'Movie'], {RATED: {type: 'RATED', orientation: 'UNDIRECTED'}} ); CALL gds.wcc.stream('ML32MRatingGraphWcc') YIELD nodeId, componentId RETURN componentId, count(*) AS componentSize ORDER BY componentSize DESC;
Q29. Strongly Connected Components of Directed Ratings
This query treats RATED edges as directed from users to movies and reports strongly connected components.
Q30. Louvain Communities in the Rating Graph
This query detects communities in the undirected User-Movie rating graph using Louvain.
CALL gds.graph.project( 'ML32MRatingGraphLouvain', ['User', 'Movie'], {RATED: {type: 'RATED', orientation: 'UNDIRECTED'}} ); CALL gds.louvain.stream('ML32MRatingGraphLouvain') YIELD nodeId, communityId RETURN communityId, count(*) AS communitySize ORDER BY communitySize DESC;
Q31. Find movies that span at least 3 genres and have at least 50 user ratings, return top-10 movies in descending order of their rating count, along with the list of genres and the rating count
This query retrieves the top 10 most rated movies that belong to at least 3 genres and have received at least 50 user ratings, showing their titles, genres, and rating counts.
MATCH (m:Movie)-[:HAS_GENRE]->(g:Genre) MATCH (u:User)-[r:RATED]->(m) WITH m, collect(DISTINCT g.name) AS genres, count(DISTINCT r) AS ratingCount WHERE size(genres) >= 3 AND ratingCount >= 50 RETURN m.title AS movie, genres, ratingCount ORDER BY ratingCount DESC LIMIT 10;
Equivalent query in GQL standard:
MATCH (m:ml32m_movie)-[:ml32m_has_genre]->(g:ml32m_genre) MATCH (u:ml32m_user)-[r:ml32m_rated]->(m) WITH m, collect(DISTINCT g.name) AS genres, count(r) AS ratingCount WHERE size(genres) >= 3 AND ratingCount >= 50 RETURN m.title AS movie, genres, ratingCount ORDER BY ratingCount DESC LIMIT 10
Q32. Find top-20 movies with the highest number of genres + tags, return this number, list of their genres, and list of their tags
This query finds the top 20 movies with the largest combined count of unique genres and tags, returning the total count along with their genre and tag lists.
MATCH (m:Movie) OPTIONAL MATCH (m)-[:HAS_GENRE]->(g:Genre) OPTIONAL MATCH (:User)-[t:TAGGED]->(m) WITH m, COLLECT(DISTINCT g.name) AS genres, COLLECT(DISTINCT t.tag) AS tags, SIZE(COLLECT(DISTINCT g.name)) + SIZE(COLLECT(DISTINCT t.tag)) AS total ORDER BY total DESC LIMIT 20 RETURN m.title AS movieTitle, total, genres, tags;
Equivalent query in GQL standard:
MATCH (m:ml32m_movie) OPTIONAL MATCH (m)-[:ml32m_has_genre]->(g:ml32m_genre) OPTIONAL MATCH (:ml32m_user)-[t:ml32m_tagged]->(m) WITH m, collect(DISTINCT g.name) AS genres, collect(DISTINCT t.tag) AS tags, size(collect(DISTINCT g.name)) + size(collect(DISTINCT t.tag)) AS total ORDER BY total DESC LIMIT 20 RETURN m.title AS movieTitle, total, genres, tags
Q33. Find (user, movie) pairs where the movie is first rated by the user, and then tagged by the user, return 10 such user-movie pairs with the latest timestamps, along with the rating and the list of tags (tagged after the rating)
This query retrieves the top 10 user-movie pairs where the user rated a movie before tagging it, sorted by the rating timestamp.
MATCH (u:User)-[r:RATED]->(m:Movie)<-[t:TAGGED]-(u) WHERE r.timestamp < t.timestamp WITH u.userId AS userId, m.title AS movieTitle, r.rating AS rating, r.timestamp AS ratingTime, COLLECT(DISTINCT t.tag) AS tags RETURN userId, movieTitle, rating, tags ORDER BY ratingTime DESC LIMIT 10;
Equivalent query in GQL standard:
MATCH (u:ml32m_user)-[r:ml32m_rated]->(m:ml32m_movie)<-[t:ml32m_tagged]-(u) WHERE r.timestamp < t.timestamp WITH u.userId AS userId, m.title AS movieTitle, r.rating AS rating, r.timestamp AS ratingTime, collect(DISTINCT t.tag) AS tags RETURN userId, movieTitle, rating, tags ORDER BY ratingTime DESC LIMIT 10