Schema Diagram and Graph Statistics
Node Statistics
| Node Label | Count |
|---|---|
| Product | 23,643,222 |
| Category | 2,487,388 |
| Review | 230,139,802 |
| Image | 17,081,240 |
| ReviewImage | 6,538,802 |
| Reviewer | 43,249,276 |
Edge Statistics
| Relationship | Count |
|---|---|
| (:Review)-[:REVIEWS]->(:Product) | 230,139,802 |
| (:Product)-[:HAS_PRODUCT_IMAGE]->(:Image) | 22,531,444 |
| (:Product)-[:ALSO_BUY]->(:Product) | 97,123,542 |
| (:Reviewer)-[:WROTE]->(:Review) | 230,139,802 |
| (:Review)-[:HAS_REVIEW_IMAGE]->(:Image) | 6,840,462 |
| (:Product)-[:IN_CATEGORY]->(:Category) | 62,832,310 |
| (:Product)-[:ALSO_VIEW]->(:Product) | 92,658,319 |
Disk space: 173.09 GiB
Total import time: 56,061.585 Seconds
Data Loading
Overview
Data import is handled in two stages: node creation followed by sequential edge ingestion. We put all
the JSON files under the data import directory of Neo4j. This directory can be set in the
neo4j.conf file using the parameter dbms.directories.import.
For the implementation of Amazon Open Dataset on Neo4j, we used the original files downloaded from their website. We will read the JSON files unpacked from the downloaded file to complete node and edge imports.
Create Indices
To speed up the import process and future queries, we create B+-tree indices for some node labels.
CREATE CONSTRAINT IF NOT EXISTS FOR (p:Product) REQUIRE p.asin IS UNIQUE; CREATE CONSTRAINT IF NOT EXISTS FOR (i:Image) REQUIRE i.url IS UNIQUE; CREATE CONSTRAINT IF NOT EXISTS FOR (u:Reviewer) REQUIRE u.reviewerID IS UNIQUE; CREATE CONSTRAINT IF NOT EXISTS FOR (c:Category) REQUIRE c.name IS UNIQUE; CREATE CONSTRAINT IF NOT EXISTS FOR (ri:ReviewImage) REQUIRE ri.url IS UNIQUE;
Define Product Related Nodes and Edges (16,365.674 seconds)
First we define the Product nodes, Image nodes,
Category nodes, HAS_PRODUCT_IMAGE edges, ALSO_BUY edges,
ALSO_VIEW edges, and IN_CATEGORY edges.
Note that in the following code block, the file path in the second line is a placeholder. You should replace "path_to_folder" to a specific Amazon product category being listed in the original dataset, and "meta_file" with the actual metadata json file name. The file name will start with "meta".
CALL apoc.periodic.iterate( 'CALL apoc.load.json("file:///amazon/amazon_data/path_to_folder/meta_file") YIELD value RETURN value', ' MERGE (p:Product {asin: value.asin}) // Parse price range WITH p, value, split(REPLACE(value.price, "$", ""), " - ") AS priceRange SET p.title = coalesce(value.title, ""), p.brand = coalesce(value.brand, ""), p.description = apoc.text.join(value.description, ", "), p.price_min = toFloat(priceRange[0]), p.price_max = CASE WHEN size(priceRange) > 1 THEN toFloat(priceRange[1]) ELSE toFloat(priceRange[0]) END // Handle image URLs WITH p, value FOREACH (_ IN CASE WHEN value.imageURL IS NOT NULL AND value.imageURLHighRes IS NOT NULL AND size(value.imageURL) = size(value.imageURLHighRes) THEN [1] ELSE [] END | FOREACH (pair IN apoc.coll.zip(value.imageURL, value.imageURLHighRes) | MERGE (img:Image {url: pair[0]}) SET img.highRes = pair[1] MERGE (p)-[:HAS_PRODUCT_IMAGE]->(img) ) ) // ALSO_BUY edges FOREACH (related_asin IN coalesce(value.also_buy, []) | MERGE (r:Product {asin: related_asin}) MERGE (p)-[:ALSO_BUY]->(r) ) // ALSO_VIEW edges FOREACH (view_asin IN coalesce(value.also_view, []) | MERGE (v:Product {asin: view_asin}) MERGE (p)-[:ALSO_VIEW]->(v) ) // CATEGORY nodes and IN_CATEGORY edges FOREACH (cat IN coalesce(value.category, []) | MERGE (c:Category {name: apoc.text.replace(cat, "&", "&")}) MERGE (p)-[:IN_CATEGORY]->(c) ) ', {batchSize: 5000, iterateList: true, parallel: false} )
Product-related node and edge load time by category
| Category | Load Time (seconds) |
|---|---|
| Gift Cards | 4.931 |
| Magazine Subscriptions | 5.392 |
| Prime Pantry | 13.717 |
| Luxury Beauty | 20.051 |
| All Beauty | 20.657 |
| Digital Music | 21.487 |
| Software | 15.768 |
| Amazon Fashion | 99.480 |
| Video Games | 106.527 |
| Movies and TV | 207.394 |
| Kindle Store | 543.526 |
| Grocery and Gourmet Food | 281.312 |
| Industrial and Scientific | 125.058 |
| CDs and Vinyl | 331.992 |
| Appliances | 34.394 |
| Arts, Crafts and Sewing | 281.256 |
| Musical Instruments | 108.023 |
| Pet Supplies | 227.203 |
| Cell Phones and Accessories | 438.375 |
| Books | 2,665.430 |
| Patio, Lawn and Garden | 243.615 |
| Office Products | 374.614 |
| Toys and Games | 698.900 |
| Clothing, Shoes and Jewelry | 3,592.958 |
| Electronics | 1,504.732 |
| Tools and Home Improvement | 866.487 |
| Home and Kitchen | 1,402.899 |
| Automotive | 915.697 |
| Sports and Outdoors | 1,213.799 |
Define Review Related Nodes and Edges (39,695.911 seconds)
Then we define the Review nodes, ReviewImage nodes, Reviewer
nodes, HAS_REVIEW_IMAGE edges, REVIEWS edges, and WROTE
edges.
Note that in the following code block, the file path in the second line is a placeholder. You should replace "path_to_folder" to a specific Amazon product category being listed in the original dataset, and "review_file" with the actual review json file name. The file name will simply be "[CATEGORY].json".
CALL apoc.periodic.iterate( ' CALL apoc.load.json("file:///amazon/amazon_data/path_to_folder/review_file") YIELD value RETURN value ', ' CREATE (r:Review { reviewerID: value.reviewerID, asin: value.asin, unixReviewTime: value.unixReviewTime }) SET r.reviewDatetime = datetime({epochMillis: apoc.date.parse(value.reviewTime, "ms", "MM dd, yyyy")}), r.summary = coalesce(value.summary, ""), r.reviewText = coalesce(value.reviewText, ""), r.verified = value.verified, r.vote = toInteger(value.vote), r.overall = toInteger(value.overall) WITH r, value MERGE (p:Product {asin: value.asin}) MERGE (r)-[:REVIEWS]->(p) WITH r, value MERGE (u:Reviewer {reviewerID: value.reviewerID}) SET u.name = coalesce(value.reviewerName, "") MERGE (u)-[:WROTE]->(r) WITH r, value FOREACH (img_url IN coalesce(value.image, []) | MERGE (img:ReviewImage {url: img_url}) MERGE (r)-[:HAS_REVIEW_IMAGE]->(img) ) ', {batchSize: 1000, iterateList: true, parallel: false} )
Review-related node and edge load time by category
| Category | Load Time (seconds) |
|---|---|
| Magazine Subscriptions | 6.614 |
| Gift Cards | 10.147 |
| All Beauty | 24.967 |
| Prime Pantry | 31.382 |
| Appliances | 42.769 |
| Luxury Beauty | 39.892 |
| Software | 34.250 |
| Amazon Fashion | 62.413 |
| Digital Music | 115.677 |
| Industrial and Scientific | 143.358 |
| Musical Instruments | 115.777 |
| Arts, Crafts and Sewing | 230.586 |
| Video Games | 197.080 |
| Grocery and Gourmet Food | 339.453 |
| Patio, Lawn and Garden | 372.680 |
| Office Products | 477.029 |
| Pet Supplies | 513.814 |
| Automotive | 717.608 |
| CDs and Vinyl | 392.255 |
| Toys and Games | 889.196 |
| Kindle Store | 561.933 |
| Tools and Home Improvement | 957.439 |
| Cell Phones and Accessories | 1,074.606 |
| Movies and TV | 1,045.598 |
| Sports and Outdoors | 1,590.900 |
| Home and Kitchen | 3,165.101 |
| Electronics | 4,249.834 |
| Clothing, Shoes and Jewelry | 7,167.608 |
| Books | 15,125.945 |
Queries
Select a query below to view its logic and description.
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.
MATCH (seed:SeedProduct)-[:ALSO_BUY]->(other:Product) WHERE NOT other:SeedProduct // exclude seeds themselves RETURN other.asin AS ASIN, coalesce(other.title, other.asin) AS CoBuyProduct, count(DISTINCT seed) AS LinkedSeeds ORDER BY LinkedSeeds DESC LIMIT 200;
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.
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;
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.
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;
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.
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;
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.
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;
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.
MATCH p = (:Product {asin: $sourceAsin})-[:ALSO_BUY*1..4]->(:Product {asin: $targetAsin}) RETURN count(p) > 0 AS reachable LIMIT 1;
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.
MATCH p = shortestPath((src:Product {asin: $sourceAsin})-[:ALSO_BUY*1..6]->(dst:Product {asin: $targetAsin})) RETURN [n IN nodes(p) | n.asin] AS ProductPath, length(p) AS HopCount;
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.
MATCH p = (src:Product {asin: $sourceAsin})-[:ALSO_BUY*1..3]->(dst:Product {asin: $targetAsin}) WHERE all(n IN nodes(p) WHERE single(m IN nodes(p) WHERE m = n)) RETURN [n IN nodes(p) | n.asin] AS ProductPath, length(p) AS HopCount ORDER BY HopCount LIMIT $limit;
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.
MATCH p = (:Product {asin: $sourceAsin})-[:ALSO_BUY*1..3]->(:Product {asin: $targetAsin}) WITH p, reduce(total = 0.0, n IN nodes(p) | total + coalesce(n.price, 0.0)) AS PathPrice RETURN [n IN nodes(p) | n.asin] AS ProductPath, length(p) AS HopCount, PathPrice ORDER BY PathPrice DESC LIMIT $k;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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.
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;
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 gds.graph.project( 'AmazonCoBuyPageRank', 'Product', {ALSO_BUY: {type: 'ALSO_BUY', orientation: 'NATURAL'}} ); CALL gds.pageRank.stream('AmazonCoBuyPageRank') YIELD nodeId, score RETURN gds.util.asNode(nodeId).asin AS ASIN, gds.util.asNode(nodeId).title AS Product, score ORDER BY score DESC LIMIT $limit;
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 gds.graph.project( 'AmazonCoBuyWcc', 'Product', {ALSO_BUY: {type: 'ALSO_BUY', orientation: 'UNDIRECTED'}} ); CALL gds.wcc.stream('AmazonCoBuyWcc') YIELD nodeId, componentId RETURN componentId, count(*) AS ComponentSize ORDER BY ComponentSize DESC LIMIT $limit;
Q27. Strongly Connected Components of the Co-Buy Graph
This query runs strongly connected components on the directed co-buy product graph.
CALL gds.graph.project( 'AmazonCoBuyScc', 'Product', {ALSO_BUY: {type: 'ALSO_BUY', orientation: 'NATURAL'}} ); CALL gds.scc.stream('AmazonCoBuyScc') YIELD nodeId, componentId RETURN componentId, count(*) AS ComponentSize ORDER BY ComponentSize DESC LIMIT $limit;
Q28. Louvain Communities in the Co-Buy Graph
This query detects communities of products in the undirected co-buy graph using Louvain.
CALL gds.graph.project( 'AmazonCoBuyLouvain', 'Product', {ALSO_BUY: {type: 'ALSO_BUY', orientation: 'UNDIRECTED'}} ); CALL gds.louvain.stream('AmazonCoBuyLouvain') YIELD nodeId, communityId RETURN communityId, count(*) AS CommunitySize ORDER BY CommunitySize DESC LIMIT $limit;
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.
// Step 1: Pick the single most reviewed SeedProduct MATCH (seed:SeedProduct) WITH seed, COUNT { (seed)<-[:REVIEWS]-() } AS ReviewCount ORDER BY ReviewCount DESC LIMIT 1 // Step 2: First-hop co-buy neighbors MATCH (seed)-[:ALSO_BUY]->(firstHop:Product) WHERE NOT firstHop:SeedProduct // exclude seed set // Step 3: Second-hop neighbors (co-buys of firstHop) 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;
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.
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;
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;