← Back to Amazon Dataset

TigerGraph Implementation

Deployment and Query logic for TigerGraph Enterprise 4.2.4.

Schema Diagram and Graph Statistics

Amazon TigerGraph Schema

Node Statistics

Vertex Type Count
amzn_Product 23,643,222
amzn_Category 2,487,388
amzn_Review 230,139,802
amzn_Image 17,081,240
amzn_ReviewImage 6,538,802
amzn_Reviewer 43,249,276

Edge Statistics

Edge Type Count
(:amzn_Review)-[:AMZN_REVIEWS]->(:amzn_Product) 230,139,802
(:amzn_Product)-[:AMZN_HAS_PRODUCT_IMAGE]->(:amzn_Image) 22,531,444
(:amzn_Product)-[:AMZN_ALSO_BUY]->(:amzn_Product) 97,123,542
(:amzn_Product)-[:AMZN_ALSO_BUY_REVERSE]->(:amzn_Product) 97,123,542
(:amzn_Reviewer)-[:AMZN_WROTE]->(:amzn_Review) 230,139,802
(:amzn_Review)-[:AMZN_HAS_REVIEW_IMAGE]->(:amzn_ReviewImage) 6,840,462
(:amzn_Product)-[:AMZN_IN_CATEGORY]->(:amzn_Category) 62,832,310
(:amzn_Product)-[:AMZN_ALSO_VIEW]->(:amzn_Product) 92,658,319

Every vertex and edge count matches the Neo4j load of the same dataset exactly. AMZN_ALSO_BUY_REVERSE has no Neo4j counterpart: TigerGraph materializes a declared reverse edge as its own edge type, so it is listed for completeness. It doubles the storage of AMZN_ALSO_BUY and lets the co-buy graph be walked in both directions.

One category name needs special handling. 237 products in Clothing_Shoes_and_Jewelry list an empty string among their categories, and Neo4j's MERGE creates a Category whose name is "". Since version 3.9 TigerGraph rejects an empty PRIMARY_ID, so that category is stored under the name <null>. The usual stand-ins could not be used: null, n/a, none and their case variants are all real category names in the source data. Queries that return category names therefore show <null> where Neo4j shows "".

Data Loading

Overview

Data import is handled in two stages: the vertex and edge types are declared and gathered into a graph, then two loading jobs read the input files into it, one for the product-side files and one for the review-side files. Unlike the Neo4j loader, which transforms the source JSON inside Cypher, the TigerGraph jobs read flat tab-separated files. A GSQL loading job cannot zip the paired image arrays, split price ranges, parse dates or expand the several arrays of one record, so that work happens before loading and produces eleven files per Amazon category folder:

File Columns Loaded into
product.tsv asin, title, brand, description, price_min, price_max amzn_Product
image.tsv url, highRes amzn_Image
category.tsv name amzn_Category
product_image.tsv asin, url AMZN_HAS_PRODUCT_IMAGE
also_buy.tsv asin, related_asin AMZN_ALSO_BUY
also_view.tsv asin, view_asin AMZN_ALSO_VIEW
product_category.tsv asin, name AMZN_IN_CATEGORY
review.tsv review_id, reviewerID, asin, unixReviewTime, reviewDatetime, summary, reviewText, verified, vote, overall amzn_Review, AMZN_REVIEWS, AMZN_WROTE
reviewer.tsv reviewerID, name amzn_Reviewer
review_image.tsv url amzn_ReviewImage
review_has_image.tsv review_id, url AMZN_HAS_REVIEW_IMAGE

Every file has a header row and is tab-separated without quoting, because the TigerGraph 4.2 loader always ends a record at a newline and loses track of quoted fields that contain an escaped "". A tab, newline or carriage return inside a field is therefore written as \t, \n or \r, a backslash as \\, and any other control character as \xNN; the loader stores these sequences verbatim. Vertex files repeat a key once per occurrence (a category shared by many products appears once per product), and the repeats collapse on load the way Cypher's MERGE does.

Neo4j creates Review nodes without a key, but every TigerGraph vertex needs a PRIMARY_ID. Each review is given reviewerID|asin|unixReviewTime|<source file>:<row number>, which is unique per row, so the amzn_Review count equals Neo4j's node count. Missing numbers (an unparsable price, a review without votes) are written as empty fields and load as the type's default, since TigerGraph attributes cannot be null.

Creating Graph Schema

The vertex and edge types are declared in global scope and then gathered into the graph by a single CREATE GRAPH at the end. TigerGraph 4.2 rejects the other order: issuing CREATE VERTEX while a graph is selected fails with Please 'use global' first before run command: CREATE VERTEX. Every type carries the amzn_ / AMZN_ prefix, because vertex and edge types are global and other datasets share this TigerGraph instance.

gsql
USE GLOBAL

Vertex Definitions

Then we define the Product, Review, Reviewer, Category, Image and ReviewImage vertices. Attribute names match the Neo4j property names exactly.

gsql
CREATE VERTEX amzn_Product (
  PRIMARY_ID asin STRING,
  title STRING,
  brand STRING,
  description STRING,
  price_min DOUBLE,
  price_max DOUBLE
) WITH PRIMARY_ID_AS_ATTRIBUTE="true"

CREATE VERTEX amzn_Review (
  PRIMARY_ID review_id STRING,
  reviewerID STRING,
  asin STRING,
  unixReviewTime UINT,
  reviewDatetime DATETIME,
  summary STRING,
  reviewText STRING,
  verified BOOL,
  vote UINT,
  overall UINT
) WITH PRIMARY_ID_AS_ATTRIBUTE="true"

CREATE VERTEX amzn_Reviewer (
  PRIMARY_ID reviewerID STRING,
  name STRING
) WITH PRIMARY_ID_AS_ATTRIBUTE="true"

CREATE VERTEX amzn_Category (
  PRIMARY_ID name STRING
) WITH PRIMARY_ID_AS_ATTRIBUTE="true"

CREATE VERTEX amzn_Image (
  PRIMARY_ID url STRING,
  highRes STRING
) WITH PRIMARY_ID_AS_ATTRIBUTE="true"

CREATE VERTEX amzn_ReviewImage (
  PRIMARY_ID url STRING
) WITH PRIMARY_ID_AS_ATTRIBUTE="true"

Edge Definitions

All edges are directed and follow the Cypher arrow directions. AMZN_ALSO_BUY also declares a reverse edge, because the co-buy queries (Q13, Q24, Q26-Q28) treat the co-buy graph as undirected or walk it both ways. Every other backwards traversal starts from the edge's source side instead, so no other type needs one.

gsql
CREATE DIRECTED EDGE AMZN_ALSO_BUY (FROM amzn_Product, TO amzn_Product)
  WITH REVERSE_EDGE="AMZN_ALSO_BUY_REVERSE"

CREATE DIRECTED EDGE AMZN_ALSO_VIEW (FROM amzn_Product, TO amzn_Product)

CREATE DIRECTED EDGE AMZN_IN_CATEGORY (FROM amzn_Product, TO amzn_Category)

CREATE DIRECTED EDGE AMZN_HAS_PRODUCT_IMAGE (FROM amzn_Product, TO amzn_Image)

CREATE DIRECTED EDGE AMZN_REVIEWS (FROM amzn_Review, TO amzn_Product)

CREATE DIRECTED EDGE AMZN_WROTE (FROM amzn_Reviewer, TO amzn_Review)

CREATE DIRECTED EDGE AMZN_HAS_REVIEW_IMAGE (FROM amzn_Review, TO amzn_ReviewImage)

With every type declared, the graph is assembled by naming them. AMZN_ALSO_BUY_REVERSE is listed explicitly: declaring the reverse edge creates the type globally, but the graph has to name it to see it.

gsql
CREATE GRAPH amazon (
  amzn_Product, amzn_Review, amzn_Reviewer,
  amzn_Category, amzn_Image, amzn_ReviewImage,
  AMZN_ALSO_BUY, AMZN_ALSO_BUY_REVERSE, AMZN_ALSO_VIEW,
  AMZN_IN_CATEGORY, AMZN_HAS_PRODUCT_IMAGE,
  AMZN_REVIEWS, AMZN_WROTE, AMZN_HAS_REVIEW_IMAGE
)

Import Jobs

Two loading jobs cover the whole dataset. Every LOAD addresses its columns by name, SEPARATOR="\t" selects the tab, and there is deliberately no QUOTE option, which is what turns quote processing off.

Note that in the following code blocks the folder in each file path is a placeholder: replace "path_to_folder" with an Amazon product category folder, for example All_Beauty. In our run each job defined these DEFINE FILENAME and LOAD statements once per category folder (29 folders, f_product_0 to f_product_28 and so on), so one RUN LOADING JOB per job loads every category.

First the product-side job.

