Neo4j Graphs, Acceleration Frameworks, and Recommendations: A Winning Trio



The power of recommendation engines in boosting sales through contextual suggestions in any retail industry is undeniable. Contextual recommendations delve deeper into your data. Imagine you’re a retail manager tasked with improving product recommendations for your e-commerce platform or equipping your sales team with the right mix of product recommendations and the underlying context for their next conversation with their customers. You wouldn’t rely solely on historical sales data or individual customer preferences. Instead, you’d factor in various aspects like customer segmentation, product affinities, real-time context, and more to improve your sales performance and help your sales team maximize their commissions.

Neo4j, with its graph database, exemplifies this approach and offers an easier and more flexible path to implementation. It allows us to traverse relationships swiftly, uncover hidden patterns, and make informed decisions with explainability.

Contextual recommendations are not about looking at a single data point but about considering multiple factors and boosting recommendations based on what’s important in the current situation,like customer history, stock availability or product similarity, while making a sale to a retail customer. Picture a recommendation engine that doesn’t just follow rules but crafts them intelligently. Neo4j Keymaker does precisely that. It leverages graph analytics algorithms and connected data queries to score recommendations based on predefined rules. What’s more, it’s modular and scalable. Teams can fine-tune the recommendation pipeline, adjusting phases as needed. Whether it’s upselling, cross-selling, or tailoring promotions, Neo4j Keymaker simplifies the process.

This blog post will talk about how in the retail industry, recommendations can boost a sales rep’s sales on his next visit to an existing customer and how a combination of the Neo4j Graph Database and Neo4j Keymaker recommendation engine can support this process by providing the best data to accelerate sales.

Objective

As a sales rep for a pharmaceutical company, I handle a diverse range of products, including over-the-counter medicines, vitamins, prescriptions, and generics, catering to pharmacies, hospitals, and retail chains. My earnings are commission-based, directly tied to my sales performance. To optimize my sales and meet my targets, I need a recommendation system that suggests a mix of products for my visits to customers. This system should leverage past order history, market basket analysis, and consider commission and margin factors. Such a tool would enable me to sell more effectively and achieve my personal sales goals as well as support the company’s sales metrics.

Solution Architecture

Solution Architecture.

Source Data

Begin by gathering data from various disparate sources like CRM systems, MDM systems, third-party sources, etc. This data might be unstructured and spread across different systems, requiring consolidation.

Develop a Graph Data Model

Developing a graph data model is about translating your conceptual view of data into a logical model – one that captures entities, relationships, and properties. Whether you’re building recommendation engines, fraud detection systems, or knowledge graphs, a well-designed graph model is the foundation for effective data exploration and visualization. An effective graph model addresses specific business scenarios and questions, so the model should be based on what business questions are going to be asked of this data.

Populate Interconnected Data in a Graph

Transform and load the collected data into a knowledge graph. This step involves creating nodes and relationships that represent the interconnected nature of the data, enabling efficient traversal and querying. Neo4j offers various connectors and integrations to help bring together your data.

Build Recommendations

Use the interconnected data to develop recommendations. By exploring the relationships and patterns within the graph, establishing business rules, leveraging similarity and community detection algorithms, developing a scoring model, and craft personalized, contextually relevant recommendations.

Consume Recommendations

Expose the generated recommendations through a robust API. This allows applications and services to easily access and use the recommendations, enhancing user experiences and driving better decision-making.

This approach leverages the strengths of Neo4j Graph Database to handle complex, interconnected data and Neo4j Keymaker for building the recommendations.


Under the Hood: Solution Details


Source Data

In our retail sales scenario, disparate and scattered data exists, such as customer information, orders, products, campaigns, margin, commission, and market analysis. These data exist in structured and unstructured forms across multiple systems.

Knowledge Graphs

Building a knowledge graph allows us to design data models that naturally mirror real-world relationships and business logic. The idea of a knowledge graph is to incrementally add data (new entities and relationships) and extract a much higher level of knowledge or patterns from this updated graph, and this cycle can keep repeating and improving your results. This methodology allows you to get started quicker and show some results before fine-tuning the results by adding more contextual data to the graph.

Below is a simplified graph representation of the retail dataset. It highlights how well the data can be modeled and its connected contextual complexity. This model also reflects how the business operates.

Graph representation of a retail dataset.

Graph model created using Neo4j Cypher Workbench.

Recommendations

