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).
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 therequest
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(); ?>
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
Hope this helps.