gsql
CREATE LOADING JOB load_amazon_metadata FOR GRAPH amazon {
  DEFINE FILENAME f_product = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/product.tsv";
  DEFINE FILENAME f_image = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/image.tsv";
  DEFINE FILENAME f_category = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/category.tsv";
  DEFINE FILENAME f_product_image = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/product_image.tsv";
  DEFINE FILENAME f_also_buy = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/also_buy.tsv";
  DEFINE FILENAME f_also_view = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/also_view.tsv";
  DEFINE FILENAME f_product_category = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/product_category.tsv";

  LOAD f_product TO VERTEX amzn_Product VALUES ($"asin", $"title", $"brand", $"description", $"price_min", $"price_max")
      USING HEADER="true", SEPARATOR="\t";

  LOAD f_image TO VERTEX amzn_Image VALUES ($"url", $"highRes") USING HEADER="true", SEPARATOR="\t";

  LOAD f_category TO VERTEX amzn_Category VALUES ($"name") USING HEADER="true", SEPARATOR="\t";

  LOAD f_product_image TO EDGE AMZN_HAS_PRODUCT_IMAGE VALUES ($"asin", $"url")
      USING HEADER="true", SEPARATOR="\t";

  LOAD f_also_buy TO EDGE AMZN_ALSO_BUY VALUES ($"asin", $"related_asin") USING HEADER="true", SEPARATOR="\t";

  LOAD f_also_view TO EDGE AMZN_ALSO_VIEW VALUES ($"asin", $"view_asin") USING HEADER="true", SEPARATOR="\t";

  LOAD f_product_category TO EDGE AMZN_IN_CATEGORY VALUES ($"asin", $"name")
      USING HEADER="true", SEPARATOR="\t";

}

Then the review-side job. AMZN_REVIEWS and AMZN_WROTE come from columns already on review.tsv, so they need no separate edge file.

gsql
CREATE LOADING JOB load_amazon_reviews FOR GRAPH amazon {
  DEFINE FILENAME f_review = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/review.tsv";
  DEFINE FILENAME f_reviewer = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/reviewer.tsv";
  DEFINE FILENAME f_review_image = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/review_image.tsv";
  DEFINE FILENAME f_review_has_image = "/datasets/prop_datasets/amazon/tg_tsv2/path_to_folder/review_has_image.tsv";

  LOAD f_review TO VERTEX amzn_Review VALUES ($"review_id", $"reviewerID", $"asin", $"unixReviewTime", $"reviewDatetime", $"summary", $"reviewText", $"verified", $"vote", $"overall")
      USING HEADER="true", SEPARATOR="\t";
  LOAD f_review TO EDGE AMZN_REVIEWS VALUES ($"review_id", $"asin") USING HEADER="true", SEPARATOR="\t";
  LOAD f_review TO EDGE AMZN_WROTE VALUES ($"reviewerID", $"review_id") USING HEADER="true", SEPARATOR="\t";

  LOAD f_reviewer TO VERTEX amzn_Reviewer VALUES ($"reviewerID", $"name") USING HEADER="true", SEPARATOR="\t";

  LOAD f_review_image TO VERTEX amzn_ReviewImage VALUES ($"url") USING HEADER="true", SEPARATOR="\t";

  LOAD f_review_has_image TO EDGE AMZN_HAS_REVIEW_IMAGE VALUES ($"review_id", $"url")
      USING HEADER="true", SEPARATOR="\t";

}

Finally, select the graph and run them. Run the metadata job first: a referenced vertex is created implicitly, so loading reviews first would create placeholder amzn_Product vertices that the metadata job then has to fill in.

gsql
USE GRAPH amazon

RUN LOADING JOB load_amazon_metadata
RUN LOADING JOB load_amazon_reviews

Progress can be followed with SHOW LOADING STATUS ALL, and a running job stopped with ABORT LOADING JOB <job_id>. According to the loader log, the metadata job ran for 176 seconds and the review job for 507 seconds, about 11.4 minutes in total.

Queries

Select a query below to view its logic and description. Every query is a GSQL Syntax V2 definition; install one with INSTALL QUERY -SINGLE amzn_qNN and run it through RESTPP (/restpp/query/amazon/amzn_qNN). Parameters default to the values the benchmark uses. Q25-Q28 call the packaged graph-algorithm templates (GDBMS_ALGO) instead of an installed query.

1. Neighborhood
2. Path
3. Pattern
4. Aggregation
5. Analytics
6. Hybrid
7. Others
No queries in this category.

Q1. People Who Bought This Also Bought (Seed Expansion)

Subcategory: T1.1 One-Hop Neighbors

This query expands one hop from the seed products through ALSO_BUY edges, excludes the seeds themselves, and ranks neighboring products by how many seed products link to them.

gsql
CREATE OR REPLACE QUERY amzn_q01() FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<UINT review_count, STRING asin> SeedRow;

  SumAccum<UINT> @review_count;
  HeapAccum<SeedRow>(200, review_count DESC, asin ASC) @@seed_heap;
  SetAccum<STRING> @@seed_asins;
  OrAccum @is_seed;
  SetAccum<VERTEX<amzn_Product>> @linked_seeds;
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};

  /* Q16's ranking: products ordered by incoming review count. */
  counted = SELECT p
            FROM amzn_Review:r -(AMZN_REVIEWS>)- amzn_Product:p
            ACCUM p.@review_count += 1
            POST-ACCUM @@seed_heap += SeedRow(p.@review_count, p.asin);

  /* HeapAccum exposes size/top/pop rather than FOREACH, so drain it. */
  WHILE @@seed_heap.size() > 0 DO
    @@seed_asins += @@seed_heap.top().asin;
    @@seed_heap.pop();
  END;

  seeds = SELECT p FROM products:p
          WHERE @@seed_asins.contains(p.asin)
          POST-ACCUM p.@is_seed += TRUE;

  others = SELECT other
           FROM seeds:seed -(AMZN_ALSO_BUY>)- amzn_Product:other
           WHERE NOT other.@is_seed
           ACCUM other.@linked_seeds += seed
           POST-ACCUM other.@display_title +=
             CASE WHEN other.title == "" THEN other.asin
                  ELSE other.title END
           ORDER BY other.@linked_seeds.size() DESC
           LIMIT 200;

  PRINT others[others.asin AS ASIN,
               others.@display_title AS CoBuyProduct,
               others.@linked_seeds.size() AS LinkedSeeds];
}

TigerGraph notes

The Cypher version reads the :SeedProduct label written by Q16. Because that label cannot exist in TigerGraph, this query recomputes the same seed set inline: the 200 products with the most reviews.

Equivalent query in GQL standard:

gql
MATCH (seed:SeedProduct)-[:ALSO_BUY]->(other:Product)
WHERE NOT other:SeedProduct
RETURN other.asin AS ASIN,
    coalesce(other.title, other.asin) AS CoBuyProduct,
    count(DISTINCT seed) AS LinkedSeeds
ORDER BY LinkedSeeds DESC
LIMIT 200;

Q2. Two-Hop Co-Buy Recommendations

Subcategory: T1.2 k-Hop Neighborhood

This query expands from a selected product through two outgoing ALSO_BUY hops and returns distinct second-hop products.

gsql
CREATE OR REPLACE QUERY amzn_q02(STRING seed_asin = "0000031895")
FOR GRAPH amazon SYNTAX v2 {
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};
  start = SELECT p FROM products:p WHERE p.asin == seed_asin;

  /* A GSQL vertex set is inherently deduplicated, matching RETURN DISTINCT. */
  recs = SELECT rec
         FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:mid
                      -(AMZN_ALSO_BUY>)- amzn_Product:rec
         WHERE rec.asin != seed_asin
         ACCUM rec.@display_title +=
           CASE WHEN rec.title == "" THEN rec.asin ELSE rec.title END
         ORDER BY rec.@display_title ASC
         LIMIT 200;

  PRINT recs[recs.asin AS ASIN, recs.@display_title AS Product];
}

TigerGraph notes

Equivalent query in GQL standard:

gql
MATCH (:Product {asin: $asin})-[:ALSO_BUY]->(:Product)-[:ALSO_BUY]->(rec:Product)
WHERE rec.asin <> $asin
RETURN DISTINCT rec.asin AS ASIN, coalesce(rec.title, rec.asin) AS Product
ORDER BY Product
LIMIT $limit;

Q3. Products Exactly Two Co-Buy Hops Away

Subcategory: T1.3 Exact kth-Hop Neighbors

This query returns products exactly two ALSO_BUY hops from a selected product, excluding direct one-hop recommendations.

gsql
CREATE OR REPLACE QUERY amzn_q03(STRING seed_asin = "0000031895")
FOR GRAPH amazon SYNTAX v2 {
  OrAccum @is_direct_neighbor;
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};
  start = SELECT p FROM products:p WHERE p.asin == seed_asin;

  hop1 = SELECT nbr
         FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:nbr
         POST-ACCUM nbr.@is_direct_neighbor += TRUE;

  second = SELECT t
           FROM hop1:m -(AMZN_ALSO_BUY>)- amzn_Product:t
           WHERE t.asin != seed_asin AND NOT t.@is_direct_neighbor
           ACCUM t.@display_title +=
             CASE WHEN t.title == "" THEN t.asin ELSE t.title END
           ORDER BY t.@display_title ASC
           LIMIT 200;

  PRINT second[second.asin AS ASIN, second.@display_title AS Product];
}

TigerGraph notes

Excludes the seed itself and any product also reachable in one hop, which is the NOT (seed)-[:ALSO_BUY]->(second) predicate.

Equivalent query in GQL standard:

gql
MATCH (seed:Product {asin: $asin})-[:ALSO_BUY]->(:Product)-[:ALSO_BUY]->(second:Product)
WHERE second <> seed
  AND NOT (seed)-[:ALSO_BUY]->(second)