Our objective here is to build hyper-personalized product recommendations tailored to each customer while taking into account company priorities (margin, stock at hand, promotions, etc.) and maximizing seller incentives. For this we analyzed the existing manual process and identified key data points that significantly influence effective order suggestions for sales representatives and the company:

  • Order History – Identifies customer buying patterns and highlights products not purchased compared to previous quarters. This also considers seasonality patterns.
  • Market Basket – Facilitates upselling and cross-selling by recommending new products based on similar customer profiles.
  • Market Data Analysis – Provides third-party insights into competitive selling gaps for specific customer-product combinations.
  • Campaigns – Enables passing on benefits to customers through current product campaigns.
  • Commission – Motivates sales reps to promote products that increase their earnings.
  • Margin – Helps companies prioritize products that boost revenue.

The Neo4j Keymaker is a scoring recommendation engine that leverages graph analytics queries on your Neo4j knowledge graph. It is scalable and flexible, allowing for modular recommendation creation through its phased pipeline feature within the engine composition.

Below is our solution architecture with the technical stack.

Technical Stack and Solution Architecture.

Define Recommendation Rules

Let’s define a recommendation engine in Neo4j Keymaker, which will cover the above data points to generate effective recommendations. Please see the Neo4j Keymaker documentation for more details.

Creating a recommendation engine in Keymaker:

Creating a recommendation engine in Keymaker.

Next, add phases to the engine. In the Engine Phase, Keymaker begins its search for potential results within your knowledge graph. During each phase, it identifies relevant entities based on your query criteria and retrieves discovered items along with their associated scores and additional details. These scores serve as a way to rank the relevance of each result.

Neo4j Keymaker supports various types of phases, and in this example, we’re using the discovery and boost phases. By combining these phases, Keymaker creates pipelines that generate aggregated scores, forming the basis for personalized recommendations.

Adding phases to the recommendation engine.

A recommendation engine.

Phases Overview

<<variable>> this style in the following description represents engine variables that can be adjusted to increase/decrease scope.

Order History Product Discovery

Phase type: Discovery phase

Description: This discovery phase goes over the customer’s <<last_month>> and <<current_month>> data and returns item, score, and details object. Item has product name and SKU, score is based on quantity, and details captures the quantity of product for last and current month.

Cypher query: We use Neo4j’s graph pattern matching capabilities to go over Customer > Order > Product.

Similar Customer Product Discovery

Phase type: Discovery phase

Description: This discovery phase goes over similar customers’ last-month data and returns item, score, and details object. Item has product name and SKU, score is based on similar customers’ average quantities, and details captures average quantity of product for last month.

Cypher query: The customer similarity relationship is calculated based on customer segment, region, and product buying patterns using the Neo4j graph data science node similarity algorithm on an overall dataset. For Cypher query, we use SIMILAR_CUSTOMER relationship to bring similar customers’ product recommendations for cross-selling opportunities.

Consider Market Analysis Data Products

Phase type: Discovery phase

Description: This discovery phase goes over market analysis data of customers and products to identify products that customers are buying from competitors and has a potential to buy from us. It returns item, score, and details object. Item has product name and SKU, score is based on suggested quantity, details captures quantity of product.

Cypher query: For Cypher query, we use Neo4j’s graph pattern matching to go over Market Data > Customer > Product.

Product Aggregated Suggested Quantity From All Discovered Phases

Phase type: Boost phase

Description: This is a boost phase on discovered products to calculate aggregated suggested quantities of products based on discovered quantities from earlier order history, market basket, and market analysis phases. Per-product aggregated suggested quantity will allow us to perform further margin, commission, and other calculations easily.

Cypher query: For Cypher query, we use Neo4j’s Awesome Procedures On Cypher (APOC) and Scalar functions.

Boost High-Margin Products

Phase type: Boost phase

Description: This boost phase for discovered products based on margin data goes over each discovered item and brings calculated margins based on suggested quantity and returns the item, score, and additional details about the margin. This new score gets added into the discovered same items so it gets a higher score to come to top on the recommendation list.

Cypher query: For Cypher query, we are using Neo4j’s graph pattern matching to go over Customer > Product > Margin Data.

Boost High-Commission Products

Phase type: Boost phase

Description: This boost phase for discovered products based on commission data goes over each item and brings the calculated commission data based on the suggested quantity and returns the same item, score, and additional details about the commission. This new score gets added to the discovered same items so it gets a higher score to come to top on the recommendation list.

Cypher query: For Cypher query, we use Neo4j’s graph pattern matching to go over Customer > Product > Commission Data.

Boost Product Based On Stock Availability

Phase type: Boost phase

Description: This boost phase for discovered products based on their stock availability data goes over each item and brings their stock availability data based on suggested quantity and returns the same item, score, and additional details about stock availability. This new score gets added to discovered same items so it gets a higher score to come to top on the recommendation list.

Cypher query: For Cypher query, we use Neo4j’s graph pattern matching to go over Customer > Product > Distribution Center > Stock Availability.

