Schema Diagram and Graph Statistics
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.
USE GLOBAL
Vertex Definitions
Then we define the Product, Review, Reviewer, Category, Image and ReviewImage vertices. Attribute names match the Neo4j property names exactly.
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.
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.
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.
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.
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.
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.
Q1. People Who Bought This Also Bought (Seed Expansion)
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.
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:
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
This query expands from a selected product through two outgoing ALSO_BUY hops and
returns distinct second-hop products.
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:
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
This query returns products exactly two ALSO_BUY hops from a selected product,
excluding direct one-hop recommendations.
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:
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
This query filters one-hop co-buy neighbors to products whose review average meets a minimum rating threshold.
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:
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
Reviews are modeled as nodes between reviewers and products, so this query filters review-edge records by rating and helpful-vote properties.
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:
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
This query tests whether one product can reach another product through up to four
ALSO_BUY hops.
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:
MATCH p = ANY (:Product {asin: $sourceAsin})-[:ALSO_BUY]->{1,4}(:Product {asin: $targetAsin}) RETURN true AS reachable;
Q7. Shortest Co-Buy Path Between Products
This query finds the shortest directed co-buy path between two products within a bounded depth.
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:
MATCH p = SHORTEST (:Product {asin: $sourceAsin})-[:ALSO_BUY]->{1,6}(:Product {asin: $targetAsin}) RETURN p;
Q8. Simple Co-Buy Paths up to Three Hops
This query enumerates acyclic co-buy paths between two products up to three hops.
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:
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
This query ranks bounded co-buy paths by the sum of product prices along each path.
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:
MATCH p = (:Product {asin: $sourceAsin})-[:ALSO_BUY]->{1,3}(:Product {asin: $targetAsin}) RETURN p LIMIT $k;
Q10. Co-Buy Product Triangles
This query finds directed triangles in the product co-buy graph.
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:
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
This query searches for four-product cycles formed by directed ALSO_BUY
relationships, where the second and fourth products must be different.
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:
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
This query finds star-like products that point to many co-buy recommendation neighbors.
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:
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
This query treats co-buy links as undirected and finds four products where every pair is
connected by an ALSO_BUY relationship.
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:
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
This query joins reviewers through products they have both reviewed.
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:
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
This query ranks all reviewers by the average length of their review text, returning reviewer identity, review count, and average review length.
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:
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
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.
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 ... SETand 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_productattribute 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:
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
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.
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:
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
This query counts product image neighbors for each product and returns the products with the largest number of attached images.
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:
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
Reviews are modeled as nodes between reviewers and products, so this query summarizes review-edge records and their rating properties.
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:
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
This query groups review records by product category and summarizes ratings within each category.
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:
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
This query counts bounded ALSO_BUY paths from a selected product and groups them by
path length.
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:
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
This query groups co-buy paths by path length and by the category of the endpoint product.
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:
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
This query intersects products with many reviews and products with multiple images to find items with both strong review and media signals.
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:
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
This query computes degree centrality directly by counting products connected through incoming or outgoing co-buy edges.
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:
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
This query projects the product co-buy graph and runs PageRank to rank influential products.
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.
Q26. Weakly Connected Components of the Co-Buy Graph
This query treats co-buy links as undirected and reports the largest weakly connected components.
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.
Q27. Strongly Connected Components of the Co-Buy Graph
This query runs strongly connected components on the directed co-buy product graph.
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.
Q28. Louvain Communities in the Co-Buy Graph
This query detects communities of products in the undirected co-buy graph using Louvain.
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.
Q29. Second-Hop Co-Buy Influence (Ripple Effect)
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.
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:
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?
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.
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:
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;