RETURN DISTINCT second.asin AS ASIN, coalesce(second.title, second.asin) AS Product
ORDER BY Product
LIMIT $limit;

Q4. Highly Rated Co-Buy Neighbors

Subcategory: T1.4 Neighborhood with Property Filter

This query filters one-hop co-buy neighbors to products whose review average meets a minimum rating threshold.

gsql
CREATE OR REPLACE QUERY amzn_q04(STRING seed_asin = "0000031895",
                                 DOUBLE min_avg_rating = 4.0)
FOR GRAPH amazon SYNTAX v2 {
  AvgAccum @avg_rating;
  SumAccum<UINT> @review_count;
  MaxAccum<DOUBLE> @avg_rating_rounded;
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};
  start = SELECT p FROM products:p WHERE p.asin == seed_asin;

  neighbors = SELECT nbr FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:nbr;

  /* r.overall > 0 stands in for Cypher's `r.overall IS NOT NULL`. */
  rated = SELECT p
          FROM amzn_Review:r -(AMZN_REVIEWS>)- neighbors:p
          WHERE r.overall > 0
          ACCUM p.@avg_rating += r.overall,
                p.@review_count += 1
          POST-ACCUM p.@avg_rating_rounded += round(p.@avg_rating, 2),
                     p.@display_title +=
                       CASE WHEN p.title == "" THEN p.asin ELSE p.title END
          HAVING p.@avg_rating >= min_avg_rating
          ORDER BY p.@avg_rating DESC, p.@review_count DESC
          LIMIT 100;

  PRINT rated[rated.asin AS ASIN,
              rated.@display_title AS Product,
              rated.@avg_rating_rounded AS AvgRating,
              rated.@review_count AS ReviewCount];
}

TigerGraph notes

Equivalent query in GQL standard:

gql
MATCH (:Product {asin: $asin})-[:ALSO_BUY]->(p:Product)<-[:REVIEWS]-(r:Review)
WHERE r.overall IS NOT NULL
WITH p, avg(r.overall) AS AvgRating, count(r) AS ReviewCount
WHERE AvgRating >= $minRating
RETURN p.asin AS ASIN, coalesce(p.title, p.asin) AS Product, round(AvgRating, 2) AS AvgRating, ReviewCount
ORDER BY AvgRating DESC, ReviewCount DESC
LIMIT $limit;

Q5. High-Rating Helpful Review Records

Subcategory: T2.2 Edge Filtering

Reviews are modeled as nodes between reviewers and products, so this query filters review-edge records by rating and helpful-vote properties.

gsql
CREATE OR REPLACE QUERY amzn_q05(INT min_rating = 4, INT min_votes = 10)
FOR GRAPH amazon SYNTAX v2 {
  SELECT u.reviewerID AS ReviewerID,
         p.asin AS ASIN,
         r.overall AS Rating,
         r.vote AS HelpfulVotes
  INTO Result
  FROM amzn_Reviewer:u -(AMZN_WROTE>)- amzn_Review:r
                       -(AMZN_REVIEWS>)- amzn_Product:p
  WHERE r.overall >= min_rating AND r.vote >= min_votes
  ORDER BY HelpfulVotes DESC, Rating DESC
  LIMIT 100;

  PRINT Result;
}

TigerGraph notes

A row-shaped result, so this uses the SELECT ... INTO table form. r.vote >= 10 is an exact translation of coalesce(r.vote, 0) >= 10.

Equivalent query in GQL standard:

gql
MATCH (u:Reviewer)-[:WROTE]->(r:Review)-[:REVIEWS]->(p:Product)
WHERE r.overall >= $minRating
  AND coalesce(r.vote, 0) >= $minHelpfulVotes
RETURN u.reviewerID AS ReviewerID, p.asin AS ASIN, r.overall AS Rating, r.vote AS HelpfulVotes
ORDER BY HelpfulVotes DESC, Rating DESC
LIMIT $limit;

Q6. Reachability Between Products in the Co-Buy Graph

Subcategory: T2.3 Reachability between Two Nodes

This query tests whether one product can reach another product through up to four ALSO_BUY hops.

gsql
CREATE OR REPLACE QUERY amzn_q06(STRING source_asin = "0000031895",
                                 STRING target_asin = "B00JM8YXXG",
                                 INT max_hops = 4)
FOR GRAPH amazon SYNTAX v2 {
  OrAccum @visited;
  OrAccum @@reachable;
  INT hop = 0;

  products = {amzn_Product.*};
  frontier = SELECT p FROM products:p WHERE p.asin == source_asin
             POST-ACCUM p.@visited += TRUE;

  WHILE frontier.size() > 0 AND hop < max_hops AND NOT @@reachable DO
    hop = hop + 1;
    frontier = SELECT t
               FROM frontier:s -(AMZN_ALSO_BUY>)- amzn_Product:t
               WHERE NOT t.@visited
               ACCUM @@reachable += (t.asin == target_asin)
               POST-ACCUM t.@visited += TRUE;
  END;

  PRINT @@reachable AS reachable;
}

TigerGraph notes

Cypher returns count(p) > 0; the boolean is what is actually asked for, so the BFS stops as soon as the destination is seen instead of enumerating paths.

Equivalent query in GQL standard:

gql
MATCH p = ANY (:Product {asin: $sourceAsin})-[:ALSO_BUY]->{1,4}(:Product {asin: $targetAsin})
RETURN true AS reachable;

Q7. Shortest Co-Buy Path Between Products

Subcategory: T2.4 Shortest Path

This query finds the shortest directed co-buy path between two products within a bounded depth.

gsql
CREATE OR REPLACE QUERY amzn_q07(STRING source_asin = "0000031895",
                                 STRING target_asin = "B00JM8YXXG",
                                 INT max_hops = 6)
FOR GRAPH amazon SYNTAX v2 {
  OrAccum @visited;
  MinAccum<INT> @hop_count = 999999;
  MaxAccum<STRING> @path;
  OrAccum @@found;
  INT hop = 0;

  products = {amzn_Product.*};
  frontier = SELECT p FROM products:p WHERE p.asin == source_asin
             POST-ACCUM p.@visited += TRUE,
                        p.@hop_count += 0,
                        p.@path += p.asin;

  WHILE frontier.size() > 0 AND hop < max_hops AND NOT @@found DO
    hop = hop + 1;
    frontier = SELECT t
               FROM frontier:s -(AMZN_ALSO_BUY>)- amzn_Product:t
               WHERE NOT t.@visited
               ACCUM t.@hop_count += hop,
                     t.@path += s.@path + " -> " + t.asin,
                     @@found += (t.asin == target_asin)
               POST-ACCUM t.@visited += TRUE;
  END;

  target_set = SELECT p FROM products:p
               WHERE p.asin == target_asin AND p.@hop_count < 999999;

  PRINT target_set[target_set.@path AS ProductPath,
                   target_set.@hop_count AS HopCount];
}

TigerGraph notes

Cypher's shortestPath() returns one minimal path. The level-synchronous BFS below assigns each product its minimum hop count; @path keeps one witness path per vertex, which is the same "any one shortest path" guarantee.

Equivalent query in GQL standard:

gql
MATCH p = SHORTEST (:Product {asin: $sourceAsin})-[:ALSO_BUY]->{1,6}(:Product {asin: $targetAsin})
RETURN p;

Q8. Simple Co-Buy Paths up to Three Hops

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

This query enumerates acyclic co-buy paths between two products up to three hops.

gsql
CREATE OR REPLACE QUERY amzn_q08(STRING source_asin = "0000031895",
                                 STRING target_asin = "B00JM8YXXG")
FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<INT hop_count, STRING product_path> PathRow;
  HeapAccum<PathRow>(200, hop_count ASC, product_path ASC) @@paths;

  products = {amzn_Product.*};
  start = SELECT p FROM products:p WHERE p.asin == source_asin;

  p1 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:t
       WHERE t.asin == target_asin
       ACCUM @@paths += PathRow(1, s.asin + " -> " + t.asin);

  p2 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:a
                             -(AMZN_ALSO_BUY>)- amzn_Product:t
       WHERE t.asin == target_asin
         AND a.asin != source_asin AND a.asin != target_asin
       ACCUM @@paths += PathRow(2, s.asin + " -> " + a.asin
                                   + " -> " + t.asin);

  p3 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:a
                             -(AMZN_ALSO_BUY>)- amzn_Product:b
                             -(AMZN_ALSO_BUY>)- amzn_Product:t
       WHERE t.asin == target_asin
         AND a.asin != b.asin
         AND a.asin != source_asin AND a.asin != target_asin
         AND b.asin != source_asin AND b.asin != target_asin
       ACCUM @@paths += PathRow(3, s.asin + " -> " + a.asin
                                   + " -> " + b.asin + " -> " + t.asin);

  PRINT @@paths AS paths;
}

TigerGraph notes

all(n IN nodes(p) WHERE single(m IN nodes(p) WHERE m = n)) means every node on the path is distinct, so each length is enumerated with explicit inequalities.

Equivalent query in GQL standard:

gql
MATCH p = (src:Product {asin: $sourceAsin})-[:ALSO_BUY]->{1,3}(dst:Product {asin: $targetAsin})
RETURN p
LIMIT $limit;

