Similarity Demo

Customer & Products Guide

In this example, I will walk through how Neo4j can be used to understand what products a customer can and cannot access due to which documents they have signed. In addition, we can also review when documents need to be resigned as they are about to expire.

Clean Up Environment.

// Delete EVERYTHING
MATCH (n) DETACH DELETE n;
					

Load Demo Data.

// Product
MERGE (p1:Product {name: "Product 1"})
MERGE (p2:Product {name: "Product 2"})
MERGE (p3:Product {name: "Product 3"})

// Documents
MERGE (d1:Document {name: "Document A", difficulty: 1})
MERGE (d2:Document {name: "Document B", difficulty: 5})
MERGE (d3:Document {name: "Document C", difficulty: 10})
MERGE (d4:Document {name: "Document D", difficulty: 15})

// Product Relationships
MERGE (p1)-[:REQUIRES]->(d1)
MERGE (p1)-[:REQUIRES]->(d2)
MERGE (p2)-[:REQUIRES]->(d1)
MERGE (p2)-[:REQUIRES]->(d2)
MERGE (p2)-[:REQUIRES]->(d3)
MERGE (p3)-[:REQUIRES]->(d2)
MERGE (p3)-[:REQUIRES]->(d3)
MERGE (p3)-[:REQUIRES]->(d4)

WITH range(1, 10) AS ids, d1, d2
UNWIND ids AS id
// Adding 10 internal customers each with 1 account
// Customer
MERGE (c:Customer {id: id})
// Account
MERGE (ia:Account:Internal {number: id+100})
MERGE (c)-[:HAS_ACCOUNTS]->(ia)

// Documents
MERGE (dc:Documents {id: id})

// Document Relationships
MERGE (c)-[:HAS_DOCUMENTS]->(dc)
MERGE (dc)-[:SIGNED {signed: datetime()-duration({months: 11}), renew: datetime()+duration({months: 1})}]->(d1)
MERGE (dc)-[:SIGNED {signed: datetime()-duration({months: 11}), renew: datetime()+duration({months: 1})}]->(d2)

WITH range(1, 5) AS extids
UNWIND extids AS extid
// Adding 5 external accounts
MERGE (ea:Account:External {number: extid+900})

// Adding 20 transfers, creating a descending count of transactions over the 5 external accounts
WITH [5, 5, 4, 3, 3] AS transfersPerAccount
UNWIND range(1, 10) AS accountId // Increase to 10 for 10 internal accounts
WITH accountId, transfersPerAccount[(accountId - 1) % 5] AS transfersCount // Cycling through the 5 external accounts
MATCH (ia:Account:Internal {number: accountId + 100})
MATCH (ea:Account:External {number: (accountId % 5) + 901}) // Ensure unique external account for each internal account
UNWIND range(1, transfersCount) AS transferId
MERGE (t:Transfer {id: toString(accountId) + "-" + toString(transferId), amount: toInteger(100 + rand() * (990001))})
MERGE (t)-[:TO]->(ea)
MERGE (t)-[:FROM]->(ia)
				

Data Model

Customer & Products
View Data Model

Let's review the schema:

// Show database schema
CALL db.schema.visualization();
					

Match all products a customer has access to.

//
// Match all products for customer //
//

// Match products for which at least one required document has been signed by the customer.
MATCH (c:Customer {id: 1})-[:HAS_DOCUMENTS]->(:Documents)-[:SIGNED]->(d:Document)<-[:REQUIRES]-(p:Product)
// Aggregate the names of the signed documents for each product into a list.
WITH p, COLLECT(d.name) AS signedDocs

// For each product, match all its required documents.
MATCH (p)-[:REQUIRES]->(d2:Document)
// Carry forward the product, the list of signed documents for that product, 
// and aggregate all the required document names for that product into a list.
WITH p, signedDocs, COLLECT(d2.name) AS productDocs

// Filter to only include products where all the required documents are in the list of signed documents.
WHERE ALL(doc IN productDocs WHERE doc IN signedDocs)

// Return the name of the products that meet the criteria.
RETURN p.name AS ProductName
				

Find all documents that need renewing in next 2 monhts.

//
// Find all documents that need renewing in next 2 monhts //
//

// Match the SIGNED relationship between Documents and individual Document nodes.
MATCH (c:Customer {id: 1})-[:HAS_DOCUMENTS]->(:Documents)-[rel:SIGNED]->(d:Document)

// Filter for documents where the renewal date is within the next 2 months.
WHERE rel.renew < datetime() + duration('P2M')

// Calculate the days remaining until the renewal date.
// The DURATION.between() function calculates the duration between two dates.
// The ".days" accesses the day component of the duration.
WITH d, duration.between(datetime(), rel.renew).days AS DaysUntilRenewal

// Return the name of the document and the days until its renewal.
RETURN d.name AS DocumentName, DaysUntilRenewal

// Order the results by the days remaining for renewal in ascending order.
ORDER BY DaysUntilRenewal
				

View all NEW products ranked by perceived dificulty.

//
// Sort all new products by dificulty //
//

// Match the documents that the customer has signed.
MATCH (c:Customer {id: 1})-[:HAS_DOCUMENTS]->(:Documents)-[:SIGNED]->(d:Document)

// Aggregate the names of the signed documents into a list.
WITH c, COLLECT(d.name) AS signedDocs

// Match all the documents required by products.
MATCH (p:Product)-[:REQUIRES]->(d2:Document)

// Carry forward the product, the list of signed documents, and the current required document.
WITH p, signedDocs, d2

// Filter only those required documents that have NOT been signed by the customer.
WHERE NOT d2.name IN signedDocs

// Aggregate the difficulty of unsigned documents for each product.
WITH p, SUM(d2.difficulty) AS totalDifficulty

// Sort the products based on the aggregated difficulty of unsigned documents in descending order.
ORDER BY totalDifficulty DESC

// Return the product name and the total aggregated difficulty of unsigned documents.
RETURN p.name AS ProductName, totalDifficulty

// (Note: This final ORDER BY seems redundant as the results are already ordered in the previous ORDER BY clause.)
ORDER BY totalDifficulty
				

View external accounts with most money being sent to them.

//
// View external accounts with most money being sent to them //
//

// Match all Transfer nodes that have a relationship TO an External Account node
MATCH (t:Transfer)-[:TO]->(ea:Account:External)

// Aggregate (sum) the total amount of all transfers for each external account
WITH ea, sum(t.amount) AS TotalReceivedAmount

// Return the number (identifier) of the external account and the total received amount
RETURN ea.number AS ExternalAccountNumber,
"£" + replace(toString(TotalReceivedAmount), ".", ",") AS FormattedTotalReceivedAmount

// Order the results by the total received amount in descending order
ORDER BY TotalReceivedAmount DESC;