Other rules-based phases can be added, and existing phases can be removed or made inactive (like to exclude stock availability). These take effect immediately, and your recommendations will be based on new and business-optimized rule sets.

Scoring Model

Scoring directly impacts the quality of recommendations. A well-calibrated scoring mechanism ensures that the most relevant items rise to the top. Scoring determines the relevance or utility of recommended items for a specific user. It quantifies how well an item matches the user’s preferences or intent.

In our context, we sum up the scores from all phases for each recommended product to create a consolidated final score. The Recommendation engine image highlights this well.

Weighting allows you to assign different levels of importance to various factors in your recommendation model. These factors could be features, user preferences, or contextual information. Properly assigning weights ensures that your recommendation engine aligns with business goals and user expectations.

Recommendation engines are about striking a balance between accuracy, relevance, and user satisfaction. Whether you’re recommending movies, products, or articles, thoughtful scoring and weight allocation make all the difference.

In our specific scenario, we provide the following weights to our recommendation engine. These can take any value between 0 and 1. For each phase, the Cypher query uses its corresponding weight and multiplies it by the score, resulting in an individually tailored importance assigned to each recommendation.

{orderHistoryWeight:1, marketBasketWeight:1, marketDataWeight:1, 
 stockAvailabilityWeight:1, marginWeight:1, comissionWeight:1, campaignWeight:1}

Recommendations Sample Output

This engine is exposed as a GraphQL endpoint and can be invoked using an API key. Output of this engine is a JSON object that includes recommendations with their item information, score, and details object. The details object has each executed phase on the item for explainability.

Sample JSON output:

{
    "data": {
        "recommendations": [
            {
                "item": {
                    "id": "000000000000100069",
                    "name": "Lansoprazole_Orodisper Tab 30mg"
                },
                "score": 158976,
                "details": {
                    "commissionAnalysis": true,
                    "marginAnalysis": true,
                    "orderHistoryData": {
                        "productCurrentQuarterOrderedTotalQuantity": 0,
                        "productCurrentQuarterTotalOrders": 0,
                        "productLastQuarterTotalOrders": 6,
                        "productThisVisitSuggestedQuantity": 48000,
                        "productName": "Lansoprazole_Orodisper Tab 30mg",
                        "productLastQuarterOrderedTotalQuantity": 240000
                    },
                    "commissionAnalysisData": {
                        "calculatedComission": 4416,
                        "productCost": 1.84,
                        "productName": "Lansoprazole_Orodisper Tab 30mg",
                        "suggestedQuantity": 48000,
                        "productCommissionPercentage": 5
                    },
                    "orderHistory": true,
                    "suggestedQuantity": 48000,
                    "marginAnalysisData": {
                        "calculatedMargin": 66240,
                        "productCost": 1.84,
                        "productMarginPercentage": 75,
                        "productName": "Lansoprazole_Orodisper Tab 30mg",
                        "suggestedQuantity": 48000
                    }
                }
            },
            {
                "item": {
                    "id": "000000000000101700",
                    "name": "Alendronic Acid_Tab 10mg"
                },
                "score": 113652,
                "details": {
                    "commissionAnalysis": true,
                    "marginAnalysis": true,
                    "orderHistoryData": {
                        "productCurrentQuarterOrderedTotalQuantity": 0,
                        "productCurrentQuarterTotalOrders": 0,
                        "productLastQuarterTotalOrders": 1,
                        "productThisVisitSuggestedQuantity": 36000,
                        "productName": "Alendronic Acid_Tab 10mg",
                        "productLastQuarterOrderedTotalQuantity": 30000
                    },
                    "commissionAnalysisData": {
                        "calculatedComission": 3690,
                        "productCost": 2.05,
                        "productName": "Alendronic Acid_Tab 10mg",
                        "suggestedQuantity": 36000,
                        "productCommissionPercentage": 5
                    },
                    "orderHistory": true,
                    "suggestedQuantity": 36000,
                    "marginAnalysisData": {
                        "calculatedMargin": 36162,
                        "productCost": 2.05,
                        "productMarginPercentage": 49,
                        "productName": "Alendronic Acid_Tab 10mg",
                        "suggestedQuantity": 36000
                    }
                }
            }
        ]
    }
}

Recommendations in a table.

Table format of recommendations.

We transformed the JSON output using JavaScript, presenting it in a tabular format for easier readability. The resulting table captures the engine phases for each recommended product, along with their scores, suggested quantities, and stock availability information.

Consume Recommendations

As Salesforce is a go-to tool for a sales rep, to demonstrate, we integrated recommendations output by calling Neo4j Keymaker engine GraphQL API by passing required parameters like customer ID and weights for each phase and API key.