Q9. Top Weighted Co-Buy Paths by Product Price

Subcategory: T2.6 top-K Weighted Paths

This query ranks bounded co-buy paths by the sum of product prices along each path.

gsql
CREATE OR REPLACE QUERY amzn_q09(STRING source_asin = "0000031895",
                                 STRING target_asin = "B00JM8YXXG")
FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<DOUBLE path_price, INT hop_count,
                STRING product_path> WeightedPathRow;
  HeapAccum<WeightedPathRow>(100, path_price DESC,
                             product_path ASC) @@paths;

  products = {amzn_Product.*};
  start = SELECT p FROM products:p WHERE p.asin == source_asin;

  p1 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:t
       WHERE t.asin == target_asin
       ACCUM @@paths += WeightedPathRow(s.price_min + t.price_min,
                                        1,
                                        s.asin + " -> " + t.asin);

  p2 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:a
                             -(AMZN_ALSO_BUY>)- amzn_Product:t
       WHERE t.asin == target_asin
       ACCUM @@paths += WeightedPathRow(
                          s.price_min + a.price_min + t.price_min,
                          2,
                          s.asin + " -> " + a.asin + " -> " + t.asin);

  p3 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:a
                             -(AMZN_ALSO_BUY>)- amzn_Product:b
                             -(AMZN_ALSO_BUY>)- amzn_Product:t
       WHERE t.asin == target_asin
       ACCUM @@paths += WeightedPathRow(
                          s.price_min + a.price_min
                            + b.price_min + t.price_min,
                          3,
                          s.asin + " -> " + a.asin
                            + " -> " + b.asin + " -> " + t.asin);

  PRINT @@paths AS weighted_paths;
}

TigerGraph notes

DEVIATION: the Cypher reduce() sums n.price, which the loader never writes -- it writes price_min and price_max -- so in Neo4j PathPrice is always 0.0 and the ORDER BY degenerates. This sums price_min instead, which is what the query title and its top-k classification intend.

Caveat on coverage: the raw price field is absent for roughly two thirds of products (a 3,000-record sample of meta_All_Beauty.json gave 68% empty, 29% parsable, 3% unparsable CSS fragments that toFloat turns into null). price_min is therefore 0.0 for most products under TigerGraph's non-nullable DOUBLE, so path weights stay sparse -- the ranking is meaningful but weakly discriminating.

Unlike Q8 this enumerates walks, not simple paths, matching the Cypher source, which has no distinctness predicate here.

Equivalent query in GQL standard:

gql
MATCH p = (:Product {asin: $sourceAsin})-[:ALSO_BUY]->{1,3}(:Product {asin: $targetAsin})
RETURN p
LIMIT $k;

Q10. Co-Buy Product Triangles

Subcategory: T3.1 Triangle Detection

This query finds directed triangles in the product co-buy graph.

gsql
CREATE OR REPLACE QUERY amzn_q10() FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<STRING a_asin, STRING b_asin> Prefix;
  TYPEDEF TUPLE<STRING Product1, STRING Product2,
                STRING Product3> TriangleRow;

  SetAccum<STRING> @out_neighbors;
  SetAccum<Prefix> @prefixes;
  HeapAccum<TriangleRow>(200, Product1 ASC, Product2 ASC,
                         Product3 ASC) @@triangles;

  products = {amzn_Product.*};

  topology = SELECT a
             FROM products:a -(AMZN_ALSO_BUY>)- amzn_Product:b
             ACCUM a.@out_neighbors += b.asin;

  middles = SELECT b
            FROM products:a -(AMZN_ALSO_BUY>)- amzn_Product:b
            WHERE a.asin < b.asin
            ACCUM b.@prefixes += Prefix(a.asin, b.asin);

  closed = SELECT c
           FROM middles:b -(AMZN_ALSO_BUY>)- amzn_Product:c
           ACCUM FOREACH pre IN b.@prefixes DO
                   IF pre.b_asin < c.asin
                      AND c.@out_neighbors.contains(pre.a_asin) THEN
                     @@triangles += TriangleRow(pre.a_asin, pre.b_asin,
                                                c.asin)
                   END
                 END;

  PRINT @@triangles AS triangles;
}

TigerGraph notes

Directed 3-cycles p1->p2->p3->p1, canonicalized by asin < asin < asin. @out_neighbors materializes the outgoing adjacency so the closing edge can be tested without a third traversal.

Equivalent query in GQL standard:

gql
MATCH (p1:Product)-[:ALSO_BUY]->(p2:Product)-[:ALSO_BUY]->(p3:Product)-[:ALSO_BUY]->(p1)
WHERE p1.asin < p2.asin AND p2.asin < p3.asin
RETURN p1.asin AS Product1, p2.asin AS Product2, p3.asin AS Product3
LIMIT $limit;

Q11. Four-Product Co-Buy Cycles

Subcategory: T3.2 Square Detection

This query searches for four-product cycles formed by directed ALSO_BUY relationships, where the second and fourth products must be different.

gsql
CREATE OR REPLACE QUERY amzn_q11() FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<STRING a_asin, STRING b_asin> Path2;
  TYPEDEF TUPLE<STRING a_asin, STRING b_asin, STRING c_asin> Path3;
  TYPEDEF TUPLE<STRING Product1, STRING Product2,
                STRING Product3, STRING Product4> CycleRow;

  SetAccum<STRING> @out_neighbors;
  SetAccum<Path2> @path2;
  SetAccum<Path3> @path3;
  HeapAccum<CycleRow>(200, Product1 ASC, Product2 ASC,
                      Product3 ASC, Product4 ASC) @@cycles;

  products = {amzn_Product.*};

  topology = SELECT a
             FROM products:a -(AMZN_ALSO_BUY>)- amzn_Product:b
             ACCUM a.@out_neighbors += b.asin;

  hop1 = SELECT b
         FROM products:a -(AMZN_ALSO_BUY>)- amzn_Product:b
         WHERE a.asin < b.asin
         ACCUM b.@path2 += Path2(a.asin, b.asin);

  hop2 = SELECT c
         FROM hop1:b -(AMZN_ALSO_BUY>)- amzn_Product:c
         ACCUM FOREACH p IN b.@path2 DO
                 IF p.a_asin < c.asin AND p.b_asin != c.asin THEN
                   c.@path3 += Path3(p.a_asin, p.b_asin, c.asin)
                 END
               END;

  closed = SELECT d
           FROM hop2:c -(AMZN_ALSO_BUY>)- amzn_Product:d
           ACCUM FOREACH p IN c.@path3 DO
                   IF p.a_asin < d.asin
                      AND p.b_asin != d.asin AND p.c_asin != d.asin
                      AND d.@out_neighbors.contains(p.a_asin) THEN
                     @@cycles += CycleRow(p.a_asin, p.b_asin,
                                          p.c_asin, d.asin)
                   END
                 END;

  PRINT @@cycles AS four_product_cycles;
}

TigerGraph notes

Directed 4-cycles. Cypher fixes p1 as the smallest asin and, since 2026-10-17, also requires p2 <> p4 (without it a->b->c->b->a, which uses four distinct relationships, would match). So p1 < p2, p1 < p3, p1 < p4 are applied, plus b != c, b != d and c != d; of those only b != d is a real filter -- b != c and c != d would need a self-loop.

Since 2026-10-17 the workload also requires p2 <> p4 (without it Cypher would accept a->b->c->b->a, which uses four distinct relationships). The GSQL query enforces it with p.b_asin != d.asin; the GQL text below is the published Q11 with that condition added.

Equivalent query in GQL standard:

gql
MATCH (p1:Product)-[:ALSO_BUY]->(p2:Product)-[:ALSO_BUY]->(p3:Product)-[:ALSO_BUY]->(p4:Product)-[:ALSO_BUY]->(p1)
WHERE p1.asin < p2.asin AND p1.asin < p3.asin AND p1.asin < p4.asin AND p2 <> p4
RETURN p1.asin AS Product1, p2.asin AS Product2, p3.asin AS Product3, p4.asin AS Product4
LIMIT $limit;

Q12. Products with Many Co-Buy Recommendations

Subcategory: T3.3 Star Pattern Query

This query finds star-like products that point to many co-buy recommendation neighbors.

gsql
CREATE OR REPLACE QUERY amzn_q12(INT min_recommendations = 50)
FOR GRAPH amazon SYNTAX v2 {
  SetAccum<VERTEX<amzn_Product>> @neighbors;
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};

  centers = SELECT center
            FROM products:center -(AMZN_ALSO_BUY>)- amzn_Product:nbr
            ACCUM center.@neighbors += nbr
            POST-ACCUM center.@display_title +=
              CASE WHEN center.title == "" THEN center.asin
                   ELSE center.title END
            HAVING center.@neighbors.size() >= min_recommendations
            ORDER BY center.@neighbors.size() DESC
            LIMIT 100;

  PRINT centers[centers.asin AS ASIN,
                centers.@display_title AS Product,
                centers.@neighbors.size() AS RecommendationCount];
}

TigerGraph notes

Equivalent query in GQL standard:

gql
MATCH (center:Product)-[:ALSO_BUY]->(neighbor:Product)
WITH center, count(DISTINCT neighbor) AS RecommendationCount
WHERE RecommendationCount >= $minDegree
RETURN center.asin AS ASIN, coalesce(center.title, center.asin) AS Product, RecommendationCount
ORDER BY RecommendationCount DESC
LIMIT $limit;

