A recommendation engine can give site visitors an opportunity to discover the most relevant content on the site depending on the page user is browsing. A content publisher or a store can benefit from extra views/exposure or upselling.

Basic recommendation using similarity

In this post, I am sharing how I implemented a very simple cocktail recommendation system. It uses similarity and recommends the user with cocktails that have the most common ingredients with the cocktail the user is viewing. A benefit for users to see these recommendations is that if they are mixing the drink they are currently browsing, they will be able to make the recommended drinks with very little change in ingredients.

Cypher query

Let’s begin by composing our Cypher query.

First of all, let’s take an example of Mojito, and we want to recommend the user the drinks that are most similar to Mojito. Now, select Mojito using Cypher query.

MATCH (mojito:Drink {slug: 'Mojito'}) return mojito

MATCH is like SELECT in SQL, it is followed by a pattern of a graph that Cypher will match and returns the respective, nodes and relations.

Nodes are represented by parenthesis and have the following sub-components (NODE_VARIABLE:NODE_LABEL {NODE_PROPERTY: PROPERTY_VALUE_TO_MATCH}) and RETURN will allow you to select what to return in the result.

Noe4j browser can visualize the returned subgraph in a visual form. It even shows the primary relations of the returned node so, we can see all the ingredients of Mojito along with its node.

Mojito ingredients graph neo4j
Mojito ingredients graph

In cypher, you can use -[:RELATION_TYPE]-> in both directions to select relations of a node. The nodes can be named with variables as we did before for mojito. This time we will call it cocktail and the related ingredient, ingredient.

MATCH (cocktail:Drink {slug: 'Mojito'})-[:HAS_INGREDIENT]->(ingredient),

Next, we want to get all the drinks, other than the initial one, that are related to the ingredient.

(ingredient)<-[:HAS_INGREDIENT]-(otherCocktail:Drink)
WHERE cocktail <> otherCocktail

Now we can return the cocktail, otherCocktail and commonIngredients and order it by the count of commonIngredients

RETURN cocktail.name, otherCocktail.name,  collect(distinct ingredient.name) AS commonIngredients, size(collect(distinct ingredient)) as commonCount
ORDER BY commonCount DESC Limit 5

Where collect() will aggregate all the ingredients otherwise separate rows into one, and size() gives the length of the list.

Finally, our query will look something like follows.

MATCH (cocktail:Drink {slug: 'Mojito'})-[:HAS_INGREDIENT]->(ingredient),
(ingredient)<-[:HAS_INGREDIENT]-(otherCocktail:Drink)
WHERE cocktail <> otherCocktail
RETURN cocktail.name, otherCocktail.name,  collect(distinct ingredient.name) AS commonIngredients, size(collect(distinct ingredient)) as commonCount
ORDER BY commonCount DESC Limit 5

Result

This query will return the following data as a result. For the sake of brevity, I chose to only return names of the drinks and components, but you can return any properties of the returned nodes.

╒═══════════════╤══════════════════════════╤══════════════════════════╤═════════════╕
│"cocktail.name"│"otherCocktail.name"      │"commonIngredients"       │"commonCount"│
╞═══════════════╪══════════════════════════╪══════════════════════════╪═════════════╡
│"Mojito"       │"Acapulco"                │["rum","carbonated water",│5            │
│               │                          │"mint","sugar","lemon"]   │             │
├───────────────┼──────────────────────────┼──────────────────────────┼─────────────┤
│"Mojito"       │"Sudret Iced Tea"         │["rum","carbonated water",│5            │
│               │                          │"mint","sugar","lemon"]   │             │
├───────────────┼──────────────────────────┼──────────────────────────┼─────────────┤
│"Mojito"       │"Mojito #2"               │["rum","carbonated water",│5            │
│               │                          │"mint","sugar","lemon"]   │             │
├───────────────┼──────────────────────────┼──────────────────────────┼─────────────┤
│"Mojito"       │"Rum Rickey"              │["rum","carbonated water",│4            │
│               │                          │"sugar","lemon"]          │             │
├───────────────┼──────────────────────────┼──────────────────────────┼─────────────┤
│"Mojito"       │"Frozen Mango And Mint Spi│["rum","mint","sugar","lem│4            │
│               │ced Daiquiri"             │on"]                      │             │
└───────────────┴──────────────────────────┴──────────────────────────┴─────────────┘

Conclusion

The recommendation engine can enrich your site with feature of a great value to both the visitor and you as a publisher/business/store. If done right, a very simple implementation can add value to your site. The given example is as simple as it can get, but with Neo4j you can use numerous built-in graph algorithms to help you with even an extremely sophisticated recommendation engine.