In our Salesforce integration, we build custom pages using Lightning Web Components (LWC) and Backend (Business Logic) Components (Apex). This involves creating a well-structured architecture that combines front-end UI components (LWC) with back-end business logic (Apex).

Lightning Web Components (LWC), provided by Salesforce, is a robust framework designed for building modern and efficient user interfaces. One of its standout features is its seamless integration with Apex methods. These Apex methods allow you to execute server-side operations, retrieve data from Salesforce databases, or interact with third-party systems via REST APIs. By leveraging Apex methods, you can create a cohesive connection between your frontend LWC components and the backend logic within Salesforce.

Please check example code snippets of this Salesforce integration here.

Neo4j Keymaker integration with Salesforce.

You can consume the recommendations in any application of your choice by calling the Keymaker API.

Conclusion

For field sales representatives, having access to effective recommendations can make all the difference. Neo4j Graph Database and Analytics empowers businesses to harness the power of their interconnected data.

Neo4j isn’t just a database – it’s a strategic ally for businesses aiming to boost sales, improve margins, and increase rep productivity. By combining knowledge graphs, data science, and the Keymaker framework, companies can quickly build a solution to empower their sales reps with contextually relevant recommendations to boost sales and satisfy customers, with Neo4j at the heart of it.


Appendix


Keymaker Phases Cypher Queries

Order History Product Discovery

// Get customer's last quarter ordered products information and populate a map of products with their order info
MATCH (customer:Customer {id:$customerId})-[:HAS_ORDER]->(order:Order)-[orderToProductRel:CONTAINS_PRODUCT]->(product:Product)
WHERE order.date >= date("2024-01-01") AND order.date < date("2024-04-01")
WITH product, {orderedQuantity:sum(orderToProductRel.quantity), totalOrders:count(order)} as productLastQuarterData

// Get customer's current quarter ordered products information and populate a map of products with their order info
OPTIONAL MATCH (customer:Customer {id:$customerId})-[:HAS_ORDER]->(o:Order)-[orderToProductRel:CONTAINS_PRODUCT]->(targetProduct)
WHERE o.date >= date("2024-04-01") and targetProduct.id = product.id
with product, productLastQuarterData, {orderedQuantity:sum(orderToProductRel.quantity), totalOrders:count(o)} as productCurrentQuarterData

// calculate current visit suggested quantity based on past order and current order for a product and considering 20 percent growth.
with product, productLastQuarterData, productCurrentQuarterData,
    CASE 
    WHEN productLastQuarterData.totalOrders - productCurrentQuarterData.totalOrders > 0 THEN (((productLastQuarterData.orderedQuantity * 1.2) - productCurrentQuarterData.orderedQuantity) / (productLastQuarterData.totalOrders - productCurrentQuarterData.totalOrders))
    WHEN productCurrentQuarterData.totalOrders * 1.2 > productLastQuarterData.totalOrders THEN 0
    ELSE ((productLastQuarterData.orderedQuantity * 1.2) - productCurrentQuarterData.orderedQuantity)
END AS productThisVisitSuggestedQuantity

// Set item score as quantity * cost * order_history_weight_paramater_passed_in_engine_request
RETURN product AS item, productThisVisitSuggestedQuantity * product.cost * $orderHistoryWeight  AS score , {orderHistory:true, orderHistoryData: {productName: product.name, productCurrentQuarterOrderedTotalQuantity:productCurrentQuarterData.orderedQuantity, productLastQuarterOrderedTotalQuantity:productLastQuarterData.orderedQuantity, productThisVisitSuggestedQuantity:productThisVisitSuggestedQuantity, productCurrentQuarterTotalOrders:productCurrentQuarterData.totalOrders,productLastQuarterTotalOrders:productLastQuarterData.totalOrders}} AS details

Similar Customer Product Discovery

// Get customer's last quarter product ids.
MATCH (customer:Customer {id:$customerId})-[:HAS_ORDER]->(o:Order)-[cpRel:CONTAINS_PRODUCT]->(product:Product)
WHERE o.date >= date("2024-01-01") AND o.date < date("2024-04-01")
with customer, collect(distinct product.id) as customerLastQuartnerOrderedProductIds

// Get similar customers based on SIMILAR_CUSTOMER relationship, and further filter their type, city and revenue segment info
MATCH (customerType:CustomerCategory)<-[:IN_CUSTOMER_CATEGORY]-(customer)-[:SIMILAR_CUSTOMER]->(similarCustomer:Customer)-[:IN_CUSTOMER_CATEGORY]->(customerType)
WHERE id(customer) <> id(similarCustomer) and  similarCustomer.revenueSegment = customer.revenueSegment
with customerLastQuartnerOrderedProductIds, collect(distinct similarCustomer.id) as similarcustids

