← Back to MovieLens 32M Dataset

TigerGraph Implementation

Deployment and Query logic for TigerGraph Community 4.2.0.

Schema Diagram and Graph Statistics

MovieLens 32M TigerGraph Schema

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.

gsql
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.

gsql
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.

python
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.

python
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.

python
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.
gsql
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.

1. Neighborhood
2. Path
3. Pattern
4. Aggregation
5. Analytics
6. Hybrid
7. Others

Q1. Movies Rated by a User

Subcategory: T1.1 One-Hop Neighbors

This query lists the movies directly connected to a selected user by outgoing RATED relationships.

gsql
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:

gql
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

Subcategory: T1.2 k-Hop Neighborhood

This query follows a selected user to rated movies and then to their genres, returning distinct genres within two hops.

gsql
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:

gql
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

Subcategory: T1.3 Exact kth-Hop Neighbors

This query finds users exactly two RATED hops away through movies co-rated with a selected user.

gsql
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:

gql
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

Subcategory: T1.4 Neighborhood with Property Filter

This query filters a selected user's one-hop movie neighborhood to movies with ratings at or above a threshold.

gsql
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:

gql
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

Subcategory: T2.1 Node Filtering (Single Node Paths)

This query filters Movie nodes by title text and movie ID without traversing relationships.

gsql
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:

gql
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

Subcategory: T2.2 Edge Filtering

This query filters RATED relationships by numeric rating and returns their endpoint user and movie.

gsql
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:

gql
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

Subcategory: T2.3 Reachability between Two Nodes

This query tests whether a selected user can reach a selected movie within three undirected RATED hops.

gsql
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:

gql
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

Subcategory: T2.4 Shortest Path

This query finds the shortest undirected RATED path between a selected user and movie.

gsql
// 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:

gql
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

Subcategory: T2.5 All Simple Paths up to Length k

This query enumerates acyclic RATED paths between a selected user and movie up to three hops.

gsql
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:

gql
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

Subcategory: T2.6 top-K Weighted Paths

This query ranks bounded RATED paths by the sum of rating values along each path.

gsql
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:

gql
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

Subcategory: T3.1 Triangle Detection

This query finds three movies where every pair shares at least one genre through HAS_GENRE relationships.

gsql
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:

gql
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

Subcategory: T3.2 Square Detection

This query finds four movies connected in a cycle where each adjacent pair shares a genre.

gsql
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:

gql
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

Subcategory: T3.3 Star Pattern Query

This query finds user-centered stars by counting how many distinct movies each user rated.

gsql
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:

gql
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

Subcategory: T3.4 4-Clique

This query finds four movies where every pair shares at least one genre, forming a clique in the derived movie-genre similarity graph.

gsql
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:

gql
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

Subcategory: T3.5 Two-hop Join Pattern

This query finds pairs of users connected through a commonly rated movie.

gsql
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:

gql
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

Subcategory: T3.6 Line Pattern

This query retrieves users and their comedy movie ratings, returning the top 20 highest-rated user-movie pairs.

gsql
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:

gql
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

Subcategory: T4.1 Ungrouped Node Degree Statistics by Label

This query computes global statistics over the number of RATED edges incident to each movie.

gsql
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:

gql
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

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query identifies the top 10 users who have rated movies spanning the most distinct genres (with a minimum of 3 genres).

gsql
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:

gql
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

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query identifies and lists the top 20 movies that have received the highest count of perfect 5.0 ratings.

gsql
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:

gql
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

Subcategory: T4.4 Grouped Edge Count and Property Statistics

This query aggregates all ratings in the dataset and returns each rating value along with its total occurrence frequency, sorted by rating value.

gsql
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:

gql
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

Subcategory: T4.4 Grouped Edge Count and Property Statistics

This query ranks and returns the top 50 most frequently used movie tags in descending order of their frequency.

gsql
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:

gql
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

Subcategory: T4.5 Path Counts by Length

This query counts bounded RATED paths from a selected user grouped by path length.

gsql
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:

gql
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

Subcategory: T4.6 Grouped Path Counts by Length and Endpoint Type

This query groups bounded paths from a selected user by path length and by genre of reached movie endpoints.

gsql
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:

gql
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"

Subcategory: T4.7 Union and Intersection

This query finds and lists the titles of the first 10 movies that are classified under both the Comedy and Romance genres.

gsql
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:

gql
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

Subcategory: T4.7 Union and Intersection

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.

gsql
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:

gql
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

Subcategory: T5.1 Degree Centrality

This query projects the bipartite User-Movie rating graph and ranks nodes by degree centrality.

gsql
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:

gql
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

Subcategory: T5.2 PageRank

This query runs PageRank over the projected User-Movie rating graph and returns the highest scoring nodes.

gsql
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;
}
Note: Cannot be expressed in standard GQL because the GQL standard covers graph pattern matching and data manipulation only; it defines no graph algorithm procedures such as PageRank.

Q28. Weakly Connected Components of Ratings

Subcategory: T5.3 Connected Components / Weakly Connected Components

This query finds weakly connected components in the undirected User-Movie rating graph.

gsql
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;
}
Note: Cannot be expressed in standard GQL because the GQL standard covers graph pattern matching and data manipulation only; it defines no graph algorithm procedures such as connected components.

Q29. Strongly Connected Components of Directed Ratings

Subcategory: T5.4 Strongly Connected Components

This query treats RATED edges as directed from users to movies and reports strongly connected components.

Skipped: The schema contains no undirected edges, so SCC is not meaningful for this workload. Therefore, Q29 is skipped.

Q30. Louvain Communities in the Rating Graph

Subcategory: T5.5 Community Detection

This query detects communities in the undirected User-Movie rating graph using Louvain.

gsql
// 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;
}
Note: Cannot be expressed in standard GQL because the GQL standard covers graph pattern matching and data manipulation only; it defines no graph algorithm procedures such as community detection (Louvain).

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

Subcategory: T4.2 Grouped Node Degree Statistics by Label; T4.3 Ungrouped Edge Count and Property Statistics

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.

gsql
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:

gql
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

Subcategory: Other

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.

gsql
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:

gql
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)

Subcategory: Other

This query retrieves the top 10 user-movie pairs where the user rated a movie before tagging it, sorted by the rating timestamp.

gsql
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:

gql
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