Q13. Four-Product Co-Buy Cliques

Subcategory: T3.4 4-Clique

This query treats co-buy links as undirected and finds four products where every pair is connected by an ALSO_BUY relationship.

gsql
CREATE OR REPLACE QUERY amzn_q13() FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<STRING a_asin, STRING b_asin> Pair;
  TYPEDEF TUPLE<STRING a_asin, STRING b_asin, STRING c_asin> Triple;
  TYPEDEF TUPLE<STRING Product1, STRING Product2,
                STRING Product3, STRING Product4> CliqueRow;

  SetAccum<STRING> @neighbors;
  SetAccum<Pair> @pairs;
  SetAccum<Triple> @triples;
  SetAccum<VERTEX<amzn_Product>> @@pair_ends;
  SetAccum<VERTEX<amzn_Product>> @@triple_ends;
  HeapAccum<CliqueRow>(200, Product1 ASC, Product2 ASC,
                       Product3 ASC, Product4 ASC) @@cliques;

  products = {amzn_Product.*};

  topology = SELECT a
             FROM products:a -(AMZN_ALSO_BUY>|AMZN_ALSO_BUY_REVERSE>)-
                  amzn_Product:b
             ACCUM a.@neighbors += b.asin;

  build_pairs = SELECT b
                FROM products:a -(AMZN_ALSO_BUY>|AMZN_ALSO_BUY_REVERSE>)-
                     amzn_Product:b
                WHERE a.asin < b.asin
                ACCUM b.@pairs += Pair(a.asin, b.asin),
                      @@pair_ends += b;

  pair_ends = {@@pair_ends};
  build_triples = SELECT c
                  FROM pair_ends:b -(AMZN_ALSO_BUY>|AMZN_ALSO_BUY_REVERSE>)-
                       amzn_Product:c
                  ACCUM FOREACH p IN b.@pairs DO
                          IF p.b_asin < c.asin
                             AND c.@neighbors.contains(p.a_asin) THEN
                            c.@triples += Triple(p.a_asin, p.b_asin,
                                                 c.asin),
                            @@triple_ends += c
                          END
                        END;

  triple_ends = {@@triple_ends};
  close_cliques = SELECT d
                  FROM triple_ends:c
                       -(AMZN_ALSO_BUY>|AMZN_ALSO_BUY_REVERSE>)-
                       amzn_Product:d
                  ACCUM FOREACH p IN c.@triples DO
                          IF p.c_asin < d.asin
                             AND d.@neighbors.contains(p.a_asin)
                             AND d.@neighbors.contains(p.b_asin) THEN
                            @@cliques += CliqueRow(p.a_asin, p.b_asin,
                                                   p.c_asin, d.asin)
                          END
                        END;

  PRINT @@cliques AS four_product_cliques;
}

TigerGraph notes

The Cypher pattern uses undirected -[:ALSO_BUY]- throughout, so the reverse edge carries the backward direction and each product accumulates its own undirected neighbour set.

Storing neighbours as asins lets the closing-edge tests read the endpoint's own @neighbors set. An earlier version of this translation kept a global @@links set holding every edge in both directions, which on a 23M-product graph is far larger than the per-vertex sets that replace it here.

Equivalent query in GQL standard:

gql
MATCH (p1:Product)-[:ALSO_BUY]-(p2:Product),
      (p1)-[:ALSO_BUY]-(p3:Product),
      (p1)-[:ALSO_BUY]-(p4:Product),
      (p2)-[:ALSO_BUY]-(p3),
      (p2)-[:ALSO_BUY]-(p4),
      (p3)-[:ALSO_BUY]-(p4)
WHERE p1.asin < p2.asin AND p2.asin < p3.asin AND p3.asin < p4.asin
RETURN p1.asin AS Product1, p2.asin AS Product2, p3.asin AS Product3, p4.asin AS Product4
LIMIT $limit;

Q14. Reviewers Who Reviewed the Same Product

Subcategory: T3.5 Two-hop Join Pattern

This query joins reviewers through products they have both reviewed.

gsql
CREATE OR REPLACE QUERY amzn_q14() FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<STRING Reviewer1, STRING Reviewer2,
                UINT SharedProductCount> PairRow;

  SetAccum<STRING> @reviewer_ids;
  MapAccum<STRING, SumAccum<UINT>> @@shared_products;
  HeapAccum<PairRow>(100, SharedProductCount DESC,
                     Reviewer1 ASC, Reviewer2 ASC) @@rows;

  reviewers = {amzn_Reviewer.*};

  by_product = SELECT p
               FROM reviewers:u -(AMZN_WROTE>)- amzn_Review:r
                                -(AMZN_REVIEWS>)- amzn_Product:p
               ACCUM p.@reviewer_ids += u.reviewerID;

  /* count(DISTINCT p): each product contributes 1 to each of its pairs. */
  paired = SELECT p FROM by_product:p
           ACCUM FOREACH u1 IN p.@reviewer_ids DO
                   FOREACH u2 IN p.@reviewer_ids DO
                     IF u1 < u2 THEN
                       @@shared_products += ((u1 + "\t" + u2) -> 1)
                     END
                   END
                 END;

  FOREACH (pair_key, shared) IN @@shared_products DO
    @@rows += PairRow(
      substr(pair_key, 0, instr(pair_key, "\t")),
      substr(pair_key, instr(pair_key, "\t") + 1),
      shared);
  END;

  PRINT @@rows AS reviewer_pairs;
}

TigerGraph notes

Cypher walks Reviewer->Review->Product<-Review<-Reviewer. The translation collects each product's reviewer set in one forward pass, then forms the u1 < u2 pairs per product, which avoids needing reverse REVIEWS/WROTE edges.

COST: the pair step is quadratic in a product's reviewer count, and @@shared_products holds one entry per co-reviewing pair. The Cypher source has exactly the same blow-up; budget memory accordingly.

Equivalent query in GQL standard:

gql
MATCH (u1:Reviewer)-[:WROTE]->(:Review)-[:REVIEWS]->(p:Product)<-[:REVIEWS]-(:Review)<-[:WROTE]-(u2:Reviewer)
WHERE u1.reviewerID < u2.reviewerID
RETURN u1.reviewerID AS Reviewer1, u2.reviewerID AS Reviewer2, count(DISTINCT p) AS SharedProductCount
ORDER BY SharedProductCount DESC
LIMIT $limit;

Q15. Reviewers Who Write the Longest Reviews

Subcategory: T4.1 Ungrouped Node Degree Statistics by Label

This query ranks all reviewers by the average length of their review text, returning reviewer identity, review count, and average review length.

gsql
CREATE OR REPLACE QUERY amzn_q15() FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @review_count;
  SumAccum<UINT> @total_length;
  SumAccum<DOUBLE> @avg_review_length;

  reviewers = {amzn_Reviewer.*};

  wrote = SELECT u
          FROM reviewers:u -(AMZN_WROTE>)- amzn_Review:r
          ACCUM u.@review_count += 1,
                u.@total_length += length(r.reviewText)
          POST-ACCUM u.@avg_review_length = u.@total_length * 1.0 / u.@review_count;

  lengths = SELECT u
            FROM wrote:u
            ORDER BY u.@avg_review_length DESC, u.reviewerID ASC
            LIMIT 200;

  PRINT lengths[lengths.reviewerID AS ReviewerID,
                lengths.name AS ReviewerName,
                lengths.@review_count AS ReviewCount,
                lengths.@avg_review_length AS AvgReviewLength];
}

TigerGraph notes

Every reviewer, ranked by the average length of the reviews they wrote. Since 2026-10-19 the workload matches (u:Reviewer) instead of the :TopReviewer label the removed former Q18 (Most Active Reviewers) used to write. Cypher's size(r.reviewText) on a string is length() in GSQL.

The average is computed as total length / review count in POST-ACCUM, and the ranking happens in a separate SELECT. A first version that used an AvgAccum with ORDER BY/LIMIT in the same SELECT returned wrong averages on 4.2.4 (for reviewer A3S9TZ95GMLNH0, whose three reviews are 30079, 31883 and 712 characters long here, it printed ReviewCount 3 but AvgReviewLength 30981, the mean of only the first two).

ReviewerName is u.name as-is: the loader writes coalesce(reviewerName, "") for every Reviewer, so in Neo4j u.name is never null and coalesce(u.name, u.reviewerID) returns the stored name even when it is "". Ties on the average are broken by reviewerID so the 200 rows are reproducible.

Known difference: reviewText is stored with the loader's escape sequences (a newline is the two characters \n), so length() here is longer than Neo4j's size() by one character per escaped character (three per \xNN).

Equivalent query in GQL standard:

gql
MATCH (u:Reviewer)-[:WROTE]->(r:Review)
RETURN u.reviewerID AS ReviewerID,
    coalesce(u.name, u.reviewerID) AS ReviewerName,
    count(r) AS ReviewCount,
    avg(size(r.reviewText)) AS AvgReviewLength
ORDER BY AvgReviewLength DESC
LIMIT 200;

Q16. Products with the Most Reviews

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query finds the 200 products with the highest number of reviews, marks them as seed products, and returns each product's identifier, display title, and review count.