// Get similar customers last quarter ordered product ids which are not customer last quarter ordered product ids, as those are already in customer profile.
match (pc:ProductCategory)<-[:IN_PRODUCT_CATEGORY]-(pf:ProductFamily)<-[:HAS_PRODUCT_FAMILY]-(similarCustomerProduct:Product)<-[similarCustomerOrderToProductRel:CONTAINS_PRODUCT]-(similarCustomerOrder:Order)<-[:HAS_ORDER]-(similarCustomer:Customer)
where similarCustomer.id in similarcustids and similarCustomerOrder.date >= date("2024-01-01") AND similarCustomerOrder.date < date("2024-04-01") and not similarCustomerProduct.id in customerLastQuartnerOrderedProductIds

// Get similar customer product average quantity, so can be suggested as cross sell products
with similarCustomerProduct, sum(similarCustomerOrderToProductRel.quantity) as similarCustomerProductQuantity, round(avg(similarCustomerOrderToProductRel.quantity)) as similarCustomerProductAvgQuantity
with  similarCustomerProduct as product, similarCustomerProductQuantity, similarCustomerProductAvgQuantity

// Set item score as quantity * cost * market_basket_weight_paramater_passed_in_engine_request
RETURN product AS item, similarCustomerProductAvgQuantity * product.cost * $marketBasketWeight  AS score , {marketBasket:"true", marketBasketData: {productName: product.name, otherCustomersOrderedOverAllQuantity:similarCustomerProductQuantity, otherCustomersOrderedAverageQuantity:similarCustomerProductAvgQuantity }} AS details

Consider Market Analysis Data Products

// Get products for customer based on market data
MATCH (product:Product)<-[:HAS_RESEARCH]-(marktdata:MarketData)<-[:HAS_MARKET_DATA]-(customer:Customer {id:$customerId})
where marktdata.salesOpportunity > 0

// Set item score as quantity * cost * market_data_weight_paramater_passed_in_engine_request
RETURN product AS item, marktdata.salesOpportunity * product.cost * $marketDataWeight AS score , {marketOpportunityAnalysis:"true", marketOpportunityAnalysisData: {productName: product.name, productSalesOpportunityQuantity:marktdata.salesOpportunity, productCost:product.cost}} AS details

Product Aggregated Suggested Quantity From All Discovered Phases

// calculate discovered product overall suggested quantity which is sum of earlier phases discovered quantities.
// So this suggested quantity can be used for margin, commission and other calculations.
with this, _details as details, _score as score
with this, details, coalesce(details.orderHistoryData.productThisVisitSuggestedQuantity, 0) + coalesce(details.marketBasketData.otherCustomersOrderedAverageQuantity, 0) +  coalesce(details.marketOpportunityAnalysisData.productSalesOpportunityQuantity, 0) as suggestedQuantity
with this, apoc.map.setEntry(details, "suggestedQuantity", suggestedQuantity) as details, suggestedQuantity
RETURN this as item , 0 AS score , details AS details

Boost High-Margin Products

// Get margin percentage info of a discovered product
with this, _details as details, _score as score
match (product:Product {id:this.id})
where product.productMarginPercentage is not null and product.cost is not null
with product, product.productMarginPercentage as productMarginPercentage, details.suggestedQuantity as suggestedQuantity

// calculate product margin based on quantity * cost * margin percentage
with product, round(suggestedQuantity * product.cost * (product.productMarginPercentage/100)) as calculatedMargin, suggestedQuantity

// Set item score as calculated margin * margin_weight_paramater_passed_in_engine_request
RETURN product AS item, calculatedMargin * $marginWeight AS score , {marginAnalysis:true, marginAnalysisData: {productName: product.name, productMarginPercentage:product.productMarginPercentage, suggestedQuantity:suggestedQuantity, productCost:product.cost, calculatedMargin:calculatedMargin}} AS details

Boost High-Commission Products

// Get commission percentage info of a discovered product
with this, _details as details, _score as score
match (this)-[:HAS_PRODUCT_FAMILY]->(pf:ProductFamily)-[:IN_PRODUCT_CATEGORY]-(pc:ProductCategory)
where pc.comissionPercentage is not null and this.cost is not null
with this as product, max(pc.comissionPercentage) as productComissionPercentage, details.suggestedQuantity as suggestedQuantity

// calculate product commission based on quantity * cost * commission percentage
with product, productComissionPercentage, round(suggestedQuantity * product.cost * (productComissionPercentage/100)) as calculatedComission, suggestedQuantity

