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 |
Total import time: 13.04 Seconds
Data Loading Overview
Overview
We mainly used the TigerGraph GSQLShell to import the data.
Graph Schema
Use the GSQLShell tool on the TigerGraph browser UI to create vertex and edge types, and the graph. We created the reverse edge for RATED edge because it is necessary to run the Louvain community detection query.
USE GLOBAL CREATE VERTEX ML32M_Movie ( PRIMARY_ID movie_id INT, title STRING ) WITH PRIMARY_ID_AS_ATTRIBUTE="true" CREATE VERTEX ML32M_Genre ( PRIMARY_ID name STRING ) WITH PRIMARY_ID_AS_ATTRIBUTE="true" CREATE VERTEX ML32M_User ( PRIMARY_ID user_id INT ) WITH PRIMARY_ID_AS_ATTRIBUTE="true" CREATE DIRECTED EDGE ML32M_RATED ( FROM ML32M_User, TO ML32M_Movie, rating FLOAT, timestamp UINT ) WITH REVERSE_EDGE="ML32M_RATED_REVERSE" CREATE DIRECTED EDGE ML32M_HAS_GENRE ( FROM ML32M_Movie, TO ML32M_Genre ) CREATE DIRECTED EDGE ML32M_TAGGED ( FROM ML32M_User, TO ML32M_Movie, DISCRIMINATOR ( tag STRING, timestamp UINT ) ) CREATE GRAPH ml32m ( ML32M_Movie, ML32M_Genre, ML32M_User, ML32M_RATED, ML32M_RATED_REVERSE, ML32M_HAS_GENRE, ML32M_TAGGED )
Import Data
For data import, we will first define a few loading job, and then run them one by one using the correponding source files.
USE GRAPH ml32m CREATE LOADING JOB load_ml32m_movies FOR GRAPH ml32m { DEFINE FILENAME f_movies; LOAD f_movies TO VERTEX ML32M_Movie VALUES($0, $1) USING SEPARATOR="\t", HEADER="true", EOL="\n"; } CREATE LOADING JOB load_ml32m_csv_vertices FOR GRAPH ml32m { DEFINE FILENAME f_genres; DEFINE FILENAME f_users; LOAD f_genres TO VERTEX ML32M_Genre VALUES($0) USING SEPARATOR=",", HEADER="true", EOL="\n"; LOAD f_users TO VERTEX ML32M_User VALUES($0) USING SEPARATOR=",", HEADER="true", EOL="\n"; } CREATE LOADING JOB load_ml32m_csv_edges FOR GRAPH ml32m { DEFINE FILENAME f_has_genre; DEFINE FILENAME f_ratings; LOAD f_has_genre TO EDGE ML32M_HAS_GENRE VALUES($0, $1) USING SEPARATOR=",", HEADER="true", EOL="\n", VERTEX_MUST_EXIST="true"; LOAD f_ratings TO EDGE ML32M_RATED VALUES($0, $1, $2, $3) USING SEPARATOR=",", HEADER="true", EOL="\n", VERTEX_MUST_EXIST="true"; } CREATE LOADING JOB load_ml32m_tags FOR GRAPH ml32m { DEFINE FILENAME f_tags; LOAD f_tags TO EDGE ML32M_TAGGED VALUES($0, $1, $2, $3) USING SEPARATOR="|", HEADER="true", EOL="\n", VERTEX_MUST_EXIST="true"; }
We need to run the following python script to create users.csv for the import of User node.
import argparse import csv import os from pathlib import Path def generate(source: Path, destination: Path) -> tuple[int, int]: user_ids: set[int] = set() rating_count = 0 with source.open("r", encoding="utf-8", newline="") as input_file: reader = csv.reader(input_file) header = next(reader, None) expected_header = ["userId", "movieId", "rating", "timestamp"] if header != expected_header: raise ValueError(f"Unexpected header: {header!r}; expected {expected_header!r}") for line_number, row in enumerate(reader, start=2): if len(row) != 4: raise ValueError(f"{source}:{line_number}: expected 4 columns") try: user_id = int(row[0]) except ValueError as exc: raise ValueError(f"{source}:{line_number}: invalid userId {row[0]!r}") from exc if user_id < 0: raise ValueError(f"{source}:{line_number}: userId must not be negative") user_ids.add(user_id) rating_count += 1 if rating_count % 5_000_000 == 0: print(f"Scanned {rating_count:,} rating rows...", flush=True) temporary = destination.with_name(f".{destination.name}.tmp") try: with temporary.open("w", encoding="utf-8", newline="") as output_file: writer = csv.writer(output_file, lineterminator="\n") writer.writerow(["userId"]) for user_id in sorted(user_ids): writer.writerow([user_id]) os.replace(temporary, destination) except BaseException: temporary.unlink(missing_ok=True) raise return len(user_ids), rating_count def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--directory", type=Path, default=Path(__file__).resolve().parent) args = parser.parse_args() directory = args.directory.resolve() user_count, rating_count = generate(directory / "ratings_df.csv", directory / "users.csv") print(f"Scanned {rating_count:,} total rating rows.") print(f"Created users.csv with {user_count:,} unique users.") if __name__ == "__main__": main()
We need to run the following python script to create movies_tg.tsv for the import of Movie node.
import argparse import csv import os from pathlib import Path def generate(source: Path, destination: Path) -> int: temporary = destination.with_name(f".{destination.name}.tmp") count = 0 try: with source.open("r", encoding="utf-8", newline="") as input_file, temporary.open( "w", encoding="utf-8", newline="" ) as output_file: reader = csv.reader(input_file) header = next(reader, None) expected_header = ["movieId", "title", "genres"] if header != expected_header: raise ValueError(f"Unexpected header: {header!r}; expected {expected_header!r}") writer = csv.writer( output_file, delimiter="\t", quotechar=None, quoting=csv.QUOTE_NONE, lineterminator="\n", ) writer.writerow(["movieId", "title"]) for line_number, row in enumerate(reader, start=2): if len(row) != 3: raise ValueError(f"{source}:{line_number}: expected 3 columns") movie_id, title, _genres = row if not movie_id.isdigit() or not title: raise ValueError(f"{source}:{line_number}: invalid movieId or title") if any(character in title for character in ("\t", "\r", "\n")): raise ValueError(f"{source}:{line_number}: title is not TSV-safe") writer.writerow([movie_id, title]) count += 1 os.replace(temporary, destination) except BaseException: temporary.unlink(missing_ok=True) raise return count def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--directory", type=Path, default=Path(__file__).resolve().parent) args = parser.parse_args() directory = args.directory.resolve() count = generate(directory / "movies.csv", directory / "movies_tg.tsv") print(f"Created movies_tg.tsv with {count:,} movies.") if __name__ == "__main__": main()
We need to run the following python script to create tags_tg.psv for the import of TAGGED edge.
import argparse import csv import os from pathlib import Path def generate(source: Path, destination: Path) -> tuple[int, int]: temporary = destination.with_name(f".{destination.name}.tmp") count = 0 whitespace_only_count = 0 try: with source.open("r", encoding="utf-8", newline="") as input_file, temporary.open( "w", encoding="utf-8", newline="" ) as output_file: reader = csv.reader(input_file) header = next(reader, None) expected_header = ["userId", "movieId", "tag", "timestamp"] if header != expected_header: raise ValueError(f"Unexpected header: {header!r}; expected {expected_header!r}") writer = csv.writer( output_file, delimiter="|", quotechar=None, quoting=csv.QUOTE_NONE, lineterminator="\n", ) writer.writerow(expected_header) for line_number, row in enumerate(reader, start=2): if len(row) != 4: raise ValueError(f"{source}:{line_number}: expected 4 columns") user_id, movie_id, tag, timestamp = row try: numeric_values = (int(user_id), int(movie_id), int(timestamp)) except ValueError as exc: raise ValueError( f"{source}:{line_number}: invalid numeric ID or timestamp" ) from exc if any(value < 0 for value in numeric_values): raise ValueError(f"{source}:{line_number}: negative ID or timestamp") if not tag: raise ValueError(f"{source}:{line_number}: tag is an empty string") if any(character in tag for character in ("|", "\r", "\n")): raise ValueError(f"{source}:{line_number}: tag is not PSV-safe") whitespace_only_count += not tag.strip() writer.writerow([user_id, movie_id, tag, timestamp]) count += 1 os.replace(temporary, destination) except BaseException: temporary.unlink(missing_ok=True) raise return count, whitespace_only_count def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--directory", type=Path, default=Path(__file__).resolve().parent) args = parser.parse_args() directory = args.directory.resolve() count, whitespace_only_count = generate( directory / "tags.csv", directory / "tags_tg.psv" ) print(f"Created tags_tg.psv with {count:,} tags.") print(f"Preserved {whitespace_only_count:,} whitespace-only tags unchanged.") if __name__ == "__main__": main()
Finally, run the following gsql commands to complete the loading jobs:
0.81s for Movie nodes, 0.20s for Genre nodes, 0.41s for User nodes, 0.21s for HAS_GENRE edges, 10.67s for RATED edges, 0.74s for TAGGED edges.USE GRAPH ml32m RUN LOADING JOB load_ml32m_movies USING f_movies="m1:/datasets/prop_datasets/movielens/ml-32m/movies_tg.tsv" RUN LOADING JOB load_ml32m_csv_vertices USING f_genres="m1:/datasets/prop_datasets/movielens/ml-32m/genres.csv", f_users="m1:/datasets/prop_datasets/movielens/ml-32m/users.csv" RUN LOADING JOB load_ml32m_csv_edges USING f_has_genre="m1:/datasets/prop_datasets/movielens/ml-32m/has_genre_edge.csv", f_ratings="m1:/datasets/prop_datasets/movielens/ml-32m/ratings_df.csv" RUN LOADING JOB load_ml32m_tags USING f_tags="m1:/datasets/prop_datasets/movielens/ml-32m/tags_tg.psv"
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q01_movies_rated_by_user() FOR GRAPH ml32m SYNTAX V2 { SumAccum<FLOAT> @rating; MaxAccum<UINT> @rating_time; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Result = SELECT m FROM Start:u -(ML32M_RATED>:r)- ML32M_Movie:m ACCUM m.@rating = r.rating, m.@rating_time = r.timestamp ORDER BY m.@rating DESC, m.title ASC; PRINT Result[Result.movie_id, Result.title, Result.@rating, Result.@rating_time]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q02_genres_within_two_hops() FOR GRAPH ml32m SYNTAX V2 { Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Result = SELECT g FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g ORDER BY g.name ASC; PRINT Result[Result.name]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q03_users_two_rating_hops_away() FOR GRAPH ml32m SYNTAX V2 { Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Result = SELECT other FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_RATED_REVERSE>)- ML32M_User:other WHERE other.user_id != 33 ORDER BY other.user_id ASC; PRINT Result[Result.user_id]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q04_highly_rated_movies() FOR GRAPH ml32m SYNTAX V2 { SumAccum<FLOAT> @rating; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Result = SELECT m FROM Start:u -(ML32M_RATED>:r)- ML32M_Movie:m WHERE r.rating >= 4.0 ACCUM m.@rating = r.rating ORDER BY m.@rating DESC, m.title ASC; PRINT Result[Result.movie_id, Result.title, Result.@rating]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q05_filter_movies_by_title() FOR GRAPH ml32m SYNTAX V2 { Result = SELECT m FROM ML32M_Movie:m WHERE instr(m.title, "Love") >= 0 AND m.movie_id >= 99 ORDER BY m.movie_id ASC; PRINT Result[Result.movie_id, Result.title]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q06_filter_high_rating_edges() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT userId, INT movieId, STRING title, FLOAT rating> Row; ListAccum<Row> @@rows; Tmp1 = SELECT m FROM ML32M_User:u -(ML32M_RATED>:r)- ML32M_Movie:m WHERE r.rating >= 4.0 ACCUM @@rows += Row(u.user_id, m.movie_id, m.title, r.rating); PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q07_user_can_reach_movie() FOR GRAPH ml32m SYNTAX V2 { OrAccum @@reachable; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Tmp = SELECT m FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m WHERE m.movie_id == 66 ACCUM @@reachable += TRUE; IF NOT @@reachable THEN Tmp = SELECT m2 FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m1 -(ML32M_RATED_REVERSE>)- ML32M_User:u2 -(ML32M_RATED>)- ML32M_Movie:m2 WHERE m2.movie_id == 66 AND u2 != u AND m1 != m2 ACCUM @@reachable += TRUE; END; PRINT @@reachable 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.
// The rating graph is bipartite, so User->Movie paths can only have odd length. CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q08_shortest_corating_path() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT startUserId, INT viaMovieId, INT viaUserId, INT targetMovieId, INT hopCount> PathRow; HeapAccum<PathRow>(1, hopCount ASC, viaMovieId ASC, viaUserId ASC) @@path; OrAccum @@direct; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Tmp = SELECT m FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m WHERE m.movie_id == 66 ACCUM @@direct += TRUE, @@path += PathRow(u.user_id, -1, -1, m.movie_id, 1); IF NOT @@direct THEN Tmp = SELECT m2 FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m1 -(ML32M_RATED_REVERSE>)- ML32M_User:u2 -(ML32M_RATED>)- ML32M_Movie:m2 WHERE m2.movie_id == 66 AND u2 != u AND m1 != m2 ACCUM @@path += PathRow(u.user_id, m1.movie_id, u2.user_id, m2.movie_id, 3); END; PRINT @@path AS path; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q09_simple_corating_paths() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT startUserId, INT viaMovieId, INT viaUserId, INT targetMovieId, INT hopCount> PathRow; ListAccum<PathRow> @@paths; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Tmp = SELECT m FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m WHERE m.movie_id == 66 ACCUM @@paths += PathRow(u.user_id, -1, -1, m.movie_id, 1); Tmp = SELECT m2 FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m1 -(ML32M_RATED_REVERSE>)- ML32M_User:u2 -(ML32M_RATED>)- ML32M_Movie:m2 WHERE m2.movie_id == 66 AND u2 != u AND m1 != m2 ACCUM @@paths += PathRow(u.user_id, m1.movie_id, u2.user_id, m2.movie_id, 3); PRINT @@paths AS paths; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q10_top_weighted_corating_paths() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT startUserId, INT viaMovieId, INT viaUserId, INT targetMovieId, INT hopCount, FLOAT ratingWeight> PathRow; ListAccum<PathRow> @@paths; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Tmp = SELECT m FROM Start:u -(ML32M_RATED>:r)- ML32M_Movie:m WHERE m.movie_id == 66 ACCUM @@paths += PathRow(u.user_id, -1, -1, m.movie_id, 1, r.rating); Tmp = SELECT m2 FROM Start:u -(ML32M_RATED>:r1)- ML32M_Movie:m1 -(ML32M_RATED_REVERSE>:r2)- ML32M_User:u2 -(ML32M_RATED>:r3)- ML32M_Movie:m2 WHERE m2.movie_id == 66 AND u2 != u AND m1 != m2 ACCUM @@paths += PathRow(u.user_id, m1.movie_id, u2.user_id, m2.movie_id, 3, r1.rating + r2.rating + r3.rating); PRINT @@paths AS paths; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q11_three_movie_genre_triangle() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT movieAId, STRING movieA, INT movieBId, STRING movieB, INT movieCId, STRING movieC> Row; SetAccum<Row> @@distinct_rows; Tmp = SELECT c FROM ML32M_Movie:a -(ML32M_HAS_GENRE>)- ML32M_Genre:g1 -(<ML32M_HAS_GENRE)- ML32M_Movie:b -(ML32M_HAS_GENRE>)- ML32M_Genre:g2 -(<ML32M_HAS_GENRE)- ML32M_Movie:c -(ML32M_HAS_GENRE>)- ML32M_Genre:g3 -(<ML32M_HAS_GENRE)- ML32M_Movie:a WHERE a.movie_id < b.movie_id AND b.movie_id < c.movie_id ACCUM @@distinct_rows += Row(a.movie_id, a.title, b.movie_id, b.title, c.movie_id, c.title); PRINT @@distinct_rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q12_four_movie_genre_square() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT aId, STRING movieA, INT bId, STRING movieB, INT cId, STRING movieC, INT dId, STRING movieD> Row; SetAccum<Row> @@distinct_rows; Tmp = SELECT d FROM ML32M_Movie:a -(ML32M_HAS_GENRE>)- ML32M_Genre:g1 -(<ML32M_HAS_GENRE)- ML32M_Movie:b -(ML32M_HAS_GENRE>)- ML32M_Genre:g2 -(<ML32M_HAS_GENRE)- ML32M_Movie:c -(ML32M_HAS_GENRE>)- ML32M_Genre:g3 -(<ML32M_HAS_GENRE)- ML32M_Movie:d -(ML32M_HAS_GENRE>)- ML32M_Genre:g4 -(<ML32M_HAS_GENRE)- ML32M_Movie:a WHERE a.movie_id < b.movie_id AND a.movie_id < c.movie_id AND a.movie_id < d.movie_id AND b != c AND b != d AND c != d ACCUM @@distinct_rows += Row(a.movie_id, a.title, b.movie_id, b.title, c.movie_id, c.title, d.movie_id, d.title); PRINT @@distinct_rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q13_users_with_many_rated_movies( INT min_degree = 10 ) FOR GRAPH ml32m SYNTAX V2 { SetAccum<VERTEX<ML32M_Movie>> @movies; Users = SELECT u FROM ML32M_User:u -(ML32M_RATED>)- ML32M_Movie:m ACCUM u.@movies += m; Result = SELECT u FROM Users:u WHERE u.@movies.size() >= min_degree ORDER BY u.@movies.size() DESC, u.user_id ASC; PRINT Result[Result.user_id, Result.@movies.size()]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q14_four_movie_genre_clique() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT aId, STRING movieA, INT bId, STRING movieB, INT cId, STRING movieC, INT dId, STRING movieD> Row; SetAccum<Row> @@distinct_rows; // Six independently matched shared-genre relations enforce a 4-clique. Tmp = SELECT d FROM ML32M_Movie:a -(ML32M_HAS_GENRE>)- ML32M_Genre:g_ab -(<ML32M_HAS_GENRE)- ML32M_Movie:b, ML32M_Movie:a -(ML32M_HAS_GENRE>)- ML32M_Genre:g_ac -(<ML32M_HAS_GENRE)- ML32M_Movie:c, ML32M_Movie:a -(ML32M_HAS_GENRE>)- ML32M_Genre:g_ad -(<ML32M_HAS_GENRE)- ML32M_Movie:d, ML32M_Movie:b -(ML32M_HAS_GENRE>)- ML32M_Genre:g_bc -(<ML32M_HAS_GENRE)- ML32M_Movie:c, ML32M_Movie:b -(ML32M_HAS_GENRE>)- ML32M_Genre:g_bd -(<ML32M_HAS_GENRE)- ML32M_Movie:d, ML32M_Movie:c -(ML32M_HAS_GENRE>)- ML32M_Genre:g_cd -(<ML32M_HAS_GENRE)- ML32M_Movie:d WHERE a.movie_id < b.movie_id AND b.movie_id < c.movie_id AND c.movie_id < d.movie_id ACCUM @@distinct_rows += Row(a.movie_id, a.title, b.movie_id, b.title, c.movie_id, c.title, d.movie_id, d.title); PRINT @@distinct_rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q15_users_rated_same_movie() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT userA, INT userB, INT movieId, STRING sharedMovie> Row; ListAccum<Row> @@rows; Tmp2 = SELECT u2 FROM ML32M_User:u1 -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_RATED_REVERSE>)- ML32M_User:u2 WHERE u1.user_id < u2.user_id ACCUM @@rows += Row(u1.user_id, u2.user_id, m.movie_id, m.title); PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q16_top_comedy_ratings() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT userId, INT movieId, STRING movieTitle, STRING genre, FLOAT rating> Row; HeapAccum<Row>(20, rating DESC, userId ASC, movieId ASC) @@rows; Tmp = SELECT g FROM ML32M_User:u -(ML32M_RATED>:r)- ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g WHERE g.name == "Comedy" ACCUM @@rows += Row(u.user_id, m.movie_id, m.title, g.name, r.rating); PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q17_movie_rating_degree_summary() FOR GRAPH ml32m SYNTAX V2 { SumAccum<INT> @rating_degree; SumAccum<INT> @@movie_count; SumAccum<DOUBLE> @@degree_sum; MinAccum<INT> @@degree_min; MaxAccum<INT> @@degree_max; Tmp = SELECT m FROM ML32M_User:u -(ML32M_RATED>)- ML32M_Movie:m ACCUM m.@rating_degree += 1; Tmp = SELECT m FROM ML32M_Movie:m ACCUM @@movie_count += 1, @@degree_sum += m.@rating_degree, @@degree_min += m.@rating_degree, @@degree_max += m.@rating_degree; PRINT @@movie_count AS movieCount, @@degree_sum / @@movie_count AS avgRatingDegree, @@degree_min AS minRatingDegree, @@degree_max 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).
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q18_users_with_three_genres() FOR GRAPH ml32m SYNTAX V2 { SetAccum<STRING> @genres; Users = SELECT u FROM ML32M_User:u -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g ACCUM u.@genres += g.name; Result = SELECT u FROM Users:u WHERE u.@genres.size() >= 3 ORDER BY u.@genres.size() DESC, u.user_id ASC LIMIT 10; PRINT Result[Result.user_id, Result.@genres.size()]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q19_top_five_star_movies() FOR GRAPH ml32m SYNTAX V2 { SumAccum<INT> @five_star_count; Movies = SELECT m FROM ML32M_User:u -(ML32M_RATED>:r)- ML32M_Movie:m WHERE r.rating == 5.0 ACCUM m.@five_star_count += 1; Result = SELECT m FROM Movies:m ORDER BY m.@five_star_count DESC, m.movie_id ASC LIMIT 20; PRINT Result[Result.title, Result.@five_star_count]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q20_rating_frequencies() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<FLOAT rating, INT frequency> Row; GroupByAccum<FLOAT rating, SumAccum<INT> frequency> @@frequencies; // MovieLens ratings have ten legal half-star values; 100 leaves room for // unexpected values while preserving the source query's ascending order. HeapAccum<Row>(100, rating ASC) @@rows; Tmp = SELECT m FROM ML32M_User:u -(ML32M_RATED>:r)- ML32M_Movie:m ACCUM @@frequencies += (r.rating -> 1); FOREACH group_row IN @@frequencies DO @@rows += Row(group_row.rating, group_row.frequency); END; PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q21_top_tags() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<STRING tag, INT frequency> Row; GroupByAccum<STRING tag, SumAccum<INT> frequency> @@frequencies; HeapAccum<Row>(50, frequency DESC, tag ASC) @@rows; Tmp = SELECT m FROM ML32M_User:u -(ML32M_TAGGED>:t)- ML32M_Movie:m WHERE t.tag != "" ACCUM @@frequencies += (t.tag -> 1); FOREACH group_row IN @@frequencies DO @@rows += Row(group_row.tag, group_row.frequency); END; PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q22_user_rating_path_counts() FOR GRAPH ml32m SYNTAX V2 { SumAccum<INT> @@path_count_1, @@path_count_2, @@path_count_3; SetAccum<VERTEX> @@targets_1, @@targets_2, @@targets_3; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Tmp1 = SELECT m FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m ACCUM @@path_count_1 += 1, @@targets_1 += m; Tmp2 = SELECT u2 FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_RATED_REVERSE>)- ML32M_User:u2 WHERE u2 != u ACCUM @@path_count_2 += 1, @@targets_2 += u2; Tmp3 = SELECT m2 FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m1 -(ML32M_RATED_REVERSE>)- ML32M_User:u2 -(ML32M_RATED>)- ML32M_Movie:m2 WHERE u2 != u AND m2 != m1 ACCUM @@path_count_3 += 1, @@targets_3 += m2; PRINT 1 AS pathLength, @@path_count_1 AS pathCount, @@targets_1.size() AS distinctTargetCount; PRINT 2 AS pathLength, @@path_count_2 AS pathCount, @@targets_2.size() AS distinctTargetCount; PRINT 3 AS pathLength, @@path_count_3 AS pathCount, @@targets_3.size() AS distinctTargetCount; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q23_user_path_counts_by_genre() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT pathLength, STRING genre, INT movieId> MovieKey; TYPEDEF TUPLE<INT pathLength, STRING genre> GroupKey; TYPEDEF TUPLE<INT pathLength, STRING genre, INT pathCount, INT distinctMovieCount> Row; GroupByAccum<INT pathLength, STRING genre, SumAccum<INT> pathCount> @@groups; SetAccum<MovieKey> @@distinct_movies; MapAccum<GroupKey, INT> @@distinct_counts; ListAccum<Row> @@rows; Start = SELECT u FROM ML32M_User:u WHERE u.user_id == 33; Tmp = SELECT g FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g ACCUM @@groups += (1, g.name -> 1), @@distinct_movies += MovieKey(1, g.name, m.movie_id); Tmp = SELECT g FROM Start:u -(ML32M_RATED>)- ML32M_Movie:m1 -(ML32M_RATED_REVERSE>)- ML32M_User:u2 -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g WHERE u2 != u AND m != m1 ACCUM @@groups += (3, g.name -> 1), @@distinct_movies += MovieKey(3, g.name, m.movie_id); FOREACH movie_key IN @@distinct_movies DO @@distinct_counts += (GroupKey(movie_key.pathLength, movie_key.genre) -> 1); END; FOREACH group_row IN @@groups DO @@rows += Row(group_row.pathLength, group_row.genre, group_row.pathCount, @@distinct_counts.get( GroupKey(group_row.pathLength, group_row.genre))); END; PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q24_comedy_and_romance_movies() FOR GRAPH ml32m SYNTAX V2 { OrAccum @comedy; OrAccum @romance; Movies = SELECT m FROM ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g ACCUM CASE WHEN g.name == "Comedy" THEN m.@comedy += TRUE WHEN g.name == "Romance" THEN m.@romance += TRUE END; Result = SELECT m FROM Movies:m WHERE m.@comedy AND m.@romance ORDER BY m.movie_id ASC LIMIT 10; PRINT Result[Result.title]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q25_users_action_and_romance() FOR GRAPH ml32m SYNTAX V2 { OrAccum @action; OrAccum @romance; Users = SELECT u FROM ML32M_User:u -(ML32M_RATED>)- ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g ACCUM CASE WHEN g.name == "Action" THEN u.@action += TRUE WHEN g.name == "Romance" THEN u.@romance += TRUE END; Result = SELECT u FROM Users:u WHERE u.@action AND u.@romance ORDER BY u.user_id ASC LIMIT 10; PRINT Result[Result.user_id]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q26_degree_centrality() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<VERTEX node, INT score> Row; ListAccum<Row> @@rows; SumAccum<INT> @degree; Users = ML32M_User.*; Movies = ML32M_Movie.*; All = Users UNION Movies; Tmp = SELECT t FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t ACCUM s.@degree += 1; Tmp = SELECT s FROM All:s ACCUM @@rows += Row(s, s.@degree); PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q27_pagerank( FLOAT damping = 0.85, INT maximum_iteration = 20, FLOAT convergence = 0.000001 ) FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<VERTEX node, FLOAT score> Row; ListAccum<Row> @@rows; SumAccum<INT> @degree; SumAccum<FLOAT> @score; SumAccum<FLOAT> @received; SumAccum<INT> @@vertex_count; SumAccum<FLOAT> @@dangling_mass; SumAccum<FLOAT> @@change; INT iteration = 0; Users = ML32M_User.*; Movies = ML32M_Movie.*; All = Users UNION Movies; Tmp = SELECT t FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t ACCUM s.@degree += 1; Tmp = SELECT s FROM All:s ACCUM @@vertex_count += 1; All = SELECT s FROM All:s POST-ACCUM s.@score = 1.0 / @@vertex_count; WHILE iteration < maximum_iteration DO @@dangling_mass = 0; @@change = 0; All = SELECT s FROM All:s POST-ACCUM s.@received = 0, IF s.@degree == 0 THEN @@dangling_mass += s.@score END; Tmp = SELECT t FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t WHERE s.@degree > 0 ACCUM t.@received += s.@score / s.@degree; All = SELECT s FROM All:s POST-ACCUM FLOAT new_score = (1.0 - damping) / @@vertex_count + damping * (s.@received + @@dangling_mass / @@vertex_count), @@change += abs(new_score - s.@score), s.@score = new_score; iteration = iteration + 1; IF @@change <= convergence THEN BREAK; END; END; Tmp = SELECT s FROM All:s ACCUM @@rows += Row(s, s.@score); PRINT @@rows AS results, iteration AS iterations, @@change AS finalChange; }
Q28. Weakly Connected Components of Ratings
This query finds weakly connected components in the undirected User-Movie rating graph.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q28_weakly_connected_components( INT maximum_iteration = 1000 ) FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT componentId, INT componentSize> Row; ListAccum<Row> @@rows; MinAccum<INT> @component; MinAccum<INT> @candidate; SumAccum<INT> @@changes; GroupByAccum<INT componentId, SumAccum<INT> componentSize> @@sizes; INT iteration = 0; Users = ML32M_User.*; Movies = ML32M_Movie.*; All = Users UNION Movies; All = SELECT s FROM All:s POST-ACCUM s.@component = getvid(s), s.@candidate = getvid(s); WHILE iteration < maximum_iteration DO @@changes = 0; All = SELECT s FROM All:s POST-ACCUM s.@candidate = s.@component; Tmp = SELECT t FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t ACCUM t.@candidate += s.@component; All = SELECT s FROM All:s POST-ACCUM IF s.@candidate < s.@component THEN s.@component = s.@candidate, @@changes += 1 END; iteration = iteration + 1; IF @@changes == 0 THEN BREAK; END; END; Tmp = SELECT s FROM All:s ACCUM @@sizes += (s.@component -> 1); FOREACH group_row IN @@sizes DO @@rows += Row(group_row.componentId, group_row.componentSize); END; PRINT @@rows AS results, iteration AS iterations; }
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.
// This is a fixed-schema, distributed adaptation of TigerGraph's tg_louvain. // Both RATED directions are traversed. Like the Neo4j projection, this is // unweighted: every logical rating edge has weight 1, regardless of its score. CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q30_louvain_communities( INT maximum_iteration = 10 ) FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<FLOAT deltaQ, FLOAT weight, VERTEX community> Move; TYPEDEF TUPLE<INT communityId, INT communitySize> Row; ListAccum<Row> @@rows; SumAccum<FLOAT> @community_degree_sum; ListAccum<VERTEX> @community; SumAccum<FLOAT> @weighted_degree; SumAccum<FLOAT> @community_weighted_degree; MapAccum<VERTEX, SumAccum<FLOAT>> @weight_to_community; MaxAccum<Move> @best_move; ListAccum<VERTEX> @members; SumAccum<INT> @degree; SumAccum<FLOAT> @@total_edge_weight; SumAccum<INT> @@changes; GroupByAccum<INT communityId, SumAccum<INT> communitySize> @@sizes; Users = ML32M_User.*; Movies = ML32M_Movie.*; All = Users UNION Movies; All = SELECT s FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t ACCUM @@total_edge_weight += 0.5, s.@weighted_degree += 1.0, s.@community_weighted_degree += 1.0, s.@degree += 1 POST-ACCUM IF s.@degree > 1 THEN s.@community += s END; // First local-moving pass. All = SELECT t FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t WHERE s.@degree > 1 AND t.@degree > 1 ACCUM t.@best_move += Move( 1.0 + @@total_edge_weight * t.@weighted_degree * (t.@weighted_degree - s.@weighted_degree), -s.@community_weighted_degree, s.@community.get(0)) POST-ACCUM IF t.@best_move.deltaQ > 0 THEN IF -t.@best_move.weight < t.@community_weighted_degree OR (-t.@best_move.weight == t.@community_weighted_degree AND getvid(t) < getvid(t.@best_move.community)) THEN t.@community.clear(), t.@community += t.@best_move.community, t.@community_weighted_degree = -t.@best_move.weight, @@changes += 1 END END; WHILE @@changes > 0 LIMIT maximum_iteration DO @@changes = 0; All = SELECT s FROM All:s WHERE s.@degree > 1 POST-ACCUM s.@community_degree_sum = 0, s.@members.clear(), s.@weight_to_community.clear(); Tmp = SELECT s FROM All:s ACCUM FOREACH c IN s.@community DO CASE WHEN getvid(c) != -1 THEN c.@members += s END END; Tmp = SELECT s FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t WHERE t.@degree > 1 ACCUM s.@weight_to_community += (t.@community.get(0) -> 1.0); Tmp = SELECT s FROM All:s ACCUM FOREACH c IN s.@community DO CASE WHEN getvid(c) != -1 THEN c.@community_degree_sum += s.@weighted_degree END END; Tmp = SELECT s FROM All:s ACCUM FOREACH member IN s.@members DO CASE WHEN getvid(member) != -1 THEN member.@community_degree_sum = s.@community_degree_sum END END; All = SELECT s FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t WHERE t.@degree > 1 ACCUM FLOAT own_weight = 0, IF s.@weight_to_community.containsKey(s) THEN own_weight = s.@weight_to_community.get(s) END, s.@best_move += Move( s.@weight_to_community.get(t.@community.get(0)) - own_weight + (1.0 / @@total_edge_weight) * s.@weighted_degree * (s.@community_degree_sum - t.@community_degree_sum), -t.@community_weighted_degree, t.@community.get(0)) POST-ACCUM IF s.@best_move.deltaQ > 0 THEN IF -s.@best_move.weight < s.@community_weighted_degree OR (-s.@best_move.weight == s.@community_weighted_degree AND getvid(s.@community.get(0)) < getvid(s.@best_move.community)) THEN s.@community.clear(), s.@community += s.@best_move.community, s.@community_weighted_degree = -s.@best_move.weight, @@changes += 1 END END; END; // Degree-one and isolated vertices follow the same rules as tg_louvain. All = Users UNION Movies; Tmp = SELECT s FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t WHERE s.@degree == 1 AND t.@degree != 1 ACCUM s.@community += t.@community.get(0); Tmp = SELECT s FROM All:s -((ML32M_RATED>|ML32M_RATED_REVERSE>):e)- :t WHERE s.@degree == 1 AND t.@degree == 1 ACCUM IF getvid(s) <= getvid(t) THEN s.@community += s ELSE s.@community += t END; Tmp = SELECT s FROM All:s WHERE s.@degree == 0 ACCUM s.@community += s; Tmp = SELECT s FROM All:s WHERE s.@community.size() > 0 ACCUM @@sizes += (getvid(s.@community.get(0)) -> 1); FOREACH group_row IN @@sizes DO @@rows += Row(group_row.communityId, group_row.communitySize); END; PRINT @@rows AS results; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q31_multi_genre_popular_movies() FOR GRAPH ml32m SYNTAX V2 { SetAccum<STRING> @genres; SumAccum<INT> @rating_count; Tmp = SELECT g FROM ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g ACCUM m.@genres += g.name; Tmp = SELECT m FROM ML32M_User:u -(ML32M_RATED>:r)- ML32M_Movie:m ACCUM m.@rating_count += 1; Result = SELECT m FROM ML32M_Movie:m WHERE m.@genres.size() >= 3 AND m.@rating_count >= 50 ORDER BY m.@rating_count DESC, m.movie_id ASC LIMIT 10; PRINT Result[Result.title, Result.@genres, Result.@rating_count]; }
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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q32_top_movies_by_genres_and_tags() FOR GRAPH ml32m SYNTAX V2 { SetAccum<STRING> @genres; SetAccum<STRING> @tags; SumAccum<INT> @total; Tmp = SELECT g FROM ML32M_Movie:m -(ML32M_HAS_GENRE>)- ML32M_Genre:g ACCUM m.@genres += g.name; Tmp = SELECT m FROM ML32M_User:u -(ML32M_TAGGED>:t)- ML32M_Movie:m WHERE t.tag != "" ACCUM m.@tags += t.tag; Movies = SELECT m FROM ML32M_Movie:m POST-ACCUM m.@total = m.@genres.size() + m.@tags.size(); Result = SELECT m FROM Movies:m ORDER BY m.@total DESC, m.movie_id ASC LIMIT 20; PRINT Result[Result.title, Result.@total, Result.@genres, Result.@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.
CREATE OR REPLACE DISTRIBUTED QUERY ml32m_q33_rating_then_tagging() FOR GRAPH ml32m SYNTAX V2 { TYPEDEF TUPLE<INT userId, INT movieId, STRING movieTitle, FLOAT rating, UINT ratingTime> Pair; SetAccum<Pair> @@eligible_pairs; SetAccum<Pair> @@top_pairs; MapAccum<Pair, SetAccum<STRING>> @@results; HeapAccum<Pair>(10, ratingTime DESC, userId ASC, movieId ASC) @@top; Tmp = SELECT m FROM ML32M_User:u -(ML32M_RATED>:r)- ML32M_Movie:m -(<ML32M_TAGGED:t)- ML32M_User:u WHERE r.timestamp < t.timestamp ACCUM @@eligible_pairs += Pair(u.user_id, m.movie_id, m.title, r.rating, r.timestamp); FOREACH pair_row IN @@eligible_pairs DO @@top += pair_row; END; FOREACH pair_row IN @@top DO @@top_pairs += pair_row; END; // Re-traverse only to gather tags for the selected pairs. This avoids any // dependency on another query and keeps distributed nested-accumulator // methods out of the execution plan. Tmp = SELECT m FROM ML32M_User:u -(ML32M_RATED>:r)- ML32M_Movie:m -(<ML32M_TAGGED:t)- ML32M_User:u WHERE r.timestamp < t.timestamp AND @@top_pairs.contains( Pair(u.user_id, m.movie_id, m.title, r.rating, r.timestamp)) ACCUM @@results += ( Pair(u.user_id, m.movie_id, m.title, r.rating, r.timestamp) -> t.tag); PRINT @@results AS results; }
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