Not implemented in TigerGraph: this Cypher query writes a secondary label, which TigerGraph has no equivalent for. See the notes below.

TigerGraph notes

The Cypher query is not a read: after ranking products by review count it runs SET p:SeedProduct, attaching a second label that Q1 and Q29 later match on.

TigerGraph cannot express this, and the limitation is structural rather than syntactic:

  • A vertex belongs to exactly one vertex type, fixed at load time. There is no mechanism to attach a second type to a live vertex.
  • GSQL's UPDATE ... SET and the = assignment in POST-ACCUM write *attribute values* only. The data-modification statements are DELETE, INSERT INTO, and UPDATE/SET; none of them touches type or label metadata.
  • TAGs are the nearest analogue to Neo4j's multi-labelling, but they are applied through DDL and loading jobs and consumed via CREATE GRAPH ... AS <base>:<tag>. There is no tag-manipulation statement available inside a query body, and a tag-based graph is a separate graph rather than a runtime marking.
  • Adding a real is_seed_product attribute would work, but requires a SCHEMA_CHANGE job, so it is not the "identical schema" this file assumes. It would also make Q1/Q29 depend on Q16 having run first, which the Neo4j runner has to special-case in its run order.

Q1 and Q29 therefore recompute this ranking inline, which makes them self-contained and removes that ordering dependency. The ranking itself -- products by incoming review count, top 200 -- is exactly the counted block of amzn_q01, so measuring it separately would double-count the same work. ================================================================================

Equivalent query in GQL standard:

gql
MATCH (p:Product)<-[:REVIEWS]-(r:Review)
WITH p, count(r) AS ReviewCount
ORDER BY ReviewCount DESC
LIMIT 200
SET p:SeedProduct
RETURN p.asin AS ASIN, coalesce(p.title, p.asin) AS Product, ReviewCount;

Q17. Highly Rated Products with Many Reviews

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query finds products with at least 50 reviews and an average rating of at least 4.0, returning the most-reviewed high-quality products first.

gsql
CREATE OR REPLACE QUERY amzn_q17(INT min_reviews = 50,
                                 DOUBLE min_avg_rating = 4.0)
FOR GRAPH amazon SYNTAX v2 {
  AvgAccum @avg_rating;
  SumAccum<UINT> @review_count;
  MaxAccum<DOUBLE> @avg_rating_rounded;
  MaxAccum<STRING> @display_title;

  reviews = {amzn_Review.*};

  rated = SELECT p
          FROM reviews:r -(AMZN_REVIEWS>)- amzn_Product:p
          WHERE r.overall > 0
          ACCUM p.@avg_rating += r.overall,
                p.@review_count += 1
          POST-ACCUM p.@avg_rating_rounded += round(p.@avg_rating, 2),
                     p.@display_title +=
                       CASE WHEN p.title == "" THEN p.asin
                            ELSE p.title END
          HAVING p.@review_count >= min_reviews
             AND p.@avg_rating >= min_avg_rating
          ORDER BY p.@review_count DESC, p.@avg_rating DESC
          LIMIT 200;

  PRINT rated[rated.asin AS ASIN,
              rated.@display_title AS Product,
              rated.@avg_rating_rounded AS AvgRating,
              rated.@review_count AS ReviewCount];
}

TigerGraph notes

Equivalent query in GQL standard:

gql
MATCH (p:Product)<-[:REVIEWS]-(r:Review)
WHERE r.overall IS NOT NULL
WITH p, avg(r.overall) AS AvgRating, count(r) AS ReviewCount
WHERE ReviewCount >= 50 AND AvgRating >= 4.0
RETURN p.asin AS ASIN,
    coalesce(p.title, p.asin) AS Product,
    round(AvgRating, 2) AS AvgRating,
    ReviewCount
ORDER BY ReviewCount DESC, AvgRating DESC
LIMIT 200;

Q18. Products with the Most Images

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query counts product image neighbors for each product and returns the products with the largest number of attached images.

gsql
CREATE OR REPLACE QUERY amzn_q19() FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @image_count;
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};

  with_images = SELECT p
                FROM products:p -(AMZN_HAS_PRODUCT_IMAGE>)- amzn_Image:i
                ACCUM p.@image_count += 1
                POST-ACCUM p.@display_title +=
                  CASE WHEN p.title == "" THEN p.asin ELSE p.title END
                ORDER BY p.@image_count DESC
                LIMIT 200;

  PRINT with_images[with_images.asin AS ASIN,
                    with_images.@display_title AS Product,
                    with_images.@image_count AS ImageCount];
}

TigerGraph notes

Equivalent query in GQL standard:

gql
MATCH (p:Product)-[:HAS_PRODUCT_IMAGE]->(i:Image)
WITH p.asin AS ASIN, coalesce(p.title, p.asin) AS Product, count(i) AS ImageCount
RETURN ASIN, Product, ImageCount
ORDER BY ImageCount DESC
LIMIT 200;

Q19. Review Edge Rating Summary

Subcategory: T4.3 Ungrouped Edge Count and Property Statistics

Reviews are modeled as nodes between reviewers and products, so this query summarizes review-edge records and their rating properties.

gsql
CREATE OR REPLACE QUERY amzn_q20() FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @@review_count;
  AvgAccum @@avg_rating;
  MinAccum<INT> @@min_rating;
  MaxAccum<INT> @@max_rating;

  reviewers = {amzn_Reviewer.*};

  summary = SELECT r
            FROM reviewers:u -(AMZN_WROTE>)- amzn_Review:r
                             -(AMZN_REVIEWS>)- amzn_Product:p
            ACCUM @@review_count += 1,
                  @@avg_rating += r.overall,
                  @@min_rating += r.overall,
                  @@max_rating += r.overall;

  PRINT @@review_count AS ReviewCount,
        @@avg_rating AS AvgRating,
        @@min_rating AS MinRating,
        @@max_rating AS MaxRating;
}

TigerGraph notes

Counts one row per Reviewer->Review->Product path, as the Cypher MATCH does.

Equivalent query in GQL standard:

gql
MATCH (:Reviewer)-[:WROTE]->(r:Review)-[:REVIEWS]->(:Product)
RETURN count(r) AS ReviewCount,
       avg(r.overall) AS AvgRating,
       min(r.overall) AS MinRating,
       max(r.overall) AS MaxRating;

Q20. Category-Level Review Rating Summary

Subcategory: T4.4 Grouped Edge Count and Property Statistics

This query groups review records by product category and summarizes ratings within each category.

gsql
CREATE OR REPLACE QUERY amzn_q21() FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @review_count;
  AvgAccum @avg_rating;
  MaxAccum<DOUBLE> @avg_rating_rounded;

  reviews = {amzn_Review.*};

  by_category = SELECT c
                FROM reviews:r -(AMZN_REVIEWS>)- amzn_Product:p
                               -(AMZN_IN_CATEGORY>)- amzn_Category:c
                ACCUM c.@review_count += 1,
                      c.@avg_rating += r.overall
                POST-ACCUM c.@avg_rating_rounded +=
                             round(c.@avg_rating, 2)
                ORDER BY c.@review_count DESC
                LIMIT 100;

  PRINT by_category[by_category.name AS Category,
                    by_category.@review_count AS ReviewCount,
                    by_category.@avg_rating_rounded AS AvgRating];
}

TigerGraph notes

The Cypher pattern is (c)<-[:IN_CATEGORY]-(p)<-[:REVIEWS]-(r); starting from amzn_Review makes both hops forward.

Cypher's GROUP BY on c.name is done by accumulating onto the amzn_Category vertices rather than with SELECT ... INTO ... GROUP BY, so that round() can be applied in POST-ACCUM -- wrapping an aggregate function inside the table form's select list is not a construct this file relies on elsewhere.

Equivalent query in GQL standard:

gql
MATCH (c:Category)<-[:IN_CATEGORY]-(p:Product)<-[:REVIEWS]-(r:Review)
RETURN c.name AS Category,
       count(r) AS ReviewCount,
       round(avg(r.overall), 2) AS AvgRating
ORDER BY ReviewCount DESC
LIMIT $limit;

Q21. Co-Buy Path Counts by Length

Subcategory: T4.5 Path Counts by Length

This query counts bounded ALSO_BUY paths from a selected product and groups them by path length.

gsql
CREATE OR REPLACE QUERY amzn_q22(STRING seed_asin = "0000031895")
FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @paths0;
  SumAccum<UINT> @paths1;
  SumAccum<UINT> @paths2;
  SumAccum<UINT> @paths3;
  SumAccum<UINT> @@path_count_1;
  SumAccum<UINT> @@path_count_2;
  SumAccum<UINT> @@path_count_3;

  products = {amzn_Product.*};
  start = SELECT p FROM products:p WHERE p.asin == seed_asin
          POST-ACCUM p.@paths0 += 1;

  hop1 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:t
         ACCUM t.@paths1 += s.@paths0;
  stats1 = SELECT t FROM hop1:t ACCUM @@path_count_1 += t.@paths1;

  hop2 = SELECT t FROM hop1:s -(AMZN_ALSO_BUY>)- amzn_Product:t
         ACCUM t.@paths2 += s.@paths1;
  stats2 = SELECT t FROM hop2:t ACCUM @@path_count_2 += t.@paths2;

  hop3 = SELECT t FROM hop2:s -(AMZN_ALSO_BUY>)- amzn_Product:t
         ACCUM t.@paths3 += s.@paths2;
  stats3 = SELECT t FROM hop3:t ACCUM @@path_count_3 += t.@paths3;

  PRINT 1 AS PathLength, @@path_count_1 AS PathCount,
        hop1.size() AS DistinctTargetCount;
  PRINT 2 AS PathLength, @@path_count_2 AS PathCount,
        hop2.size() AS DistinctTargetCount;
  PRINT 3 AS PathLength, @@path_count_3 AS PathCount,
        hop3.size() AS DistinctTargetCount;
}