// Set item score as calculated commission * comission_weight_paramater_passed_in_engine_request
RETURN product AS item, calculatedComission * $comissionWeight AS score , {commissionAnalysis:true, commissionAnalysisData: {productName: product.name, productCommissionPercentage:productComissionPercentage, suggestedQuantity:suggestedQuantity, productCost:product.cost, calculatedComission:calculatedComission}} AS details

Boost Product Based on Stock Availability

with this, _details as details, _score as score
// Get stock inventory of a discovered item product
MATCH (c:Customer {id:$customerId})-[:IN_CITY]->(:City)-[:IN_STATE]->(s:State)<-[:DC_UNDER_STATE]-(dc:DistributionCenter)-[:HAS_INVENTORY]->(stock:Stock)-[:OF_PRODUCT]->(this)

// consider only available quantity after safety stock cut, filter products which has more stock than required
with this, stock.centerId as stockCenterId, round(stock.available * .8) as availableStockAfterSafetyCut, details.suggestedQuantity as requiredQuantity
where availableStockAfterSafetyCut > requiredQuantity // 20 percentage safety stock

// Set item score as normalized quantity * stock_availability_weight_paramater_passed_in_engine_request
RETURN this AS item, ((availableStockAfterSafetyCut - requiredQuantity)/availableStockAfterSafetyCut) * $stockAvailabilityWeight  AS score , {stockAnalysis:true, stockAnalysisData: {productName: this.name, productAvailableStockQuantity:availableStockAfterSafetyCut, requiredQuantity:requiredQuantity, stockCenterId:stockCenterId}} AS details

Graph Data Science Similar Customer Node Similarity Relationship

// Get customer to product purchase frequency data
MATCH (customer:Customer)-[:HAS_ORDER]->(:Order)-[orderToProductRel:CONTAINS_PRODUCT]->(product:Product)
WITH customer, product, COUNT(orderToProductRel) AS numPurchases
with customer, product, numPurchases as customerProductPurchasedFrequency

// Use GDS library to project an in-memory graph to run algorithms.
WITH gds.graph.project(
    'customer-product-frequency-proj',
    customer,
    product,
    {   relationshipType: "CUSTOMER_PRODUCT_FREQUENCY",
        relationshipProperties:
        {
            customerProductPurchasedFrequency: customerProductPurchasedFrequency
        }
    }
) AS graph
RETURN graph.graphName AS graph, graph.nodeCount AS nodes, graph.relationshipCount AS rels

// Use GDS library to run a node similarity algorithm on a earlier projected graph to calculate similar customers.
// Write calculated relationship along with similarity score to database. 
CALL gds.nodeSimilarity.write("customer-product-frequency-proj", {
    writeRelationshipType: 'SIMILAR_CUSTOMER',
    writeProperty: 'score'
})
YIELD nodesCompared, relationshipsWritten

Salesforce Integration

Backend (Business Logic) Components (Apex)

Look for TODO comments in the code and replace text with your deployed keymaker values:

public with sharing class SalesforceNeo4jKeymakerApex {
    public SalesforceNeo4jKeymakerApex() {
    }
    @AuraEnabled(cacheable=true)
    public static String callout(String customerId, Decimal orderHistoryWeight,
         Decimal marketBasketWeight, Decimal marketDataWeight, Decimal stockAvailabilityWeight,
         Decimal marginWeight, Decimal comissionWeight){
                 
        // Ne4j Keymaker Engine GraphQL using variable references
        Map queryMap = new Map();
        // Add Query using variable references
        queryMap.put('query', 'query {' +
            'recommendations(' +
            '    params: {customerId: "' + customerId + '",' +
            '             orderHistoryWeight: ' + orderHistoryWeight + ',' +
            '             marketBasketWeight: ' + marketBasketWeight + ',' +
            '             marketDataWeight: ' + marketDataWeight + ',' +                                                            
            '             stockAvailabilityWeight: ' + stockAvailabilityWeight + ',' +
            '             marginWeight: ' + marginWeight + ',' +                              
            '             comissionWeight: ' + comissionWeight + ',' +                              
            '            }' +
            '    engineID: ""' + // TODO replace  with your deployment value
            '    first: 999' +
            '    skip: 0' +
            '){' +
            '    item' +
            '    score' +
            '    details' +
            ' }' +
            '}');


            // Turn the query map into a JSON string
            String jsonBody = JSON.serialize(queryMap);


            // Post request
            Http http = new Http();
            HttpRequest request = new HttpRequest();
            request.setTimeout(120000);
            request.setEndpoint('http://:/graphql'); // TODO replace  and  with your deployment value
            request.setMethod('POST');
            request.setHeader('Content-Type','application/json; charset=utf-8');
            request.setHeader('api-key', ''); // TODO replace  with your deployment value
            request.setBody(jsonBody);


            // Get response
            HttpResponse res = http.send(request);
            Integer statusCode = res.getStatusCode();
            String status = res.getStatus();
            String body = res.getBody();
            System.debug('Request Body: ' + res.getBody());
            System.debug(statusCode);
            System.debug(status);
            System.debug(body);
            return res.getBody();
         }
}

