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 & Data Modelling
  2. Part 2: Content Based Recommendations
  3. Part 3: Collaborative Filtering
  4. WordPress Recommendations with Neo4j – Part 4: PageRank with APOC Procedures

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.

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.

// 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),
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
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.

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


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.