TigerGraph notes

Walk counts, not simple-path counts: @paths<n> propagates the number of distinct walks reaching each product, and the distinct-target count is the size of each hop's vertex set.

Equivalent query in GQL standard:

gql
MATCH path = (:Product {asin: $asin})-[:ALSO_BUY]->{1,3}(target:Product)
RETURN length(path) AS PathLength,
       count(path) AS PathCount,
       count(DISTINCT target) AS DistinctTargetCount
ORDER BY PathLength;

Q22. Co-Buy Path Counts by Length and Endpoint Category

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

This query groups co-buy paths by path length and by the category of the endpoint product.

gsql
CREATE OR REPLACE QUERY amzn_q23(STRING seed_asin = "0000031895")
FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @paths0;
  SumAccum<UINT> @paths1;
  SumAccum<UINT> @paths2;
  SumAccum<UINT> @paths3;
  MapAccum<STRING, SumAccum<UINT>> @@path_counts_1;
  MapAccum<STRING, SumAccum<UINT>> @@path_counts_2;
  MapAccum<STRING, SumAccum<UINT>> @@path_counts_3;
  MapAccum<STRING, SumAccum<UINT>> @@target_counts_1;
  MapAccum<STRING, SumAccum<UINT>> @@target_counts_2;
  MapAccum<STRING, SumAccum<UINT>> @@target_counts_3;

  products = {amzn_Product.*};
  start = SELECT p FROM products:p WHERE p.asin == seed_asin
          POST-ACCUM p.@paths0 += 1;

  hop1 = SELECT t FROM start:s -(AMZN_ALSO_BUY>)- amzn_Product:t
         ACCUM t.@paths1 += s.@paths0;
  stats1 = SELECT t FROM hop1:t -(AMZN_IN_CATEGORY>)- amzn_Category:c
           ACCUM @@path_counts_1 += (c.name -> t.@paths1),
                 @@target_counts_1 += (c.name -> 1);

  hop2 = SELECT t FROM hop1:s -(AMZN_ALSO_BUY>)- amzn_Product:t
         ACCUM t.@paths2 += s.@paths1;
  stats2 = SELECT t FROM hop2:t -(AMZN_IN_CATEGORY>)- amzn_Category:c
           ACCUM @@path_counts_2 += (c.name -> t.@paths2),
                 @@target_counts_2 += (c.name -> 1);

  hop3 = SELECT t FROM hop2:s -(AMZN_ALSO_BUY>)- amzn_Product:t
         ACCUM t.@paths3 += s.@paths2;
  stats3 = SELECT t FROM hop3:t -(AMZN_IN_CATEGORY>)- amzn_Category:c
           ACCUM @@path_counts_3 += (c.name -> t.@paths3),
                 @@target_counts_3 += (c.name -> 1);

  PRINT @@path_counts_1 AS PathCounts1,
        @@target_counts_1 AS EndpointProductCounts1,
        @@path_counts_2 AS PathCounts2,
        @@target_counts_2 AS EndpointProductCounts2,
        @@path_counts_3 AS PathCounts3,
        @@target_counts_3 AS EndpointProductCounts3;
}

TigerGraph notes

Q21 broken down by the endpoint's category. A product in several categories contributes to each, matching the Cypher MATCH.

Equivalent query in GQL standard:

gql
MATCH path = (:Product {asin: $asin})-[:ALSO_BUY]->{1,3}(target:Product)-[:IN_CATEGORY]->(c:Category)
RETURN length(path) AS PathLength,
       c.name AS EndpointCategory,
       count(path) AS PathCount,
       count(DISTINCT target) AS EndpointProductCount
ORDER BY PathLength, PathCount DESC
LIMIT $limit;

Q23. Products with Both Many Reviews and Images

Subcategory: T4.7 Union and Intersection

This query intersects products with many reviews and products with multiple images to find items with both strong review and media signals.

gsql
CREATE OR REPLACE QUERY amzn_q24(INT min_reviews = 50, INT min_images = 5)
FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @review_count;
  SumAccum<UINT> @image_count;
  MaxAccum<STRING> @display_title;

  reviews = {amzn_Review.*};

  well_reviewed = SELECT p
                  FROM reviews:r -(AMZN_REVIEWS>)- amzn_Product:p
                  ACCUM p.@review_count += 1
                  HAVING p.@review_count >= min_reviews;

  answer = SELECT p
           FROM well_reviewed:p -(AMZN_HAS_PRODUCT_IMAGE>)- amzn_Image:i
           ACCUM p.@image_count += 1
           POST-ACCUM p.@display_title +=
             CASE WHEN p.title == "" THEN p.asin ELSE p.title END
           HAVING p.@image_count >= min_images
           ORDER BY p.@image_count DESC
           LIMIT 100;

  PRINT answer[answer.asin AS ASIN,
               answer.@display_title AS Product,
               answer.@image_count AS ImageCount];
}

TigerGraph notes

Equivalent query in GQL standard:

gql
MATCH (p:Product)<-[:REVIEWS]-(r:Review)
WITH p, count(r) AS ReviewCount
WHERE ReviewCount >= $minReviews
WITH collect(p) AS ReviewedProducts
MATCH (p:Product)-[:HAS_PRODUCT_IMAGE]->(i:Image)
WITH ReviewedProducts, p, count(i) AS ImageCount
WHERE p IN ReviewedProducts AND ImageCount >= $minImages
RETURN p.asin AS ASIN, coalesce(p.title, p.asin) AS Product, ImageCount
ORDER BY ImageCount DESC
LIMIT $limit;

Q24. Top Products by Co-Buy Degree Centrality

Subcategory: T5.1 Degree Centrality

This query computes degree centrality directly by counting products connected through incoming or outgoing co-buy edges.

gsql
CREATE OR REPLACE QUERY amzn_q25() FOR GRAPH amazon SYNTAX v2 {
  SetAccum<VERTEX<amzn_Product>> @neighbors;
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};

  topology = SELECT a
             FROM products:a -(AMZN_ALSO_BUY>|AMZN_ALSO_BUY_REVERSE>)-
                  amzn_Product:b
             ACCUM a.@neighbors += b;

  scores = SELECT p FROM products:p
           POST-ACCUM p.@display_title +=
             CASE WHEN p.title == "" THEN p.asin ELSE p.title END
           ORDER BY p.@neighbors.size() DESC
           LIMIT 100;

  PRINT scores[scores.asin AS ASIN,
               scores.@display_title AS Product,
               scores.@neighbors.size() AS CoBuyDegree];
}

TigerGraph notes

The Cypher pattern is undirected -[:ALSO_BUY]-. The declared reverse edge lets this traverse both directions in one pattern, so each product accumulates only its own neighbours -- no cross-vertex writes, which matters on a cluster. count(DISTINCT neighbor) is the SetAccum size.

Equivalent query in GQL standard:

gql
MATCH (p:Product)-[:ALSO_BUY]-(neighbor:Product)
RETURN p.asin AS ASIN,
       coalesce(p.title, p.asin) AS Product,
       count(DISTINCT neighbor) AS CoBuyDegree
ORDER BY CoBuyDegree DESC
LIMIT $limit;

Q25. PageRank on the Co-Buy Product Graph

Subcategory: T5.2 PageRank

This query projects the product co-buy graph and runs PageRank to rank influential products.

gsql
CALL GDBMS_ALGO.centrality.pagerank(
  "amzn_Product", "AMZN_ALSO_BUY", 0.0000001, 20, 0.85, 100, TRUE, "", "", FALSE
)

TigerGraph notes

Replaces the gds.graph.project + gds.pageRank.stream pair. The Neo4j projection is ALSO_BUY with orientation NATURAL, so the directed edge type is passed as-is.

Signature: (vertex type, edge type, tolerance, max iterations, damping factor, top-k to print, print to console, result attribute, file path, display edges).

The source gds.pageRank.stream call passes only the graph name and supplies NO tuning parameters, so it runs on Neo4j GDS's own defaults. The values below are those defaults written out explicitly, because the TigerGraph packaged query requires them positionally -- they are not values the source specified. Neo4j GDS defaults: maxIterations=20, tolerance=1e-7, dampingFactor=0.85.

Output: the top 100 Vertex_ID/score pairs, where Vertex_ID is amzn_Product.asin; the packaged query does not also return the title.

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

Q26. Weakly Connected Components of the Co-Buy Graph

Subcategory: T5.3 Connected Components / Weakly Connected Components

This query treats co-buy links as undirected and reports the largest weakly connected components.

gsql
CALL GDBMS_ALGO.community.wcc(
  ["amzn_Product"], ["AMZN_ALSO_BUY", "AMZN_ALSO_BUY_REVERSE"], 0, TRUE, "", ""
)