Lightning Web Components (LWC)

neo4j-keymaker-recommendations-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

neo4j-keymaker-recommendations.js

import { LightningElement,api,wire,track } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import neo4jId from '@salesforce/schema/Opportunity.neo4jId__c';
import fetch_recommendation from '@salesforce/apex/SalesforceNeo4jKeymakerApex.callout'


export default class Neo4jKeymakerRecommendations extends LightningElement {
    @api recordId;
    @track customerId;
    @track recommendations;
    @track showTable = false; // hide table initially


    // weights default value
    @track orderHistoryWeight = 0.1;
    @track marketBasketWeight = 0.1;
    @track marketDataWeight = 0.1;
    @track stockAvailabilityWeight = 0.1;
    @track marginWeight = 0.1;
    @track comissionWeight = 0.1;


    @wire(getRecord, { recordId: '$recordId', fields: [neo4jId] })
    wiredRecord({ error, data }) {
        if (data) {
            this.customerId = data.fields.neo4jId__c.value;          
        } else if (error) {
            console.error('Error fetching record', error);
        }
    }
    handleorderHistoryWeight(event) {
        this.orderHistoryWeight = event.target.value;
    }


    handlemarketBasketWeight(event) {
        this.marketBasketWeight = event.target.value;
    }
    handlemarketDataWeight(event) {
        this.marketDataWeight = event.target.value;
    }


    handlestockAvailabilityWeight(event) {
        this.stockAvailabilityWeight = event.target.value;
    } handlemarginWeight(event) {
        this.marginWeight = event.target.value;
    }


    handlecomissionWeight(event) {
        this.comissionWeight = event.target.value;
    }
   
    // handle submit button
    handleButtonClick() {
        this.fetchRecommendation(
            this.customerId,
            this.orderHistoryWeight,
            this.marketBasketWeight,
            this.marketDataWeight,
            this.stockAvailabilityWeight,
            this.marginWeight,
            this.comissionWeight
        );
        this.showTable = true;
    }


    // make apex call on submit click.
    fetchRecommendation(
        customerId,
        orderHistoryWeight,
        marketBasketWeight,
        marketDataWeight,
        stockAvailabilityWeight,
        marginWeight,
        comissionWeight
    ) {
        // call apex to bring recommendations.
        fetch_recommendation({
            customerId: customerId,
            orderHistoryWeight: orderHistoryWeight,
            marketBasketWeight: marketBasketWeight,
            marketDataWeight: marketDataWeight,
            stockAvailabilityWeight: stockAvailabilityWeight,
            marginWeight: marginWeight,
            comissionWeight: comissionWeight
        }).then(data => {
            this.recommendations = JSON.parse(data).data.recommendations;
            this.recommendations.forEach(item => {
               
                if (item.details) {
                    if (typeof item.details.orderHistory === 'undefined'){
                        item.details.orderHistory = false;
                    }
                    if (typeof item.details.marketBasket === 'undefined'){
                        item.details.marketBasket = false;
                    }
                    if (typeof item.details.stockAnalysis === 'undefined'){
                        item.details.stockAnalysis = false;
                    }
                    if (typeof item.details.marginAnalysis === 'undefined'){
                        item.details.marginAnalysis = false;
                    }
                    if (typeof item.details.commissionAnalysis === 'undefined'){
                        item.details.commissionAnalysis = false;
                    }
                    if (typeof item.details.marketOpportunityAnalysis === 'undefined'){
                        item.details.marketOpportunityAnalysis = false;
                    }
                    if (typeof item.details.stockAnalysisData === 'undefined'){
                        item.details.stockAnalysisData = {
                            productAvailableStockQuantity: 0
                        };
                    }
                    if (typeof item.details.orderHistoryData === 'undefined'){
                        item.details.orderHistoryData = {
                            productThisVisitSuggestedQuantity: 0
                        };
                    }
                    if (typeof item.details.marketBasketData === 'undefined'){
                        item.details.marketBasketData = {
                            otherCustomersOrderedAverageQuantity: 0
                        };
                    }
                } else{
                    console.error("no details for item ",item)
                }
            });
         
            // limit to display 10 recommendations
            this.recommendations = this.recommendations.slice(0, 10);
        });
    }


}

neo4j-keymaker-recommendations-template.html

