← Back to Yelp Dataset

TigerGraph Implementation

Deployment and Query logic for TigerGraph Community 4.2.0.

Schema Diagram and Graph Statistics

Yelp TigerGraph Schema

Node Statistics

Node Label Count
Business 150,346
Category 1,311
Review 6,990,247
Tip 908,915
User 1,987,897

Edge Statistics

Relationship Count
(:Tip)-[:TIP_FOR]-(:Business) 908,915
(:User)-[:FRIEND]-(:User) 14,611,748
(:Business)-[:IN_CATEGORY]-(:Category) 668,592
(:Review)-[:REVIEWS]-(:Business) 6,990,247
(:User)-[:TIPPED]-(:Tip) 908,915
(:User)-[:WROTE]-(:Review) 6,990,247

Data Loading

Creating Graph Schema

Use the GSQLShell tool on the TigerGraph to create vertex and edge types.

gsql
CREATE GRAPH yelp_graph()

Vertex Definitions

Then we define Business, User, Review, Tip, and Category vertices.

First we create the Business vertex.

gsql
CREATE VERTEX Business (
    PRIMARY_ID business_id STRING,
    address STRING,
    categories STRING,
    city STRING,
    is_open INT,
    latitude DOUBLE,
    longitude DOUBLE,
    name STRING,
    postal_code STRING,
    review_count INT,
    stars DOUBLE,
    state STRING
) WITH primary_id_as_attribute="true"

Next we create the User vertex.

gsql
CREATE VERTEX User (
    PRIMARY_ID user_id STRING,
    name STRING,
    review_count INT,
    average_stars DOUBLE,
    compliment_cool INT,
    compliment_cute INT,
    compliment_funny INT,
    compliment_hot INT,
    compliment_list INT,
    compliment_more INT,
    compliment_note INT,
    compliment_photos INT,
    compliment_plain INT,
    compliment_profile INT,
    compliment_writer INT,
    cool INT,
    elite STRING,
    fans INT,
    friends STRING,
    funny INT,
    useful INT,
    yelping_since STRING
) WITH primary_id_as_attribute="true"

Next we create the Review vertex.

gsql
CREATE VERTEX Review (
    PRIMARY_ID review_id STRING,
    business_id STRING,
    user_id STRING,
    cool INT,
    funny INT,
    useful INT,
    stars DOUBLE,
    date STRING,
    text STRING
) WITH primary_id_as_attribute="true"

Next we create the Tip vertex.

gsql
CREATE VERTEX Tip (
    PRIMARY_ID tip_id STRING,
    user_id STRING,
    business_id STRING,
    text STRING,
    compliment_count INT,
    date STRING
) WITH primary_id_as_attribute="true"

Next we create the Category vertex.

gsql
CREATE VERTEX Category (
    PRIMARY_ID category STRING
) WITH primary_id_as_attribute="true"

Edge Definitions

Next we define four edge types: TIPPED, TIP_FOR, IN_CATEGORY, FRIEND, and WROTE.

gsql
USE GRAPH yelp

CREATE SCHEMA_CHANGE JOB add_yelp_edges FOR GRAPH yelp {
    ADD DIRECTED EDGE TIPPED (
        FROM User,
        TO Tip
    );

    ADD DIRECTED EDGE TIP_FOR (
        FROM Tip,
        TO Business
    );

    ADD DIRECTED EDGE IN_CATEGORY (
        FROM Business,
        TO Category,
        DISCRIMINATOR(occurrence UINT)
    );

    ADD DIRECTED EDGE FRIEND (
        FROM User,
        TO User
    );

    ADD DIRECTED EDGE WROTE (
        FROM User,
        TO Review
    );

    ADD DIRECTED EDGE REVIEWS (
        FROM Review,
        TO Business
    );
}

RUN SCHEMA_CHANGE JOB add_yelp_edges

Import Jobs

First make sure we are using the correct graph schema yelp_graph.

Looad Business Nodes (0.4s)

gsql
USE GRAPH yelp

CREATE LOADING JOB load_yelp_businesses FOR GRAPH yelp {
    DEFINE FILENAME businesses_file;

    LOAD businesses_file
    TO VERTEX Business VALUES (
        $"business_id",
        $"address",
        $"categories",
        $"city",
        $"is_open",
        $"latitude",
        $"longitude",
        $"name",
        $"postal_code",
        $"review_count",
        $"stars",
        $"state"
    )
    USING JSON_FILE="true";
}

RUN LOADING JOB load_yelp_businesses
USING businesses_file="/datasets/prop_datasets/yelp/yelp_academic_dataset_business.json"

Load User Nodes (8.52s)

gsql
USE GRAPH yelp

CREATE LOADING JOB load_yelp_users FOR GRAPH yelp {
    DEFINE FILENAME users_file;

    LOAD users_file
    TO VERTEX User VALUES (
        $"user_id",
        $"name",
        $"review_count",
        $"average_stars",
        $"compliment_cool",
        $"compliment_cute",
        $"compliment_funny",
        $"compliment_hot",
        $"compliment_list",
        $"compliment_more",
        $"compliment_note",
        $"compliment_photos",
        $"compliment_plain",
        $"compliment_profile",
        $"compliment_writer",
        $"cool",
        $"elite",
        $"fans",
        $"friends",
        $"funny",
        $"useful",
        $"yelping_since"
    )
    USING JSON_FILE="true";
}

RUN LOADING JOB load_yelp_users
USING users_file="/datasets/prop_datasets/yelp/yelp_academic_dataset_user.json"

Load Review nodes, WROTE edges, and REVIEWS edges (16.62s)

There are 33 reviews in the source file referencing User IDs that are not in the User source file. Thus, we used the following python script to filter out those bad review records.

python
import argparse
import json
import os
import sys
from collections import Counter
from pathlib import Path

def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("--users", required=True, type=Path)
    parser.add_argument("--reviews", required=True, type=Path)
    parser.add_argument("--output", required=True, type=Path)
    return parser.parse_args()

def main():
    args = parse_args()
    partial = Path(str(args.output) + ".partial")

    if args.output.exists():
        raise SystemExit(f"Refusing to overwrite existing output: {args.output}")
    if partial.exists():
        raise SystemExit(
            f"Partial output already exists: {partial}\n"
            "Move or remove it after checking whether an earlier run was interrupted."
        )

    user_ids = set()
    with args.users.open("r", encoding="utf-8") as source:
        for line_number, line in enumerate(source, 1):
            try:
                user_ids.add(json.loads(line)["user_id"])
            except (json.JSONDecodeError, KeyError) as error:
                raise SystemExit(
                    f"Invalid user record at line {line_number}: {error}"
                ) from error

    print(f"Loaded {len(user_ids):,} valid user IDs", file=sys.stderr)

    total = 0
    kept = 0
    missing_users = Counter()

    with args.reviews.open("r", encoding="utf-8") as source, partial.open(
        "x", encoding="utf-8"
    ) as destination:
        for line_number, line in enumerate(source, 1):
            try:
                review = json.loads(line)
                user_id = review["user_id"]
            except (json.JSONDecodeError, KeyError) as error:
                raise SystemExit(
                    f"Invalid review record at line {line_number}: {error}"
                ) from error

            total += 1
            if user_id in user_ids:
                destination.write(line)
                kept += 1
            else:
                missing_users[user_id] += 1

            if total % 1_000_000 == 0:
                print(
                    f"Processed {total:,}; kept {kept:,}; skipped {total - kept:,}",
                    file=sys.stderr,
                )

        destination.flush()
        os.fsync(destination.fileno())

    os.replace(partial, args.output)

    print(f"Total reviews:   {total:,}")
    print(f"Kept reviews:    {kept:,}")
    print(f"Skipped reviews: {total - kept:,}")
    print(f"Missing users:   {len(missing_users):,}")
    for user_id, count in sorted(missing_users.items()):
        print(f"  {user_id}: {count}")

