Building an Amazon-Like Recommendation Engine Using Slash GraphQL

Back in the early 2000s, I was working on a project implementing an eCommerce solution by Art Technology Group (ATG), now owned by Oracle. The ATG Dynamo product was an impressive solution as it included a persistence layer and a scenarios module. At the time, companies like Target and Best Buy used the Dynamo solution, leveraging the scenario module to provide recommendations to the customer.

As an example, the Dynamo eCommerce solution was smart enough to remember when a customer added a product to their cart and later removed it. As an incentive, the scenario server could be designed to offer the same customer a modest discount on a future visit if they re-added the product into their cart and purchased it within the next 24 hours.

Since those days, I have always wanted to create a simple recommendations engine, which is my goal for this publication.

About the Recommendations Example

I wanted to keep things simple and create some basic domain objects for the recommendations engine.  In this example, the solution will make recommendations for musical artists and the underlying Artist object is quite simple:

Java
 




x


1
@AllArgsConstructor
2
@NoArgsConstructor
3
@Data
4
public class Artist {
5
   private String name;
6
}



In a real system, there would be so many more attributes to track. However, in this example, the name of the artist will suffice.

As one might expect, customers will rate artists on a scale of 1 to 5, where a value of five represents the best score possible.  Of course, it is possible (and expected) that customers will not rate every artist.  The customer will be represented (again) by a very simple Customer object:

Java
 




xxxxxxxxxx
1


 
1
@AllArgsConstructor
2
@NoArgsConstructor
3
@Data
4
public class Customer {
5
   private String username;
6
}



The concept of a customer rating an artist will be captured in the following Rating object:

Java
 




xxxxxxxxxx
1


 
1
@AllArgsConstructor
2
@NoArgsConstructor
3
@Data
4
public class Rating {
5
   private String id;
6
   private double score;
7
   private Customer by;
8
   private Artist about;
9
}



In my normal Java programming efforts I would likely use private Customer customer and private Artist artist for my objects, but I wanted to follow the pattern employed by graph databases, where I employ variables like by and about instead.  This should become more clear as the article continues.

Dgraph Slash GraphQL

With the popularity of graph databases, I felt like my exploration of creating a recommendations engine should employ a graph database. After all, GraphQL has become a popular language for talking to services about graphs. While I only have some knowledge around graph databases, my analysis seemed to conclude that a graph database is the right choice for this project and is often the source for real-world services making recommendations. Graph databases are a great solution when the relationships (edges) between your data (nodes) are just as important as the data itself — and a recommendation engine is the perfect example.  

However, since I'm just starting out with graph databases, I certainly didn't want to worry about starting up a container or running a GraphQL database locally.  Instead I wanted to locate a SaaS provider. I decided to go with Dgraph's fully-managed backend service, called Slash GraphQL. It's a hosted, native GraphQL solution. The Slash GraphQL service was just released on September 10th, 2020, and includes a free 10,000 credits service, which can be enabled by using the following link:

https://slash.dgraph.io/

After launching this URL, a new account can be created using the normal authorization services:

Slash Graph authentication

In my example, I created a backend called "spring-boot-demo" which ultimately resulted in the following dashboard:

graphql dashboard

The process to get started was quick and free, making it effortless to configure the Slash GraphQL service.

Configuring Slash GraphQL

As with any database solution, we must write a schema and deploy it to the database.  With Slash GraphQL, this was quick and easy:

Plain Text
 




xxxxxxxxxx
1
16


 
1
type Artist {
2
   name: String! @id @search(by: [hash, regexp])
3
   ratings: [Rating] @hasInverse(field: about)
4
}
5
 
          
6
type Customer {
7
   username: String! @id @search(by: [hash, regexp])
8
   ratings: [Rating] @hasInverse(field: by)
9
}
10
 
          
11
type Rating {
12
   id: ID!
13
   about: Artist!
14
   by: Customer!
15
   score: Int @search
16
}



In fact, my original design was far more complex than it needed to be, and the level of effort behind making revisions was far easier than I expected. I quickly began to see the value as a developer to be able to alter the schema without much effort.

With the schema in place, I was able to quickly populate some basic Artist information:

JSON
 




xxxxxxxxxx
1
14


 
1
mutation {
2
 addArtist(input: [
3
   {name: "Eric Johnson"},
4
   {name: "Genesis"},
5
   {name: "Led Zeppelin"},
6
   {name: "Rush"},
7
   {name: "Triumph"},
8
   {name: "Van Halen"},
9
   {name: "Yes"}]) {
10
   artist {
11
     name
12
   }
13
 }
14
}