TigerGraph notes

Neo4j projects ALSO_BUY with orientation UNDIRECTED. Passing both AMZN_ALSO_BUY and its reverse edge makes the traversal genuinely undirected rather than relying on WCC's own symmetry.

print_limit 0 suppresses the per-vertex assignment list while keeping the component-size map. Sort that map by value descending and keep 100 entries to reproduce ORDER BY ComponentSize DESC LIMIT 100.

Verified on TigerGraph 4.2.4: with print_results the component-size map of the full co-buy graph is about 373 MB, more than the 32 MB response limit of the GSQL template endpoint, so the call must write its assignments with file_path and be aggregated outside the query.

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

Q27. Strongly Connected Components of the Co-Buy Graph

Subcategory: T5.4 Strongly Connected Components

This query runs strongly connected components on the directed co-buy product graph.

gsql
CALL GDBMS_ALGO.community.scc_small_world(
  "amzn_Product", "AMZN_ALSO_BUY", "AMZN_ALSO_BUY_REVERSE", 100000, TRUE
)

TigerGraph notes

Here the Neo4j projection is NATURAL, so this is a genuine directed SCC.

Signature: (vertex type, forward edge type, reverse edge type, max iterations, print to console). SCC needs to walk edges in both directions, and the declared reverse edge supplies the backward direction properly -- so unlike the Yelp workload this does not have to pass the forward type twice. Sort the returned component-size map descending and keep 100 entries.

Verified on TigerGraph 4.2.4: the printed component-count map is about 310 MB, more than the 32 MB response limit of the GSQL template endpoint, and this template has no file_path option.

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

Q28. Louvain Communities in the Co-Buy Graph

Subcategory: T5.5 Community Detection

This query detects communities of products in the undirected co-buy graph using Louvain.

gsql
CALL GDBMS_ALGO.community.louvain(
  ["amzn_Product"], ["AMZN_ALSO_BUY", "AMZN_ALSO_BUY_REVERSE"],
  "", 10, "", "", TRUE
)

TigerGraph notes

Neo4j projects ALSO_BUY with orientation UNDIRECTED. TigerGraph's packaged Louvain only treats a graph as undirected when reverse edges are present, which is why AMZN_ALSO_BUY declares one: passing both directions reproduces the source projection faithfully. Without the reverse edge this query could not be translated -- the Yelp workload skips its Louvain for exactly that reason.

Signature: (vertex type set, edge type set, weight attribute, max iterations, result attribute, file path, print to console). ALSO_BUY is unweighted, so the weight attribute is left empty; max iterations is TigerGraph's default of 10. Sort the returned community-size map descending and keep 100 entries.

Verified on TigerGraph 4.2.4: the installed GDBMS_ALGO.community.louvain template has the signature (v_type_set, e_type_set, weight_attribute, total_passes_count, maximum_iteration, total_batch_count, result_attribute, file_path) and prints nothing. Its patterns are written -(e_type_set:e)-, which SYNTAX V2 accepts only for undirected edge types, so with the directed AMZN_ALSO_BUY it fails to compile (The query specifies V2 syntax but uses V1 here). Running Louvain here needs an undirected co-buy edge type or a modified copy of the template.

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

Q29. Second-Hop Co-Buy Influence (Ripple Effect)

Subcategory: T2.1 Node Filtering (Single Node Paths); T3.6 Line Pattern; T4.1 Ungrouped Node Degree Statistics by Label

This query starts from the most-reviewed seed product, follows two ALSO_BUY hops, and ranks second-hop products by how many first-hop neighbors connect back to them.

gsql
CREATE OR REPLACE QUERY amzn_q30() FOR GRAPH amazon SYNTAX v2 {
  TYPEDEF TUPLE<UINT review_count, STRING asin> SeedRow;

  SumAccum<UINT> @review_count;
  HeapAccum<SeedRow>(200, review_count DESC, asin ASC) @@seed_heap;
  SetAccum<STRING> @@seed_asins;
  STRING top_seed_asin = "";
  OrAccum @is_seed;
  SetAccum<VERTEX<amzn_Product>> @first_hops;
  MaxAccum<STRING> @display_title;

  products = {amzn_Product.*};

  counted = SELECT p
            FROM amzn_Review:r -(AMZN_REVIEWS>)- amzn_Product:p
            ACCUM p.@review_count += 1
            POST-ACCUM @@seed_heap += SeedRow(p.@review_count, p.asin);

  /* top() is the most-reviewed product; read it before draining the heap. */
  top_seed_asin = @@seed_heap.top().asin;

  WHILE @@seed_heap.size() > 0 DO
    @@seed_asins += @@seed_heap.top().asin;
    @@seed_heap.pop();
  END;

  mark_seeds = SELECT p FROM products:p
               WHERE @@seed_asins.contains(p.asin)
               POST-ACCUM p.@is_seed += TRUE;

  seed = SELECT p FROM products:p WHERE p.asin == top_seed_asin;

  first_hop = SELECT nbr
              FROM seed:s -(AMZN_ALSO_BUY>)- amzn_Product:nbr
              WHERE NOT nbr.@is_seed;

  second_hop = SELECT t
               FROM first_hop:m -(AMZN_ALSO_BUY>)- amzn_Product:t
               WHERE t.asin != top_seed_asin
               ACCUM t.@first_hops += m
               POST-ACCUM t.@display_title +=
                 CASE WHEN t.title == "" THEN t.asin ELSE t.title END
               ORDER BY t.@first_hops.size() DESC
               LIMIT 200;

  PRINT second_hop[second_hop.asin AS ASIN,
                   second_hop.@display_title AS SecondHopProduct,
                   second_hop.@first_hops.size() AS SharedNeighbors];
}

TigerGraph notes

The Cypher version takes the single most-reviewed :SeedProduct, then excludes first-hop products that are themselves :SeedProduct. Both the top-1 product and the full top-200 seed set are needed, so Q16's ranking is recomputed inline and used twice.

Equivalent query in GQL standard:

gql
MATCH (seed:SeedProduct)<-[:REVIEWS]-(review:Review)
WITH seed, count(review) AS ReviewCount
ORDER BY ReviewCount DESC
LIMIT 1
MATCH (seed)-[:ALSO_BUY]->(firstHop:Product)
WHERE NOT firstHop:SeedProduct
MATCH (firstHop)-[:ALSO_BUY]->(secondHop:Product)
WHERE secondHop <> seed
RETURN secondHop.asin AS ASIN,
    coalesce(secondHop.title, secondHop.asin) AS SecondHopProduct,
    count(DISTINCT firstHop) AS SharedNeighbors
ORDER BY SharedNeighbors DESC
LIMIT 200;

Q30. Are Reviews with More Images More Helpful?

Subcategory: T2.1 Node Filtering (Single Node Paths); T4.2 Grouped Node Degree Statistics by Label

This query finds highly helpful reviews, counts any images attached to each review, and returns reviewer, rating, helpful-vote, and image-count fields for comparison.

gsql
CREATE OR REPLACE QUERY amzn_q31(INT min_votes = 1)
FOR GRAPH amazon SYNTAX v2 {
  SumAccum<UINT> @image_count;

  reviews = {amzn_Review.*};

  voted = SELECT r FROM reviews:r WHERE r.vote >= min_votes;

  counted = SELECT r
            FROM voted:r -(AMZN_HAS_REVIEW_IMAGE>)- amzn_ReviewImage:ri
            ACCUM r.@image_count += 1;

  answer = SELECT r FROM voted:r
           ORDER BY r.vote DESC
           LIMIT 200;

  PRINT answer[answer.reviewerID AS ReviewerID,
               answer.reviewerID AS ReviewerName,
               answer.overall AS Rating,
               answer.vote AS HelpfulVotes,
               answer.@image_count AS ImageCount];
}

TigerGraph notes

DEVIATION: the Cypher OPTIONAL MATCH traverses [:HAS_IMAGE] to :ReviewImage, but the loader creates HAS_REVIEW_IMAGE, so in Neo4j that pattern never binds and ImageCount is always 0 -- which makes the query unable to answer the question in its own title. The published edge statistics report 6,840,462 HAS_REVIEW_IMAGE edges, so the data is there and only the type name is wrong. This uses AMZN_HAS_REVIEW_IMAGE and so returns real counts.

r.vote IS NOT NULL becomes r.vote > 0: the importer writes null for a missing vote, which becomes 0 under TigerGraph's non-nullable INT.

The OPTIONAL MATCH is reproduced by selecting the qualifying reviews first and then accumulating image counts over them, so reviews with no image are kept with ImageCount 0. Both ReviewerID and ReviewerName come from amzn_Review.reviewerID, as in the Cypher source.

Equivalent query in GQL standard:

gql
MATCH (r:Review)
WHERE r.vote IS NOT NULL
OPTIONAL MATCH (r)-[:HAS_IMAGE]->(ri:ReviewImage)
WITH r.reviewerID AS ReviewerID,
    coalesce(r.reviewerID, "Unknown") AS ReviewerName,
    r.overall AS Rating,
    r.vote AS HelpfulVotes,
    count(ri) AS ImageCount
RETURN ReviewerID, ReviewerName, Rating, HelpfulVotes, ImageCount
ORDER BY HelpfulVotes DESC
LIMIT 200;