if __name__ == "__main__":
    main()

Then run the following command in your terminal to create a filtered Review file.

bash
python3 /datasets/prop_datasets/yelp/tigergraph_filter_yelp_reviews.py \
  --users /datasets/prop_datasets/yelp/yelp_academic_dataset_user.json \
  --reviews /datasets/prop_datasets/yelp/yelp_academic_dataset_review.json \
  --output /datasets/prop_datasets/yelp/yelp_reviews_tg_filtered.json

Finally, create import job for Review nodes, WROTE edges, and REVIEWS edges.

gsql
USE GRAPH yelp

CREATE LOADING JOB load_yelp_reviews FOR GRAPH yelp {
    DEFINE FILENAME reviews_file;

    LOAD reviews_file
        TO VERTEX Review VALUES (
            $"review_id",
            $"business_id",
            $"user_id",
            $"cool",
            $"funny",
            $"useful",
            $"stars",
            $"date",
            $"text"
        ),
        TO EDGE WROTE VALUES (
            $"user_id",
            $"review_id"
        ),
        TO EDGE REVIEWS VALUES (
            $"review_id",
            $"business_id"
        )
    USING JSON_FILE="true";
}

RUN LOADING JOB load_yelp_reviews
USING reviews_file="/datasets/prop_datasets/yelp/yelp_reviews_tg_filtered.json"

Load Tip nodes, TIPPED edges, and TIP_FOR edges (1.84s)

gsql
USE GRAPH yelp

CREATE LOADING JOB load_yelp_tips FOR GRAPH yelp {
    DEFINE FILENAME tips_file;

    LOAD tips_file
        TO VERTEX Tip VALUES (
            $"tip_id",
            $"user_id",
            $"business_id",
            $"text",
            $"compliment_count",
            $"date"
        ),
        TO EDGE TIPPED VALUES (
            $"user_id",
            $"tip_id"
        ),
        TO EDGE TIP_FOR VALUES (
            $"tip_id",
            $"business_id"
        )
    USING JSON_FILE="true";
}

RUN LOADING JOB load_yelp_tips
USING tips_file="m1:/datasets/prop_datasets/yelp/yelp_tip.json"

Load Category nodes (0.9s)

gsql
USE GRAPH yelp

CREATE LOADING JOB load_yelp_categories FOR GRAPH yelp {
    DEFINE FILENAME category_file;

    LOAD category_file
    TO VERTEX Category VALUES ($0)
    USING HEADER="true", SEPARATOR=",";
}

RUN LOADING JOB load_yelp_categories
USING category_file="m1:/datasets/prop_datasets/yelp/yelp_categories.csv"

Load IN_CATEGORY edges (0.78s)

A standard TigerGraph edge is uniquely identified by its edge type, source, and target. Loading the same pair twice overwrites/collapses it, unlike in Neo4j this would just create a separate edge. Thus, we need an extra "occurence" attribute on the edge for some Business that has two categories with the same name in the original data. We created the file category_edges_with_occurrence.csv and add a new occurence column for edge records with the same origin and destination.

gsql
CREATE LOADING JOB load_yelp_category_edges_occurrence FOR GRAPH yelp {
    DEFINE FILENAME category_edge_file;

    LOAD category_edge_file
    TO EDGE IN_CATEGORY VALUES (
        $0,  // business_id
        $1,  // category
        $2   // occurrence discriminator
    )
    USING HEADER="true",
          SEPARATOR=",",
          VERTEX_MUST_EXIST="true";
}

RUN LOADING JOB load_yelp_category_edges_occurrence
USING category_edge_file="m1:/datasets/prop_datasets/yelp/category_edges_with_occurrence.csv"

Load FRIEND edges (3.75s)

gsql
USE GRAPH yelp

CREATE LOADING JOB load_yelp_friends FOR GRAPH yelp {
    DEFINE FILENAME friend_file;

    LOAD friend_file
    TO EDGE FRIEND VALUES (
        $0,  // source User ID
        $1   // destination User ID
    )
    USING HEADER="true",
          SEPARATOR=",",
          QUOTE="double",
          VERTEX_MUST_EXIST="true";
}

RUN LOADING JOB load_yelp_friends
USING friend_file="m1:/datasets/prop_datasets/yelp/friend_edges.csv"

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. Find the friends of all "John"s

Subcategory: T1.1 One-Hop Neighbors

This query retrieves the name and user ID of up to 20 friends of users named "John".

gsql
CREATE QUERY yelp_q01() FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING name, STRING user_id> Row;
  HeapAccum<Row>(20, name ASC, user_id ASC) @@rows;
  johns = {User.*};
  friends = SELECT f
            FROM johns:j -(FRIEND>)- User:f
            WHERE j.name == "John"
            ACCUM @@rows += Row(f.name, f.user_id);
  PRINT @@rows AS results;
}

Equivalent query in GQL standard:

gql
MATCH (:User {name: "John"})-[:FRIEND]->(friend)
RETURN friend.name, friend.user_id
LIMIT 20

Q2. Find all users within two friend hops of a given user

Subcategory: T1.2 k-Hop Neighborhood

This query finds all distinct users reachable within one or two FRIEND hops from a starting user.

gsql
CREATE QUERY yelp_q02(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  OrAccum @seen;
  start = SELECT s FROM User:s WHERE s.user_id == start_id
          POST-ACCUM s.@seen += TRUE;
  hop1 = SELECT t FROM start:s -(FRIEND>)- User:t
         WHERE NOT t.@seen POST-ACCUM t.@seen += TRUE;
  hop2 = SELECT t FROM hop1:s -(FRIEND>)- User:t
         WHERE NOT t.@seen POST-ACCUM t.@seen += TRUE;
  neighbors = hop1 UNION hop2;
  answer = SELECT n FROM neighbors:n
           ORDER BY n.name ASC, n.user_id ASC LIMIT 100;
  PRINT answer[answer.name AS name, answer.user_id AS user_id];
}

Equivalent query in GQL standard:

