Using database meta_values to calculate new post order using pre_get_posts or a ‘request’ hook

UPDATE: I have worked out my implementation of the selected answer at the bottom of this post.

I’m implementing a sorting algorithm based on Reddit’s hotness algorithm, in addition to a time-decay (like Hacker News hotness algorithm).

Read More

The code I am currently satisfied with is a result of trial and errors in some Excel sheets and whatnot. Here’s the working code in PHP:

<?php get_header();
$timenow = time() - 1211380200; // this is my custom Epoch time, it translates to when my first WP post was made ?>

<?php
if ( have_posts() ) : while ( have_posts() ) : the_post(); 

$hearts = get_post_meta($post->ID, '_tjnz_hearts', true);
$plays      = get_post_meta($post->ID, '_tjnz_deals_plays', true);
$downloads  = get_post_meta($post->ID, '_tjnz_deals_downloads', true);
$ups        = get_post_meta($post->ID, '_tjnz_temperature_upvotes', true);
$downs      = get_post_meta($post->ID, '_tjnz_temperature_downvotes', true);
$date       = get_post_time('U', true); // fetches the GMT postdate of post in Unix format

$score = ( ( $hearts * 2 ) + $plays + $downloads + $ups ) - $downs;
$order = log( max( abs( $score ), 1 ), 6 );
$seconds = $date - 1211380200;
if( $score > 0 ) { $sign = 1;   } elseif( $score < 0 ) { $sign = -1; } else { $sign = 0; }
$hotness = round( $order + ( ( $sign * $seconds ) / 336000 ), 7 );
$degrees = round( ( $order * ( $sign * 32 ) ) + ( ( -4 * ( $timenow - $seconds ) ) / 336000 ), 7 ); ?>
        <p>
            <?php the_title(); ?> <?php echo $post->ID; ?><br />
            <?php echo 'hearts: ' . $hearts . '<br />';
            echo 'plays: ' .$plays . '<br />';
            echo 'dls: ' .$downloads . '<br />';
            echo 'up: ' .$ups . '<br />';
            echo 'down: ' .$downs . '<br />';
            echo 'date: ' .$date . '<br /><br />';
            echo 'score: ' .$score . '<br />';
            echo 'order: ' .$order . '<br />';
            echo 'seconds: ' .$seconds . '<br />';
            echo 'timenow: ' .$timenow . '<br />';
            echo 'difference: ' .($timenow - $seconds) . '<br />';
            echo 'sign: ' .$sign . '<br /><br />';
            echo 'hotness: ' .$hotness . '<br />';
            echo 'degrees: ' .$degrees; ?><hr />
        </p>

<?php endwhile; else: ?>
<p><?php _e('Sorry, no posts matched your criteria.'); ?></p>
<?php endif; ?>

The problem

The problem with this PHP code is that the WordPress query was already made, so it just returns the posts in anti-chronological order.

  • I want to sort posts by ‘Hotness’ score
  • I don’t want to use a new WP_Query object or query_posts() because that will stomp on the original query, which causes unnecessary Database interactions. The site is fairly busy and I’m hosting over a thousand posts now.
  • I’m not sure how to utilize pre_get_posts or use the request hook. Yes, I have read the documentation but for me it’s getting too advanced now.

The database structure

I am saving meta_values in table wp_postmeta, as you can see from my PHP code snippet. The actual Hotness score is calculated on-the-fly, and not saved into the database because that wouldn’t be very efficient and most of all, not real time for users.

My actual question

I can’t sort the query by meta_value, because the meta_values are only part of the equation. I want to use the result of the equation as sorting order.

How do I change the main WordPress query to look at the meta_values, calculate each posts hotness, and return those posts from hottest to coldest?

My implementation of the chosen answer

In functions.php I have added the following two functions.

// add custom wp_postmeta when a new post is created
add_action( 'wp_insert_post', 'tjnz_prepare_postmeta' );
function tjnz_prepare_postmeta( $post_id ) {
    if ( !wp_is_post_revision( $post_id ) ) {
        $hotness = round( ( time() - 1211380200 ) / 336000, 7 );
        add_post_meta( $post_id, '_tjnz_hearts', 0, true );
        add_post_meta( $post_id, '_tjnz_plays', 0, true );
        add_post_meta( $post_id, '_tjnz_downloads', 0, true );
        add_post_meta( $post_id, '_tjnz_upvotes', 0.000, true );
        add_post_meta( $post_id, '_tjnz_downvotes', 0.000, true );
        add_post_meta( $post_id, '_tjnz_hotness', $hotness, true );
    }
}