At the same time, I added a few fictional Customer records:

JSON
 




xxxxxxxxxx
1
12


 
1
mutation {
2
 addCustomer(input: [
3
   {username: "Doug"},
4
   {username: "Jeff"},
5
   {username: "John"},
6
   {username: "Russell"},
7
   {username: "Stacy"}]) {
8
   customer {
9
     username
10
   }
11
 }
12
}



As a result, the five customers will provide ratings for the seven artists, using the following table:

seven artists in table

An example of the rating process is shown below:

JSON
 




xxxxxxxxxx
1
13


 
1
mutation {
2
 addRating(input: [{
3
   by: {username: "Jeff"},
4
   about: { name: "Triumph"},
5
   score: 4}])
6
 {
7
   rating {
8
     score
9
     by { username }
10
     about { name }
11
   }
12
 }
13
}



With the Slash GraphQL data configured and running, I can now switch gears and work on the Spring Boot service.

The Slope One Ratings Algorithm

In 2005, a research paper by Daniel Lemire and Anna Maclachian introduced the Slope One family of collaborative filtering algorithms. This simple form of item-based collaborative filtering looked to be a perfect fit for a recommendations service, because it takes into account ratings by other customers in order to score items not rated for a given customer.

In pseudo-code, the Recommendations Service would achieve the following objectives:

With the customer map created, the core of the Slope One ratings processing will execute by calling the SlopeOne class:

For this example, a random Customer is selected and the corresponding object from the Map<Customer, HashMap<Artist, Double>> projectedData map is analyzed to return the following results:

JSON
 




xxxxxxxxxx
1
25


 
1
{
2
   "matchedCustomer": {
3
       "username": "Russell"
4
   },
5
   "recommendationsMap": {
6
       "Artist(name=Eric Johnson)": 0.7765842245950264,
7
       "Artist(name=Yes)": 0.7661904474477843,
8
       "Artist(name=Triumph)": 0.7518039724158979,
9
       "Artist(name=Van Halen)": 0.7635436007978691
10
   },
11
   "ratingsMap": {
12
       "Artist(name=Genesis)": 0.4,
13
       "Artist(name=Led Zeppelin)": 1.0,
14
       "Artist(name=Rush)": 0.6
15
   },
16
   "resultsMap": {
17
       "Artist(name=Eric Johnson)": 0.7765842245950264,
18
       "Artist(name=Genesis)": 0.4,
19
       "Artist(name=Yes)": 0.7661904474477843,
20
       "Artist(name=Led Zeppelin)": 1.0,
21
       "Artist(name=Rush)": 0.6,
22
       "Artist(name=Triumph)": 0.7518039724158979,
23
       "Artist(name=Van Halen)": 0.7635436007978691
24
   }
25
}



In the example above, the "Russell" user was randomly selected. When looking at the original table (above), Russell only provided ratings for Genesis, Led Zeppelin, and Rush. The only artist that he truly admired was Led Zeppelin. This information is included in the ratingsMap object and also in the resultsMap object.

The resultsMap object includes projected ratings for the other four artists: Eric Johnson, Yes, Triumph, and Van Halen. To make things easier, there is a recommendationsMap included in the payload, which includes only the artists that were not rated by Russell.

Based upon the other reviews, the Recommendations Service would slightly favor Eric Johnson over the other four items—with a score of 0.78, which is nearly a value of four in the five-point rating system.

The Recommendations Service

In order to use the Recommendations Service, the Spring Boot server simply needs to be running and configured to connect to the Slash GraphQL cloud-based instance. The GraphQL Endpoint on the Slash GraphQL Dashboard can be specified in the application.yml as slash-graph-ql.hostname or via passing in the value via the ${SLASH_GRAPH_QL_HOSTNAME} environment variable.

The basic recommendations engine can be called using the following RESTful URI:

GET - {spring-boot-service-host-name}/recommend

This action is configured by the RecommendationsController, as shown below:

Java
 




xxxxxxxxxx
1


 
1
@GetMapping(value = "/recommend")
2
public ResponseEntity<Recommendation> recommend() {
3
   try {
4
       return new ResponseEntity<>(recommendationService.recommend(), HttpStatus.OK);
5
   } catch (Exception e) {
6
       return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
7
   }
8
}