<template>
    <lightning-card title="Joystick">
        <div class="slds-m-around_medium">
            <div class="slds-grid slds-wrap">
                <div class="slds-col">
                    <div class="slds-grid slds-align_absolute-center">
                        <div class="slds-col">
                            <lightning-input type="range" label="Order History Weight" value={orderHistoryWeight} min="0" max="1" step="0.01" onchange={handleorderHistoryWeight}></lightning-input>
                        </div>
                        <div class="slds-col">
                            <p>{orderHistoryWeight}</p>
                        </div>
                    </div>
                    <div class="slds-grid slds-align_absolute-center">
                        <div class="slds-col">
                            <lightning-input type="range" label="Market Basket Weight" value={marketBasketWeight} min="0" max="1" step="0.01" onchange={handlemarketBasketWeight}></lightning-input>
                        </div>
                        <div class="slds-col">
                            <p>{marketBasketWeight}</p>
                        </div>
                    </div>
                    <div class="slds-grid slds-align_absolute-center">
                        <div class="slds-col">
                            <lightning-input type="range" label="Market Data Weight" value={marketDataWeight} min="0" max="1" step="0.01" onchange={handlemarketDataWeight}></lightning-input>
                        </div>
                        <div class="slds-col">
                            <p>{marketDataWeight}</p>
                        </div>
                    </div>
                    <div class="slds-grid slds-align_absolute-center">
                        <div class="slds-col">
                            <lightning-input type="range" label="Stock Availability Weight" value={stockAvailabilityWeight} min="0" max="1" step="0.01" onchange={handlestockAvailabilityWeight}></lightning-input>
                        </div>
                        <div class="slds-col">
                            <p>{stockAvailabilityWeight}</p>
                        </div>
                    </div>
                </div>
                <div class="slds-col" style="margin-left: 1rem;">
                    <div class="slds-grid slds-align_absolute-center">
                        <div class="slds-col">
                            <lightning-input type="range" label="Margin Weight" value={marginWeight} min="0" max="1" step="0.01" onchange={handlemarginWeight}></lightning-input>
                        </div>
                        <div class="slds-col">
                            <p>{marginWeight}</p>
                        </div>
                    </div>
                    <div class="slds-grid slds-align_absolute-center">
                        <div class="slds-col">
                            <lightning-input type="range" label="Commission Weight" value={comissionWeight} min="0" max="1" step="0.01" onchange={handlecomissionWeight}></lightning-input>
                        </div>
                        <div class="slds-col">
                            <p>{comissionWeight}</p>
                        </div>
                    </div>
                </div>
            </div>
            <lightning-button label="Search Recommendation" onclick={handleButtonClick} variant="brand"></lightning-button>
        </div>
        <template if:true={showTable}>
            <div class="slds-scrollable slds-m-around_medium">
                <table class="slds-table slds-table_cell-buffer slds-table_bordered slds-table_col-bordered slds-table_striped slds-max-medium-table_stacked-horizontal">
                    <thead>
                        <tr class="slds-line-height_reset">
                            <th class="slds-text-title_caps">Product</th>
                            <th class="slds-text-title_caps">Priority</th>
                            <th class="slds-text-title_caps">OrderHistory</th>
                            <th class="slds-text-title_caps">MarketBasket</th>
                            <th class="slds-text-title_caps">StockAnalysis</th>
                            <th class="slds-text-title_caps">MarginAnalysis</th>
                            <th class="slds-text-title_caps">CommissionAnalysis</th>
                            <th class="slds-text-title_caps">MarketOpportunityAnalysis</th>
                            <th class="slds-text-title_caps">SuggestedQuantity</th>
                            <th class="slds-text-title_caps">AvailableStockQuantity</th>
                           
                        </tr>
                    </thead>
                    <tbody>
                        <template for:each={recommendations} for:item="recommendation" for:index="index">
                            <tr key={recommendation.item.id}>
                                <td>{recommendation.item.name}</td>
                                <td>{index}</td>
                                <td>{recommendation.details.orderHistory}</td>
                                <td>{recommendation.details.marketBasket}</td>
                                <td>{recommendation.details.stockAnalysis}</td>
                                <td>{recommendation.details.marginAnalysis}</td>
                                <td>{recommendation.details.commissionAnalysis}</td>
                                <td>{recommendation.details.marketOpportunityAnalysis}</td>
                                <td>{recommendation.details.suggestedQuantity}</td>
                                <td>{recommendation.details.stockAnalysisData.productAvailableStockQuantity}</td>
                                <!-- Add additional columns as needed -->
                            </tr>
                        </template>
                    </tbody>
                </table>
            </div>
        </template>
    </lightning-card>
</template>