// build an array of Hotness stats for post
function tjnz_temperature( $tjnz_post_id, $tjnz_timenow ) {
    $hearts     = get_post_meta($tjnz_post_id, '_tjnz_hearts', true);
    $plays      = get_post_meta($tjnz_post_id, '_tjnz_plays', true);
    $downloads  = get_post_meta($tjnz_post_id, '_tjnz_downloads', true);
    $ups        = get_post_meta($tjnz_post_id, '_tjnz_upvotes', true);
    $downs      = get_post_meta($tjnz_post_id, '_tjnz_downvotes', true);
    $hotness    = get_post_meta($tjnz_post_id, '_tjnz_hotness', true);
    $date       = get_post_time('U', true);
    $score      = $hearts + $downloads + $ups - $downs;
    $log        = log( max( abs( $score ), 1 ), 6 );
    $seconds    = $date - 1211380200;
    if( $score >= 0 ) { 
        $sign = 1;
    } else { 
        $sign = -1;
    }
    $degrees    = round( ( $log * ( $sign * 32 ) ) + ( ( -9.6 * ( $tjnz_timenow - $seconds ) ) / 336000 ) + 10, 7 );
    //      round to 7 digits    positive/negative    -2.4 degrees/day      realtime post age       +10 free degrees

    return array(
        'hearts'    => $hearts,
        'plays'     => $plays,
        'downloads' => $downloads,
        'ups'       => $ups,
        'downs'     => $downs,
        'score'     => $score,
        'hotness'   => $hotness,
        'degrees'   => $degrees
    );
}

_tjnz_hotness is now a meta_value for every published post, and will be updated every time a user upvotes, downvotes, downloads, or favorites (hearts) a post. The value of _tjnz_hotness is initially based on the publish time (GMT) of the post with a score of 0. The publish time is based on the Unix Epoch timestamp of the post, minus my personal Epoch (the date my blog launched). It really doesn’t matter what that number is, it just makes the _tjnz_hotness value lower. A post published right after my blog launched will have a value close to 0.

_tjnz_degrees is calculated on every page load. The page uses function tjnz_temperature() to calculate the degrees value at the time the page was loaded. It fetches all the meta info from the post, and uses it to calculate the current temperature of the post.

The main difference between hotness and degrees, is that hotness is used for the actual sorting of the post. Increasing/decreasing this value basically offsets the post on a timeline, relative to other posts. The degrees value is based on this, but it takes the realtime age of a post into account.

Essentially, what this means is that if a post’s hotness doesn’t change, the degrees value actually starts dropping (0.1 degree per hour, 2.4 per day).

The setup of the log base 6 also makes it so that more and more upvotes are needed to keep the post ‘hot’. Eventually there will be a point where it simply can’t win from newer posts anymore. With my formula, it takes about 5-7 days for a post to become inevitably ‘cold’.

On my custom page, I list the posts as follows (it’s a debug output, no actual post content is shown yet).

<?php
/*
Template Name: Hot
*/
get_header();
$timenow = time() - 1211380200; 
$hot_query = new WP_Query(
    array(
        'post_status' => 'publish',
        'post_type' => 'post',
        'meta_key' => '_tjnz_hotness',
        'posts_per_page' => 30,
        'orderby' => 'meta_value_num',
        'order' => 'DESC'
    )
);

if ( $hot_query->have_posts() ) : while ( $hot_query->have_posts() ) : $hot_query->the_post(); 
    $tjnz_temperature = tjnz_temperature( $post->ID, $timenow );
?>
    <?php the_title(); ?> <?php echo $post->ID; ?><br />
    <?php echo 'hearts: ' . $tjnz_temperature['hearts'] . '<br />';
    echo 'plays: ' . $tjnz_temperature['plays'] . '<br />';
    echo 'dls: ' . $tjnz_temperature['downloads'] . '<br />';
    echo 'up: ' . $tjnz_temperature['ups'] . '<br />';
    echo 'down: ' . $tjnz_temperature['downs'] . '<br />';
    echo 'score: ' . $tjnz_temperature['score'] . '<br />';
    echo 'hotness: ' . $tjnz_temperature['hotness'] . '<br />';
    echo 'degrees: ' . $tjnz_temperature['degrees']; ?><hr />

<?php endwhile; else : ?>
    <p>Oops, Post Not Found!</p>

<?php endif; get_footer(); ?>

Related posts

Leave a Reply

1 comment

  1. The other day I was thinking on how to make something like this, I would recommend to save the hottnes as a post meta value, that is updated on every vote, save or update, for that you will need the save_posts filter and then you can get the posts ordered with pre_get_posts and a meta query something like

    $query->set('meta_key'=>'hottnes');
    $query->set('orderby'=>'meta_value_num');
    $query->set('order'=>'DESC');
    

    Hope this helps.