Which calls the RecommendationService:

Java
 




xxxxxxxxxx
1
32


 
1
@Slf4j
2
@RequiredArgsConstructor
3
@Service
4
public class RecommendationService {
5
   private final ArtistService artistService;
6
   private final CustomerService customerService;
7
   private final SlashGraphQlProperties slashGraphQlProperties;
8
 
          
9
   private static final String RATING_QUERY = "query RatingQuery {"
10
         + "queryRating { "
11
         + "id, " 
12
         + "score, " 
13
         + "by { username }, " 
14
         + "about { name } " 
15
         + "}}";
16
 
          
17
   public Recommendation recommend() throws Exception {
18
       ResponseEntity<String> responseEntity = RestTemplateUtils.query(slashGraphQlProperties.getHostname(), RATING_QUERY);
19
 
          
20
      try {
21
           ObjectMapper objectMapper = new ObjectMapper();
22
           SlashGraphQlResultRating slashGraphQlResult = objectMapper.readValue(responseEntity.getBody(), SlashGraphQlResultRating.class);
23
        
24
           log.debug("slashGraphQlResult={}", slashGraphQlResult);
25
           return makeRecommendation(slashGraphQlResult.getData());
26
       } catch (JsonProcessingException e) {
27
           throw new Exception("An error was encountered processing responseEntity=" + responseEntity.getBody(), e);
28
       }
29
   }
30
...
31
 
          
32
}



Please note — something that might be missed at a quick glance of this code, is the power and ease in being able to pull out a subgraph to perform the recommendation. In the example above, the slashGraphQlResult.getData() line is providing a subgraph to the makeRecommendation() method.

The RATING_QUERY is the expected Slash GraphQL format to retrieve Rating objects.  The RestTemplateUtils.query() method is part of a static utility class, to keep things DRY (don't repeat yourself):

Java
 




xxxxxxxxxx
1
13


 
1
public final class RestTemplateUtils {
2
   private RestTemplateUtils() { }
3
   private static final String MEDIA_TYPE_GRAPH_QL = "application/graphql";
4
   private static final String GRAPH_QL_URI = "/graphql";
5
 
          
6
   public static ResponseEntity<String> query(String hostname, String query) {
7
       RestTemplate restTemplate = new RestTemplate();
8
       HttpHeaders headers = new HttpHeaders();
9
       headers.setContentType(MediaType.valueOf(MEDIA_TYPE_GRAPH_QL));
10
       HttpEntity<String> httpEntity = new HttpEntity<>(query, headers);
11
       return restTemplate.exchange(hostname + GRAPH_QL_URI, HttpMethod.POST, httpEntity, String.class);
12
   }
13
}



Once the slashGraphQlResult object is retrieved, the makeRecommendation() private method is called, which returns the following Recommendation object. (This was shown above in JSON format):

Java
 




xxxxxxxxxx
1


 
1
@AllArgsConstructor
2
@NoArgsConstructor
3
@Data
4
public class Recommendation {
5
   private Customer matchedCustomer;
6
   private HashMap<Artist, Double> recommendationsMap;
7
   private HashMap<Artist, Double> ratingsMap;
8
   private HashMap<Artist, Double> resultsMap;
9
}


Conclusion

In this article, an instance of Dgraph Slash GraphQL was created with a new schema and sample data was loaded.  That data was then utilized by a Spring boot service which served as a basic recommendations engine.  For those interested in the full source code, please review the GitLab repository.

From a cost perspective, I am quite impressed with the structure that Slash GraphQL provides.  The new account screen indicated that I have 10,000 credits to use, per month, for no charge.  In the entire time I used Slash GraphQL to prototype and create everything for this article, I only utilized 292 credits.  Current pricing for Slash GraphQL makes use of the service very attractive, at $9.99/month for 5GB of data transfer. No hidden costs. No costs for data storage. No cost per query.

Using a graph database for the first time did present a small learning curve and I am certain there is far more than I can learn by continuing this exercise.  I felt that Slash GraphQL exceeded my expectations with the ability to change the schema as I learned more about my needs.  As a feature developer, this is a very important aspect that should be recognized, especially compared to the same scenario with a traditional database.

In my next article, I'll introduce an Angular (or React) client into this process, which will interface directly with GraphQL and the Recommendation Service running in Spring Boot.

Have a really great day!

 

 

 

 

Top