gql
MATCH p = (start:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{1,2}(neighbor:User)
WHERE neighbor != start
RETURN DISTINCT neighbor.name AS name, neighbor.user_id AS user_id
ORDER BY name, user_id
LIMIT 100

Q3. Get 3-hop neighbors of a given user

Subcategory: T1.3 Exact kth-Hop Neighbors

This query identifies the unique names and user IDs of all neighbors exactly three friend-connections away from a given user.

gsql
CREATE QUERY yelp_q03(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  start = SELECT s FROM User:s WHERE s.user_id == start_id;
  hop3 = SELECT n
         FROM start:s -(FRIEND>)- User:x
                      -(FRIEND>)- User:y
                      -(FRIEND>)- User:n
         WHERE n.user_id != start_id;
  PRINT hop3[hop3.name AS name, hop3.user_id AS user_id];
}

Equivalent query in GQL standard:

gql
MATCH (start:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{3}(neighbor)
WHERE neighbor.user_id != "Oi1qbcz2m2SnwUeztGYcnQ"
RETURN DISTINCT neighbor.name, neighbor.user_id

Q4. Find high-review-count users within two friend hops

Subcategory: T1.4 Neighborhood with Property Filter

This query finds distinct users within one or two FRIEND hops of a starting user, filtered to users with at least 100 reviews and an average rating of 4.0 or higher.

gsql
CREATE QUERY yelp_q04(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  OrAccum @seen;
  start = SELECT s FROM User:s WHERE s.user_id == start_id
          POST-ACCUM s.@seen += TRUE;
  hop1 = SELECT t FROM start:s -(FRIEND>)- User:t
         WHERE NOT t.@seen POST-ACCUM t.@seen += TRUE;
  hop2 = SELECT t FROM hop1:s -(FRIEND>)- User:t
         WHERE NOT t.@seen POST-ACCUM t.@seen += TRUE;
  candidates = hop1 UNION hop2;
  answer = SELECT n FROM candidates:n
           WHERE n.review_count >= 100 AND n.average_stars >= 4.0
           ORDER BY n.review_count DESC, n.average_stars DESC LIMIT 100;
  PRINT answer[answer.name AS name, answer.user_id AS user_id,
               answer.review_count AS review_count,
               answer.average_stars AS average_stars];
}

Equivalent query in GQL standard:

gql
MATCH p = (start:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{1,2}(neighbor:User)
WHERE neighbor != start
  AND neighbor.review_count >= 100
  AND neighbor.average_stars >= 4.0
RETURN DISTINCT neighbor.name AS name,
       neighbor.user_id AS user_id,
       neighbor.review_count AS review_count,
       neighbor.average_stars AS average_stars
ORDER BY review_count DESC, average_stars DESC
LIMIT 100

Q5. Find open, highly rated businesses in Pennsylvania

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

This query filters individual Business nodes by state, open status, rating, and review count without traversing edges.

gsql
CREATE QUERY yelp_q05() FOR GRAPH yelp SYNTAX v2 {
  answer = SELECT b FROM Business:b
           WHERE b.state == "PA" AND b.is_open == 1
             AND b.stars >= 4.5 AND b.review_count >= 100
           ORDER BY b.stars DESC, b.review_count DESC LIMIT 25;
  PRINT answer[answer.name AS name, answer.city AS city,
               answer.stars AS rating, answer.review_count AS review_count];
}

Equivalent query in GQL standard:

gql
MATCH (b:Business)
WHERE b.state = "PA"
  AND b.is_open = 1
  AND b.rating >= 4.5
  AND b.review_count >= 100
RETURN b.name, b.city, b.rating, b.review_count
ORDER BY b.rating DESC, b.review_count DESC
LIMIT 25

Q6. Find useful five-star review edges

Subcategory: T2.2 Edge Filtering

The Yelp Neo4j schema stores review attributes on Review nodes that mediate User-to-Business edges, so this query filters those edge records by rating and usefulness.

gsql
CREATE QUERY yelp_q06() FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING user_name, STRING review_id, DOUBLE stars,
                INT useful_votes, STRING business_name, STRING city> Row;
  HeapAccum<Row>(20, useful_votes DESC) @@rows;
  users = {User.*};
  reviews = SELECT r
            FROM users:u -(WROTE>)- Review:r -(REVIEWS>)- Business:b
            WHERE r.stars == 5 AND r.useful >= 10
            ACCUM @@rows += Row(u.name, r.review_id, r.stars, r.useful,
                                b.name, b.city);
  PRINT @@rows AS results;
}

Equivalent query in GQL standard:

gql
MATCH (u:User)-[:WROTE]->(r:Review)-[:REVIEWS]->(b:Business)
WHERE r.stars = 5
  AND r.useful >= 10
RETURN u.name AS user_name,
       r.id AS review_id,
       r.stars AS stars,
       r.useful AS useful_votes,
       b.name AS business_name,
       b.city AS city
ORDER BY useful_votes DESC
LIMIT 20

Q7. Check whether Steven can reach Niki within four friend hops

Subcategory: T2.3 Reachability between Two Nodes

This query tests whether a bounded friend path exists between two User nodes.

gsql
CREATE QUERY yelp_q07(
  STRING source_id = "Oi1qbcz2m2SnwUeztGYcnQ",
  STRING target_id = "WwulXySQN8t2hwqH_yWurA")
FOR GRAPH yelp SYNTAX v2 {
  OrAccum @seen;
  OrAccum @@reachable;
  source = SELECT s FROM User:s WHERE s.user_id == source_id
           POST-ACCUM s.@seen += TRUE;
  frontier = source;
  INT hop = 0;
  WHILE frontier.size() > 0 AND hop < 4 AND NOT @@reachable DO
    hop = hop + 1;
    frontier = SELECT t FROM frontier:s -(FRIEND>)- User:t
               WHERE NOT t.@seen
               ACCUM @@reachable += (t.user_id == target_id)
               POST-ACCUM t.@seen += TRUE;
  END;
  PRINT @@reachable AS reachable;
}

Equivalent query in GQL standard:

gql
MATCH p = ANY (source:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{1,4}(target:User {user_id: "WwulXySQN8t2hwqH_yWurA"})
RETURN true AS reachable

Q8. Find the shortest friend path between Steven and Niki

Subcategory: T2.4 Shortest Path

This query returns the shortest bounded path connecting two users in the friend graph.

gsql
CREATE QUERY yelp_q08(
  STRING source_id = "Oi1qbcz2m2SnwUeztGYcnQ",
  STRING target_id = "WwulXySQN8t2hwqH_yWurA")
FOR GRAPH yelp SYNTAX v2 {
  OrAccum @seen;
  MinAccum<INT> @distance = 999999;
  MaxAccum<STRING> @path;
  OrAccum @@found;
  source = SELECT s FROM User:s WHERE s.user_id == source_id
           POST-ACCUM s.@seen += TRUE, s.@distance += 0, s.@path += s.name;
  frontier = source;
  INT hop = 0;
  WHILE frontier.size() > 0 AND hop < 6 AND NOT @@found DO
    hop = hop + 1;
    frontier = SELECT t FROM frontier:s -(FRIEND>)- User:t
               WHERE NOT t.@seen
               ACCUM t.@distance += hop,
                     t.@path += s.@path + " -> " + t.name,
                     @@found += (t.user_id == target_id)
               POST-ACCUM t.@seen += TRUE;
  END;
  target_user = SELECT t FROM User:t
                WHERE t.user_id == target_id AND t.@distance < 999999;
  PRINT target_user[target_user.@path AS user_path,
                    target_user.@distance AS hop_count];
}

Equivalent query in GQL standard:

gql
MATCH p = SHORTEST (:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{1,6}(:User {user_id: "WwulXySQN8t2hwqH_yWurA"})
RETURN p

Q9. Enumerate simple friend paths between Steven and Niki up to length three

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

This query materializes acyclic friend paths up to three hops between two users.

gsql
CREATE QUERY yelp_q09(
  STRING source_id = "Oi1qbcz2m2SnwUeztGYcnQ",
  STRING target_id = "WwulXySQN8t2hwqH_yWurA")
FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<INT hop_count, STRING user_ids> PathRow;
  HeapAccum<PathRow>(20, hop_count ASC, user_ids ASC) @@paths;
  source = SELECT s FROM User:s WHERE s.user_id == source_id;
  p1 = SELECT t FROM source:s -(FRIEND>)- User:t
       WHERE t.user_id == target_id
       ACCUM @@paths += PathRow(1, s.user_id + " -> " + t.user_id);
  p2 = SELECT t FROM source:s -(FRIEND>)- User:a -(FRIEND>)- User:t
       WHERE t.user_id == target_id AND a.user_id != source_id
         AND a.user_id != target_id
       ACCUM @@paths += PathRow(2, s.user_id + " -> " + a.user_id
                                  + " -> " + t.user_id);
  p3 = SELECT t FROM source:s -(FRIEND>)- User:a
                                -(FRIEND>)- User:b
                                -(FRIEND>)- User:t
       WHERE t.user_id == target_id AND a.user_id != b.user_id
         AND a.user_id != source_id AND b.user_id != source_id
         AND a.user_id != target_id AND b.user_id != target_id
       ACCUM @@paths += PathRow(3, s.user_id + " -> " + a.user_id
                                  + " -> " + b.user_id + " -> " + t.user_id);
  PRINT @@paths AS paths;
}

Equivalent query in GQL standard:

gql
MATCH p = ACYCLIC (:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{1,3}(:User {user_id: "WwulXySQN8t2hwqH_yWurA"})
RETURN p
LIMIT 20

Q10. Find friend triangles around Steven

Subcategory: T3.1 Triangle Detection

This query finds three-user cycles in the friend graph anchored at Steven.

gsql
CREATE QUERY yelp_q10(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING friend_one, STRING friend_two> Row;
  OrAccum @is_start_friend;
  SetAccum<Row> @@distinct_rows;
  HeapAccum<Row>(25, friend_one ASC, friend_two ASC) @@rows;
  start = SELECT a FROM User:a WHERE a.user_id == start_id;
  friends = SELECT b FROM start:a -(FRIEND>)- User:b
            POST-ACCUM b.@is_start_friend += TRUE;
  triangles = SELECT c FROM friends:b -(FRIEND>)- User:c
              WHERE c.@is_start_friend AND b.user_id != c.user_id
              ACCUM @@distinct_rows += Row(b.name, c.name);
  FOREACH row IN @@distinct_rows DO
    @@rows += row;
  END;
  PRINT @@rows AS results;
}

Equivalent query in GQL standard:

gql
MATCH (a:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-(b:User),
      (b)-[:FRIEND]-(c:User),
      (c)-[:FRIEND]-(a)
WHERE a <> b AND b <> c AND a <> c
RETURN DISTINCT b.name AS friend_one, c.name AS friend_two
LIMIT 25

Q11. Find friend squares around Steven

Subcategory: T3.2 Square Detection

This query searches for four-user cycles that include Steven.

gsql
CREATE QUERY yelp_q11(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING first_friend, STRING bridge_friend,
                STRING second_friend> Row;
  OrAccum @is_start_friend;
  HeapAccum<Row>(25, first_friend ASC, bridge_friend ASC,
                 second_friend ASC) @@rows;
  start = SELECT a FROM User:a WHERE a.user_id == start_id;
  friends = SELECT b FROM start:a -(FRIEND>)- User:b
            POST-ACCUM b.@is_start_friend += TRUE;
  squares = SELECT d FROM friends:b -(FRIEND>)- User:c
                                  -(FRIEND>)- User:d
            WHERE d.@is_start_friend AND b.user_id != c.user_id
              AND b.user_id != d.user_id AND c.user_id != d.user_id
            ACCUM @@rows += Row(b.name, c.name, d.name);
  PRINT @@rows AS results;
}

Equivalent query in GQL standard:

gql
MATCH (a:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-(b:User),
      (b)-[:FRIEND]-(c:User),
      (c)-[:FRIEND]-(d:User),
      (d)-[:FRIEND]-(a)
WHERE a <> b AND a <> c AND a <> d
  AND b <> c AND b <> d AND c <> d
RETURN DISTINCT b.name AS first_friend, c.name AS bridge_friend, d.name AS second_friend
LIMIT 25

Q12. Find high-review-count friend stars

Subcategory: T3.3 Star Pattern Query

This query finds users who are connected to many prolific reviewer friends.

gsql
CREATE QUERY yelp_q12() FOR GRAPH yelp SYNTAX v2 {
  SumAccum<INT> @prolific_friend_count;
  users = {User.*};
  centers = SELECT c FROM users:c -(FRIEND>)- User:f
            WHERE f.review_count >= 100
            ACCUM c.@prolific_friend_count += 1
            ORDER BY c.@prolific_friend_count DESC LIMIT 25;
  PRINT centers[centers.name AS center_name,
                centers.user_id AS center_id,
                centers.@prolific_friend_count AS prolific_friend_count];
}

Equivalent query in GQL standard:

gql
MATCH (center:User)-[:FRIEND]->(friend:User)
WHERE friend.review_count >= 100
RETURN center.name AS center_name,
       center.user_id AS center_id,
       count(friend) AS prolific_friend_count
ORDER BY prolific_friend_count DESC
LIMIT 25

Q13. Find four-user cliques around Steven

Subcategory: T3.4 4-Clique

This query finds fully connected groups of four users containing Steven.

gsql
CREATE QUERY yelp_q13(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING user_b, STRING user_c, STRING user_d> Row;
  OrAccum @is_start_friend;
  SetAccum<STRING> @friend_ids;
  HeapAccum<Row>(10, user_b ASC, user_c ASC, user_d ASC) @@rows;
  start = SELECT a FROM User:a WHERE a.user_id == start_id;
  friends = SELECT b FROM start:a -(FRIEND>)- User:b
            POST-ACCUM b.@is_start_friend += TRUE;
  adjacency = SELECT x FROM friends:x -(FRIEND>)- User:y
              ACCUM x.@friend_ids += y.user_id;
  cliques = SELECT d FROM friends:b -(FRIEND>)- User:c
                                  -(FRIEND>)- User:d
            WHERE c.@is_start_friend AND d.@is_start_friend
              AND b.user_id < c.user_id AND c.user_id < d.user_id
              AND d.user_id IN b.@friend_ids
            ACCUM @@rows += Row(b.name, c.name, d.name);
  PRINT @@rows AS results;
}

Equivalent query in GQL standard:

gql
MATCH (a:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-(b:User),
      (a)-[:FRIEND]-(c:User),
      (a)-[:FRIEND]-(d:User),
      (b)-[:FRIEND]-(c),
      (b)-[:FRIEND]-(d),
      (c)-[:FRIEND]-(d)
WHERE b.user_id < c.user_id AND c.user_id < d.user_id
RETURN b.name AS user_b, c.name AS user_c, d.name AS user_d
LIMIT 10

Q14. Find mutual friends between Steven and Niki

Subcategory: T3.5 Two-hop Join Pattern

This query finds the names and user IDs of all mutual friends between two specific users, Steven and Niki.

gsql
CREATE QUERY yelp_q14(
  STRING first_id = "Oi1qbcz2m2SnwUeztGYcnQ",
  STRING second_id = "WwulXySQN8t2hwqH_yWurA")
FOR GRAPH yelp SYNTAX v2 {
  OrAccum @friend_of_first;
  first = SELECT a FROM User:a WHERE a.user_id == first_id;
  first_friends = SELECT m FROM first:a -(FRIEND>)- User:m
                  POST-ACCUM m.@friend_of_first += TRUE;
  second = SELECT b FROM User:b WHERE b.user_id == second_id;
  mutual = SELECT m FROM second:b -(FRIEND>)- User:m
           WHERE m.@friend_of_first;
  PRINT mutual[mutual.name AS name, mutual.user_id AS user_id];
}

Equivalent query in GQL standard:

gql
MATCH (a:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]->(mutual)<-[:FRIEND]-(b:User {user_id: "WwulXySQN8t2hwqH_yWurA"})
RETURN DISTINCT mutual.name, mutual.user_id

Q15. For a given user's friends, find their reviewed businesses along with locations

Subcategory: T3.6 Line Pattern

This query lists the businesses, including their physical addresses and coordinates, that have been reviewed by a given user's friends.

gsql
CREATE QUERY yelp_q15(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING friend_name, STRING business_name, STRING address,
                STRING city, STRING state, DOUBLE latitude,
                DOUBLE longitude> Row;
  SetAccum<Row> @@rows;
  start = SELECT u FROM User:u WHERE u.user_id == start_id;
  businesses = SELECT b
               FROM start:u -(FRIEND>)- User:f
                            -(WROTE>)- Review:r
                            -(REVIEWS>)- Business:b
               ACCUM @@rows += Row(f.name, b.name, b.address, b.city,
                                   b.state, b.latitude, b.longitude);
  PRINT @@rows AS results;
}

Equivalent query in GQL standard:

gql
MATCH (u:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]->(friend:User)-[:WROTE]->(r:Review)-[:REVIEWS]->(b:Business)
RETURN DISTINCT friend.name AS friend_name,
b.name AS business_name,
b.address, b.city, b.state,
b.latitude, b.longitude
ORDER BY friend_name, business_name

Q16. Count the friends of all "John"s

Subcategory: T4.1 Ungrouped Node Degree Statistics by Label

This query calculates the total number of unique friends belonging to all users named "John".

gsql
CREATE QUERY yelp_q16() FOR GRAPH yelp SYNTAX v2 {
  SetAccum<VERTEX<User>> @@friends;
  johns = {User.*};
  found = SELECT f FROM johns:j -(FRIEND>)- User:f
          WHERE j.name == "John" ACCUM @@friends += f;
  PRINT @@friends.size() AS total_friends;
}

Equivalent query in GQL standard:

gql
MATCH (:User {name: "John"})-[:FRIEND]->(friend)
RETURN count(DISTINCT friend) AS total_friends

Q17. Find businesses with only negative reviews

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query finds businesses where every review rating is less than 3 stars, ordered by the volume of reviews.

gsql
CREATE QUERY yelp_q17() FOR GRAPH yelp SYNTAX v2 {
  MaxAccum<DOUBLE> @max_stars;
  SumAccum<INT> @review_count;
  ListAccum<DOUBLE> @stars;
  reviews = {Review.*};
  businesses = SELECT b FROM reviews:r -(REVIEWS>)- Business:b
               ACCUM b.@max_stars += r.stars,
                     b.@review_count += 1,
                     b.@stars += r.stars;
  answer = SELECT b FROM businesses:b
           WHERE b.@review_count > 0 AND b.@max_stars < 3
           ORDER BY b.@review_count DESC;
  PRINT answer[answer.name AS name, answer.@stars AS stars];
}

Equivalent query in GQL standard:

gql
MATCH (b:Business)<-[:REVIEWS]-(r:Review)
RETURN b.name, collect(r.stars) AS stars
  WHERE ALL(star IN stars WHERE star < 3)
ORDER BY size(stars) DESC

Q18. Summarize review-edge counts and ratings

Subcategory: T4.3 Ungrouped Edge Count and Property Statistics (Optional)

The Yelp schema stores review rating on Review nodes rather than relationship properties, so this query summarizes the review-mediated User-to-Business edges.

gsql
CREATE QUERY yelp_q18() FOR GRAPH yelp SYNTAX v2 {
  SumAccum<INT> @@review_count;
  AvgAccum @@avg_stars;
  MinAccum<DOUBLE> @@min_stars;
  MaxAccum<DOUBLE> @@max_stars;
  users = {User.*};
  reviews = SELECT r
            FROM users:u -(WROTE>)- Review:r -(REVIEWS>)- Business:b
            ACCUM @@review_count += 1, @@avg_stars += r.stars,
                  @@min_stars += r.stars, @@max_stars += r.stars;
  PRINT @@review_count AS review_edge_count,
        @@avg_stars AS avg_review_stars,
        @@min_stars AS min_review_stars,
        @@max_stars AS max_review_stars;
}

Equivalent query in GQL standard:

gql
MATCH (:User)-[:WROTE]->(r:Review)-[:REVIEWS]->(:Business)
RETURN count(r) AS review_edge_count,
       avg(r.stars) AS avg_review_stars,
       min(r.stars) AS min_review_stars,
       max(r.stars) AS max_review_stars

Q19. Summarize review ratings by business state

Subcategory: T4.4 Grouped Edge Count and Property Statistics (Optional)

This query groups review-mediated User-to-Business edges by the destination business state and summarizes ratings.

gsql
CREATE QUERY yelp_q19() FOR GRAPH yelp SYNTAX v2 {
  SELECT b.state AS state, COUNT(*) AS review_edge_count,
         AVG(r.stars) AS avg_review_stars,
         MIN(r.stars) AS min_review_stars,
         MAX(r.stars) AS max_review_stars
  INTO state_summary
  FROM User:u -(WROTE>)- Review:r -(REVIEWS>)- Business:b
  GROUP BY b.state
  ORDER BY review_edge_count DESC
  LIMIT 25;
  PRINT state_summary;
}

Equivalent query in GQL standard:

gql
MATCH (:User)-[:WROTE]->(r:Review)-[:REVIEWS]->(b:Business)
RETURN b.state AS state,
       count(r) AS review_edge_count,
       avg(r.stars) AS avg_review_stars,
       min(r.stars) AS min_review_stars,
       max(r.stars) AS max_review_stars
ORDER BY review_edge_count DESC
LIMIT 25

Q20. Count Steven's friend paths by length

Subcategory: T4.5 Path Counts by Length

This query counts all bounded friend paths from Steven and groups them by hop length.

gsql
CREATE QUERY yelp_q20(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  SetAccum<VERTEX<User>> @@n1, @@n2, @@n3;
  SumAccum<INT> @@p1, @@p2, @@p3;
  start = SELECT s FROM User:s WHERE s.user_id == start_id;
  h1 = SELECT a FROM start:s -(FRIEND>)- User:a
       ACCUM @@p1 += 1, @@n1 += a;
  h2 = SELECT b FROM start:s -(FRIEND>)- User:a -(FRIEND>)- User:b
       ACCUM @@p2 += 1, @@n2 += b;
  h3 = SELECT c FROM start:s -(FRIEND>)- User:a
                               -(FRIEND>)- User:b
                               -(FRIEND>)- User:c
       ACCUM @@p3 += 1, @@n3 += c;
  PRINT 1 AS path_length, @@p1 AS path_count,
        @@n1.size() AS distinct_neighbor_count;
  PRINT 2 AS path_length, @@p2 AS path_count,
        @@n2.size() AS distinct_neighbor_count;
  PRINT 3 AS path_length, @@p3 AS path_count,
        @@n3.size() AS distinct_neighbor_count;
}

Equivalent query in GQL standard:

gql
MATCH p = (:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{1,3}(neighbor:User)
RETURN length(p) AS path_length,
       count(p) AS path_count,
       count(DISTINCT neighbor) AS distinct_neighbor_count
ORDER BY path_length ASC

Q21. Count Steven's friend paths by length and rating bucket

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

This query extends Q20 by grouping endpoint users into average-star buckets.

gsql
CREATE QUERY yelp_q21(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  MapAccum<STRING, SumAccum<INT>> @@path_counts;
  MapAccum<STRING, SetAccum<VERTEX<User>>> @@neighbors;
  start = SELECT s FROM User:s WHERE s.user_id == start_id;
  h1 = SELECT n FROM start:s -(FRIEND>)- User:n
       ACCUM STRING bucket = CASE
                               WHEN n.average_stars >= 4.0 THEN "high"
                               WHEN n.average_stars >= 3.0 THEN "medium"
                               ELSE "low"
                             END,
             @@path_counts += ("1:" + bucket -> 1),
             @@neighbors += ("1:" + bucket -> n);
  h2 = SELECT n FROM start:s -(FRIEND>)- User:a -(FRIEND>)- User:n
       ACCUM STRING bucket = CASE
                               WHEN n.average_stars >= 4.0 THEN "high"
                               WHEN n.average_stars >= 3.0 THEN "medium"
                               ELSE "low"
                             END,
             @@path_counts += ("2:" + bucket -> 1),
             @@neighbors += ("2:" + bucket -> n);
  h3 = SELECT n FROM start:s -(FRIEND>)- User:a
                               -(FRIEND>)- User:b
                               -(FRIEND>)- User:n
       ACCUM STRING bucket = CASE
                               WHEN n.average_stars >= 4.0 THEN "high"
                               WHEN n.average_stars >= 3.0 THEN "medium"
                               ELSE "low"
                             END,
             @@path_counts += ("3:" + bucket -> 1),
             @@neighbors += ("3:" + bucket -> n);
  PRINT @@path_counts AS path_counts_by_length_and_bucket,
        @@neighbors AS distinct_neighbors_by_length_and_bucket;
}

Equivalent query in GQL standard:

gql
MATCH p = (:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{1,3}(neighbor:User)
WITH p, neighbor,
     CASE
       WHEN neighbor.average_stars >= 4.0 THEN "high"
       WHEN neighbor.average_stars >= 3.0 THEN "medium"
       ELSE "low"
     END AS rating_bucket
RETURN length(p) AS path_length,
       rating_bucket,
       count(p) AS path_count,
       count(DISTINCT neighbor) AS distinct_neighbor_count
ORDER BY path_length ASC, path_count DESC
LIMIT 25

Q22. Find the total number of reviews and tips for each business

Subcategory: T4.7 Union and Intersection

This query calculates the combined count of reviews and tips for every business, sorting them in descending order of total user interactions.

gsql
CREATE QUERY yelp_q22() FOR GRAPH yelp SYNTAX v2 {
  SumAccum<INT> @num_reviews, @num_tips;
  reviews = {Review.*};
  rb = SELECT b FROM reviews:r -(REVIEWS>)- Business:b
       ACCUM b.@num_reviews += 1;
  tips = {Tip.*};
  tb = SELECT b FROM tips:t -(TIP_FOR>)- Business:b
       ACCUM b.@num_tips += 1;
  businesses = {Business.*};
  answer = SELECT b FROM businesses:b
           ORDER BY (b.@num_reviews + b.@num_tips) DESC;
  PRINT answer[answer.name AS business_name,
               answer.@num_reviews AS num_reviews,
               answer.@num_tips AS num_tips,
               (answer.@num_reviews + answer.@num_tips) AS total];
}

Equivalent query in GQL standard:

gql
MATCH (b:Business)
OPTIONAL MATCH (b)<-[:REVIEWS]-(r:Review)
OPTIONAL MATCH (b)<-[:TIPPED]-(t:Tip)
RETURN b.name AS business_name,
       count(DISTINCT r) AS num_reviews,
       count(DISTINCT t) AS num_tips,
       (count(DISTINCT r) + count(DISTINCT t)) AS total
ORDER BY total DESC

Q23. Find the businesses with both tips and reviews, and return their counts

Subcategory: T4.7 Union and Intersection

This query finds businesses that have received both reviews and tips, returning the counts of each along with their combined total.

gsql
CREATE QUERY yelp_q23() FOR GRAPH yelp SYNTAX v2 {
  SumAccum<INT> @num_reviews, @num_tips;
  reviews = {Review.*};
  rb = SELECT b FROM reviews:r -(REVIEWS>)- Business:b
       ACCUM b.@num_reviews += 1;
  tips = {Tip.*};
  tb = SELECT b FROM tips:t -(TIP_FOR>)- Business:b
       ACCUM b.@num_tips += 1;
  businesses = {Business.*};
  answer = SELECT b FROM businesses:b
           WHERE b.@num_reviews > 0 AND b.@num_tips > 0
           ORDER BY (b.@num_reviews + b.@num_tips) DESC;
  PRINT answer[answer.name AS business_name,
               answer.@num_reviews AS num_reviews,
               answer.@num_tips AS num_tips,
               (answer.@num_reviews + answer.@num_tips) AS total];
}

Equivalent query in GQL standard:

gql
MATCH (b:Business)<-[:REVIEWS]-(r:Review)
MATCH (b)<-[:TIPPED]-(t:Tip)
RETURN b.name AS business_name,
       count(DISTINCT r) AS num_reviews,
       count(DISTINCT t) AS num_tips,
       (count(DISTINCT r) + count(DISTINCT t)) AS total
ORDER BY total DESC

Q24. Print all the text reviews and tips for a specific restaurant

Subcategory: T4.7 Union and Intersection

This query compiles all text content from reviews and tips written for a specific restaurant named "Acme Oyster House".

gsql
CREATE QUERY yelp_q24(STRING business_name = "Acme Oyster House")
FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING business_id, STRING text, STRING comment_type> Row;
  // Cypher UNION removes duplicate result rows. SetAccum preserves that
  // behavior when identical review/tip tuples are encountered.
  SetAccum<Row> @@comments;
  businesses = SELECT b FROM Business:b WHERE b.name == business_name;
  reviews = SELECT r FROM businesses:b -(<REVIEWS)- Review:r
            ACCUM @@comments += Row(b.business_id, r.text, "review");
  tips = SELECT t FROM businesses:b -(<TIP_FOR)- Tip:t
         ACCUM @@comments += Row(b.business_id, t.text, "tip");
  PRINT @@comments AS results;
}

Equivalent query in GQL standard:

gql
MATCH (b:Business {name: "Acme Oyster House"})
MATCH (b)<-[:REVIEWS]-(r:Review)
RETURN b.id AS business_id, r.text AS text, 'review' AS type
UNION
MATCH (b:Business {name: "Acme Oyster House"})
MATCH (b)<-[:TIPPED]-(t:Tip)
RETURN b.id AS business_id, t.text AS text, 'tip' AS type

Q25. Run degree centrality on the in-memory friend-graph and write the results back to the database

Subcategory: T5.1 Degree Centrality

This query runs the Degree Centrality algorithm on the friend graph to count each user's direct connections and stores the centrality score.

The queries in the Analytics category would require projecting a in-memory GDS graph first. Run the below query to do it.

gsql
// Neo4j projects FRIEND as UNDIRECTED. The loaded TigerGraph data contains
// reciprocal FRIEND rows, so counting FRIEND once as the forward edge set and
// once as the reverse edge set reproduces Neo4j's incident-relationship count.
// normalize=FALSE is required because Neo4j gds.degree.stream returns raw degree.
// Output: top 10 Vertex_ID/score pairs. Vertex_ID is the User.user_id; the
// packaged query does not also print User.name.
CALL GDBMS_ALGO.centrality.degree_cent(
  ["User"], ["FRIEND"], ["FRIEND"], TRUE, TRUE,
  10, TRUE, "", "", FALSE
);

Then run the following analytic query.

Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and centrality algorithms are not supported by standard GQL.

Q26. Run page rank on the in-memory friend-graph and write the results back to the database

Subcategory: T5.2 PageRank

This query runs the PageRank algorithm on the friend graph to measure user influence and lists the top 10 most influential users.

The queries in the Analytics category would require projecting a in-memory GDS graph first. Run the below query to do it.

gsql
// The 4.2.4 packaged query accepts one vertex type and one edge type as STRING,
// not SET<STRING>. Reciprocal FRIEND rows make its directed walk equivalent to
// the source workload's undirected projection for transition probabilities.
// Neo4j defaults are maxIterations=20, tolerance=1e-7, dampingFactor=0.85.
// Output: top 10 Vertex_ID/score pairs; Vertex_ID is User.user_id.
CALL GDBMS_ALGO.centrality.pagerank(
  "User", "FRIEND", 0.0000001, 20, 0.85, 10, TRUE, "", "", FALSE
);

Then run the following analytic query.

Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and projected graphs are not supported by standard GQL.

Q27. Find weakly connected components (WCC) and query the size of each WCC

Subcategory: T5.3 Connected Components / Weakly Connected Components

This query groups users into weakly connected components based on mutual or directional friendships and lists the sizes of the largest components.

The queries in the Analytics category would require projecting a in-memory GDS graph first. Run the below query to do it.

gsql
// print_limit=0 suppresses the per-vertex assignment list while retaining the
// component-size maps. Sort the returned `sizes` map by value descending and
// keep 10 entries to reproduce Neo4j's ORDER BY component_size DESC LIMIT 10.
CALL GDBMS_ALGO.community.wcc(
  ["User"], ["FRIEND"], 0, TRUE, "", ""
);

Then run the following analytic query.

Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and weakly connected components algorithms are not supported by standard GQL.

Q28. Find strongly connected components in the directed friend graph

Subcategory: T5.4 Strongly Connected Components

This query runs strongly connected components on the directed FRIEND graph and reports the largest components.

The queries in the Analytics category would require projecting a in-memory GDS graph first. Run the below query to do it.

gsql
// Despite its title, the Neo4j source projects FRIEND with orientation
// UNDIRECTED before calling SCC, so its Q28 result is component-equivalent to
// Q27. TigerGraph FRIEND has reciprocal rows but no separate reverse-edge type;
// passing FRIEND for both directions preserves that source behavior.
// Sort the returned component-size map descending and keep 20 entries.
CALL GDBMS_ALGO.community.scc_small_world(
  "User", "FRIEND", "FRIEND", 100000, TRUE
);

Then run the following analytic query.

Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and strongly connected component algorithms are not supported by standard GQL.

Q29. Find Louvain Community and query the size of each community

Subcategory: T5.5 Community Detection

This query identifies community structures within the user friendship network using the Louvain modularity optimization algorithm and lists the largest communities.

The queries in the Analytics category would require projecting a in-memory GDS graph first. Run the below query to do it.

Not supported by TigerGraph. TigerGraph 4.2.4's packaged Louvain template is not directly compatible with the reciprocal directed, unweighted FRIEND schema and does not stream community assignments. Skip this query instead of maintaining a custom template plus distributed-file aggregation.

Then run the following analytic query.

Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and community detection algorithms are not supported by standard GQL.

Q30. Count 3-hop neighbors of a given user

Subcategory: T1.3 Exact kth-Hop Neighbors and T4.1 Ungrouped Node Degree Statistics by Label

This query calculates the total number of unique users who are exactly three friend-connections away from a given user.

gsql
CREATE QUERY yelp_q30(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  SetAccum<VERTEX<User>> @@neighbors;
  start = SELECT s FROM User:s WHERE s.user_id == start_id;
  h3 = SELECT n FROM start:s -(FRIEND>)- User:a
                               -(FRIEND>)- User:b
                               -(FRIEND>)- User:n
       WHERE n.user_id != start_id ACCUM @@neighbors += n;
  PRINT @@neighbors.size() AS three_hop_count;
}

Equivalent query in GQL standard:

gql
MATCH (start:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]-{3}(neighbor)
WHERE neighbor.user_id != "Oi1qbcz2m2SnwUeztGYcnQ"
RETURN count(DISTINCT neighbor) AS three_hop_count

Q31. Who has the most mutual friends with a given user

Subcategory: T3.5 Two-hop Join Pattern and T4.1 Ungrouped Node Degree Statistics by Label

This query finds the top 50 users who share the highest number of mutual friends with a given user.

gsql
CREATE QUERY yelp_q31(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  SetAccum<STRING> @mutual_friend_ids;
  start = SELECT me FROM User:me WHERE me.user_id == start_id;
  others = SELECT other
           FROM start:me -(FRIEND>)- User:f -(<FRIEND)- User:other
           WHERE other.user_id != start_id
           ACCUM other.@mutual_friend_ids += f.user_id
           ORDER BY other.@mutual_friend_ids.size() DESC,
                    other.user_id ASC
           LIMIT 50;
  PRINT others[others.user_id AS user_id,
               others.name AS name,
               others.@mutual_friend_ids.size() AS mutual_count];
}

Equivalent query in GQL standard:

gql
MATCH (me:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]->(f:User)
MATCH (other:User)-[:FRIEND]->(f)
WHERE other != me
RETURN other.name, count(f) AS mutual_count
ORDER BY mutual_count DESC
LIMIT 50

Q32. For a given user's friends, count the friend-business pairs

Subcategory: T3.6 Line Pattern and T4.1 Ungrouped Node Degree Statistics by Label

This query counts the total number of distinct friend-business review relationships among the friends of a given user.

gsql
CREATE QUERY yelp_q32(STRING start_id = "Oi1qbcz2m2SnwUeztGYcnQ")
FOR GRAPH yelp SYNTAX v2 {
  SetAccum<STRING> @@pairs;
  start = SELECT u FROM User:u WHERE u.user_id == start_id;
  businesses = SELECT b
               FROM start:u -(FRIEND>)- User:f
                            -(WROTE>)- Review:r
                            -(REVIEWS>)- Business:b
               ACCUM @@pairs += f.user_id + "\t" + b.business_id;
  PRINT @@pairs.size() AS total_results;
}

Equivalent query in GQL standard:

gql
CALL {
  MATCH (u:User {user_id: "Oi1qbcz2m2SnwUeztGYcnQ"})-[:FRIEND]->(friend:User)-[:WROTE]->(r:Review)-[:REVIEWS]->(b:Business)
  RETURN DISTINCT
    friend.name AS friend_name,
    b.name AS business_name,
    b.address AS address,
    b.city AS city,
    b.state AS state,
    b.latitude AS latitude,
    b.longitude AS longitude
}
RETURN count(*) AS total_results

Q33. Find all users who commented on the above businesses

Subcategory: Other

This query lists all unique users who have written reviews for businesses situated within a 5-mile radius of a specific coordinate.

The following query requires a new attribute on Business nodes be set. Run this query to set location on Business nodes.

gsql
CREATE QUERY yelp_q33(DOUBLE center_lat = 39.877788,
                      DOUBLE center_lon = -75.69293535784999,
                      DOUBLE radius_m = 8046.72)
FOR GRAPH yelp SYNTAX v2 {
  MaxAccum<DOUBLE> @distance_m;
  DOUBLE earth_radius_m = 6371000.0;
  businesses = {Business.*};
  nearby = SELECT b FROM businesses:b
           ACCUM DOUBLE dlat = radians(b.latitude - center_lat),
                 DOUBLE dlon = radians(b.longitude - center_lon),
                 DOUBLE a = square(sin(dlat / 2.0))
                   + cos(radians(center_lat)) * cos(radians(b.latitude))
                   * square(sin(dlon / 2.0)),
                 b.@distance_m += 2.0 * earth_radius_m * asin(sqrt(a))
           HAVING b.@distance_m <= radius_m;
  users = SELECT u
          FROM User:u -(WROTE>)- Review:r -(REVIEWS>)- nearby:b;
  answer = SELECT u FROM users:u ORDER BY u.name ASC;
  PRINT answer[answer.user_id AS user_id, answer.name AS name];
}

Then we build point index with the location values.

Note: Cannot be expressed in standard GQL because spatial calculations, point representations, and distance filters are not supported by standard GQL.

Q34. Find the median of the above locations (from Q15)

Subcategory: Other

This query calculates the median latitude and longitude of all businesses reviewed by the friends of a given user.

Not supported by TigerGraph. TigerGraph ordinary GSQL has no direct equivalent of Neo4j's percentileCont(value, 0.5). Implement this only if an exact-median Query UDF or an approved statistics package is available. Substituting AVG would change the logic and is intentionally not done here.
Note: Cannot be expressed in standard GQL because percentileCont is a non-standard aggregate function not supported by standard GQL.

Q35. Count the number of users who commented on the above businesses

Subcategory: Other

This query calculates the total number of unique users who have reviewed businesses within a 5-mile radius of a specific coordinate.

The following query requires a new attribute on Business nodes be set. Run this query to set location on Business nodes.

gsql
CREATE QUERY yelp_q35(DOUBLE center_lat = 39.877788,
                      DOUBLE center_lon = -75.69293535784999,
                      DOUBLE radius_m = 8046.72)
FOR GRAPH yelp SYNTAX v2 {
  MaxAccum<DOUBLE> @distance_m;
  DOUBLE earth_radius_m = 6371000.0;
  businesses = {Business.*};
  nearby = SELECT b FROM businesses:b
           ACCUM DOUBLE dlat = radians(b.latitude - center_lat),
                 DOUBLE dlon = radians(b.longitude - center_lon),
                 DOUBLE a = square(sin(dlat / 2.0))
                   + cos(radians(center_lat)) * cos(radians(b.latitude))
                   * square(sin(dlon / 2.0)),
                 b.@distance_m += 2.0 * earth_radius_m * asin(sqrt(a))
           HAVING b.@distance_m <= radius_m;
  users = SELECT u
          FROM User:u -(WROTE>)- Review:r -(REVIEWS>)- nearby:b;
  PRINT users.size() AS user_count;
}

Then we build point index with the location values.

Note: Cannot be expressed in standard GQL because spatial calculations, point representations, and distance filters are not supported by standard GQL.

Q36. Find the most frequent names

Subcategory: Other

This query counts the occurrences of each user name in the dataset and returns the top 30 most frequent names in descending order.

gsql
CREATE QUERY yelp_q36() FOR GRAPH yelp SYNTAX v2 {
  SELECT u.name AS name, COUNT(*) AS occurrences
  INTO name_counts
  FROM User:u
  GROUP BY u.name
  ORDER BY occurrences DESC
  LIMIT 30;
  PRINT name_counts;
}

Equivalent query in GQL standard:

gql
MATCH (u:User)
RETURN u.name AS name, count(*) AS occurrences
ORDER BY occurrences DESC
LIMIT 30

Q37. Find all businesses within 5 miles from a spicified location

Subcategory: Other

This query retrieves all businesses located within a 5-mile radius of a specific geographical coordinate, sorted by proximity.

The following query requires a new attribute on Business nodes be set. Run this query to set location on Business nodes.

gsql
CREATE QUERY yelp_q37(DOUBLE center_lat = 39.877788,
                      DOUBLE center_lon = -75.69293535784999,
                      DOUBLE radius_m = 8046.72)
FOR GRAPH yelp SYNTAX v2 {
  MaxAccum<DOUBLE> @distance_m;
  DOUBLE earth_radius_m = 6371000.0;
  businesses = {Business.*};
  answer = SELECT b FROM businesses:b
           ACCUM DOUBLE dlat = radians(b.latitude - center_lat),
                 DOUBLE dlon = radians(b.longitude - center_lon),
                 DOUBLE a = square(sin(dlat / 2.0))
                   + cos(radians(center_lat)) * cos(radians(b.latitude))
                   * square(sin(dlon / 2.0)),
                 b.@distance_m += 2.0 * earth_radius_m * asin(sqrt(a))
           HAVING b.@distance_m <= radius_m
           ORDER BY b.@distance_m ASC;
  PRINT answer[answer.name AS name, answer.address AS address,
               answer.city AS city, answer.state AS state,
               answer.latitude AS latitude, answer.longitude AS longitude,
               answer.@distance_m AS dist_meters];
}

Then we build point index with the location values.

Note: Cannot be expressed in standard GQL because spatial distance calculations and point representations are not supported by standard GQL.

Q38. Find the top-10 nearest business to the specific location

Subcategory: Other

This query identifies the 10 nearest businesses to a specific geographical coordinate and ranks them in order of distance.

The following query requires a new attribute on Business nodes be set. Run this query to set location on Business nodes.

gsql
CREATE QUERY yelp_q38(DOUBLE center_lat = 39.877788,
                      DOUBLE center_lon = -75.69293535784999)
FOR GRAPH yelp SYNTAX v2 {
  MaxAccum<DOUBLE> @distance_m;
  DOUBLE earth_radius_m = 6371000.0;
  businesses = {Business.*};
  answer = SELECT b FROM businesses:b
           ACCUM DOUBLE dlat = radians(b.latitude - center_lat),
                 DOUBLE dlon = radians(b.longitude - center_lon),
                 DOUBLE a = square(sin(dlat / 2.0))
                   + cos(radians(center_lat)) * cos(radians(b.latitude))
                   * square(sin(dlon / 2.0)),
                 b.@distance_m += 2.0 * earth_radius_m * asin(sqrt(a))
           ORDER BY b.@distance_m ASC LIMIT 10;
  PRINT answer[answer.name AS name, answer.address AS address,
               answer.city AS city, answer.state AS state,
               answer.latitude AS latitude, answer.longitude AS longitude,
               answer.@distance_m AS dist_meters];
}

Then we build point index with the location values.

Note: Cannot be expressed in standard GQL because spatial distance calculations and point representations are not supported by standard GQL.

Q39. Find reviews containing “disgusting”

Subcategory: Other

This query searches for and returns up to 100 review texts that contain the word "disgusting".

gsql
CREATE QUERY yelp_q39() FOR GRAPH yelp SYNTAX v2 {
  reviews = {Review.*};
  answer = SELECT r FROM reviews:r
           WHERE instr(lower(r.text), "disgusting") >= 0 LIMIT 100;
  PRINT answer[answer.review_id AS id, answer.text AS text];
}

Equivalent query in GQL standard:

gql
MATCH (r:Review)
WHERE toLower(r.text) CONTAINS "disgusting"
RETURN r.id, r.text
LIMIT 100

Q40. Find the most frequently used words in reviews

Subcategory: Other

This query extracts, filters (length > 3), and ranks the top 20 most frequent individual words used across all review text.

gsql
CREATE QUERY yelp_q40() FOR GRAPH yelp SYNTAX v2 {
  TYPEDEF TUPLE<STRING word, INT freq> Row;
  MapAccum<STRING, SumAccum<INT>> @@frequencies;
  HeapAccum<Row>(20, freq DESC, word ASC) @@top_words;
  reviews = {Review.*};
  scanned = SELECT r FROM reviews:r
            ACCUM FOREACH word IN split(lower(r.text), " ") DO
                    IF length(word) > 3 THEN
                      @@frequencies += (word -> 1)
                    END
                  END;
  FOREACH (word, frequency) IN @@frequencies DO
    @@top_words += Row(word, frequency);
  END;
  PRINT @@top_words AS results;
}

Equivalent query in GQL standard:

gql
MATCH (r:Review)
UNWIND split(toLower(r.text), " ") AS word
RETURN word, count(*) AS freq
  WHERE size(word) > 3
ORDER BY freq DESC
LIMIT 20