WordPress Recommendations with Neo4j – Part 2: Content Based Recommendations

This post is part of a series on building a recommendation engine with WordPress. If you haven’t already done so, check out the posts below:

  1. Part 1: Hooks
  2. Part 2: Content Based Recommendations
  3. Part 3: Collaborative Filtering

Content Based Recommendations

Now that we have some data in our Database, we can use the information to create recommendations for the user. The simplest recommendation we can provide is recommending posts in the same category.

MATCH (p:Post)-[:HAS_TAXONOMY]->(:Taxonomy)<-[:HAS_TAXONOMY]-(recommended:Post)
WHERE p.ID = 1234
RETURN recommended

However, WordPress does this already. We can do a lot better. As we also have the post’s author, we can use this information as an additional recommendation criteria. If a site has multiple authors, this could be a good indicator of post quality and therefore our recommendations should treat it as such.

Relating the Author

Firstly, let’s create a function to merge the User into our Graph by their ID.

CREATE CONSTRAINT ON (u:User) ASSERT u.user_id IS UNIQUE
User.php
<?php
namespace Neopress;

use GraphAware\Neo4j\Client\Transaction\Transaction;

class User {

/**
* Create a Cypher Query for a Category
*
* @param Int $post_id
* @return void
*/
public static function merge(Transaction $tx, $user_id) {
$cypher = 'MERGE (u:User {user_id: {user_id}})';

$tx->push($cypher, ['user_id' => $user_id]);
}

}

Now, we can add the extra queries to our transaction that will run this merge query and attach the User to their post via an :AUTHORED relationship.

Post.php
// Create Author
User::merge($tx, $author);

// Relate Author to Post
$cypher = '
MATCH (p:Post {ID: {post_id}})
MATCH (u:User {user_id: {user_id}})
MERGE (u)-[:AUTHORED]->(p)
';

$tx->push($cypher, ['post_id' => $post_id, 'user_id' => $user_id]);

If you hit Update on your post, you will now see that the User has been related to the post.

It’s worth noting at this stage that the our :AUTHORED relationships are directed towards our Post where the :HAS_TAXONOMY relationship is an outward relationship to our Taxonomy nodes. Luckily, Neo4j allows us to traverse in both directions by omitting a direction and traverse multiple types by separating them by a pipe (|). As we’d like to give our :AUTHORED relationship more weight than a categorisation, I have added in a quick CASE statement to give the recommendation a score based on the type of connection.

MATCH (p:Post {ID: 110})-[:HAS_TAXONOMY|AUTHORED]-(target)-[:HAS_TAXONOMY|AUTHORED]-(recommendation:Post)
WITH labels(target) as labels, recommendation.title as recommendation, case when 'User' in labels(target) then 10 else 5 end as weight
RETURN recommendation, collect(DISTINCT labels) as labels, sum(weight) as weighting
ORDER BY weighting DESC LIMIT 5
recommendation labels weighting
WordPress Recommendations with Neo4j [[User],[Taxonomy,Category]] 15
Quick TDD setup with Node, ES6, Gulp and Mocha [[User],[Taxonomy,Category]] 15
ES6 Import & Export – A beginners guide [[User],[Taxonomy,Category]] 15
ES6 Promises – 5 Things I Wish I’d Known [[User],[Taxonomy,Category]] 15
2,100 startups in 1 building? [[Taxonomy,Category]] 5

Although the last post is in the same category, as the post is written by another User it receives a lower score than the rest. Now that we’ve got a working query, let’s create a new Recommendation class that we can use in our Themes.

Recommend.php
<?php
namespace Neopress;

use WP_Query;

class Recommend {

/**
* Provide a simple list of recommendations by Taxonomy
*
* @param int $post_id [description]
* @return WP_Query
*/
public static function byTaxonomy($post_id) {
$cypher = '
MATCH (p:Post)-[:HAS_TAXONOMY]->(:Taxonomy)<-[:HAS_TAXONOMY]-(recommended:Post)
WHERE p.ID = {post_id}
AND recommended.status = "publish"
RETURN id(recommended) as ID, recommended.created_at
ORDER BY recommended.created_at DESC
LIMIT 5
';

$params = ['post_id' => $post_id];

$results = Neopress::client()->run($cypher, $params);

// Get Post IDs from Query
$ids = [];

foreach ($results->getRecords() as $row) {
array_push($ids, $row->get('ID'));
}

// Query
return new WP_Query([
'post__in' => $ids
]);
}

}

In this file I have created a static method that will run a Cypher query to get our recommendations, take the ID’s and then use these to run a WP_Query to get the posts from the WordPress with the matching IDs. We can use this in our Themes by calling the method statically.

$posts = Neopress\Recommend::byWeighting(get_the_id());
foreach ($posts as $post) {
// Echo Something
}

Conclusion

Now we’ve done the heavy lifting, we can see that providing contextual recommendations is relatively simple. To improve the recommendations we could look at supplementing the graph with extra information from our posts, or using the updated_post_meta action to add extra meta data and relationships into to the graph. Ultimately, the recommendation engine is only as good as the information.

Next we will look at using Collaborative Filtering to provide a more personalised recommendation.