<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Content Ranking · Jared Lynskey</title>
    <link>https://jared.lynskey.co.nz/en/tags/content-ranking/</link>
    <description>Short stories of value on software engineering and engineering leadership — performance reviews, hiring and scaling teams, CI/CD, DevOps monitoring and agentic AI. Written from Seoul.</description>
    <generator>Hugo</generator>
    <language>en</language>
    <copyright>© 2026 Jared Lynskey</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/en/tags/content-ranking/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Content Ranking Algorithms: Signals Beyond Views and Likes</title>
      <link>https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/</guid>
      <pubDate>Fri, 06 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>content ranking</category>
      <category>metrics</category>
      <category>SEO</category>
      <category>machine learning</category>
      <category>user engagement</category>
      <category>content strategy</category>
      <description>What a content ranking algorithm does, why ranking on views and likes alone makes the popular more popular, and the quieter signals worth blending into one score.</description>
      <content:encoded><![CDATA[<p>A content ranking algorithm decides the order a feed, a search page or a &ldquo;top posts&rdquo; list shows things in. It gives every item a score built from several signals (how people engage with it, what the content itself is like, how fresh and relevant it is) and sorts by that score. Rank on views and likes alone, though, and you get a rich-get-richer loop: whatever sits at the top gets seen, so it collects more views and likes, so it stays at the top.</p>
<p>That&rsquo;s the obvious flaw in views and upvotes: they reward whatever&rsquo;s already popular. Fresh content starves at the bottom of the feed, and the platform slowly turns into a rerun channel. Ranking a feed well is one of the quietly hard problems where I work, so I&rsquo;ve spent a fair bit of time thinking about which other signals are worth trusting.</p>

<h3 class="relative group">Watch what people do, not what they tap
    <div id="watch-what-people-do-not-what-they-tap" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#watch-what-people-do-not-what-they-tap" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Explicit feedback is scarce and biased; behaviour is plentiful. Time on page and scroll depth tell you whether someone actually read the thing or bailed after the first paragraph. Revisits are even better — coming back to a piece is a stronger endorsement than any upvote. And a fast exit is a useful negative signal: the content promised something the reader didn&rsquo;t get.</p>

<h3 class="relative group">Signals baked into the content itself
    <div id="signals-baked-into-the-content-itself" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#signals-baked-into-the-content-itself" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Good content doesn&rsquo;t always collect early likes, but it usually shows its quality in other ways. Novelty is one — does the piece cover ground the platform hasn&rsquo;t seen? Structure is another; clean formatting, decent grammar and clarity correlate with quality more than we&rsquo;d like to admit. An author&rsquo;s track record is a fair prior too, judged on how their past work held readers rather than on raw like counts. Early comments help as well: three genuinely positive comments beat thirty reflexive upvotes, and a bit of semantic analysis can flag content that fills a gap the platform has been missing.</p>

<h3 class="relative group">Borrow signals from outside
    <div id="borrow-signals-from-outside" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#borrow-signals-from-outside" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The wider internet tells you what&rsquo;s resonating right now. Content that lines up with trending search keywords has built-in relevance, and quick publishing on a rising topic deserves a boost. What&rsquo;s buzzing on Twitter or Reddit is often a preview of what your own users will want next — with a little prediction you can promote matching fresh content before the wave instead of after it. Your own search box is underrated too: if users keep searching for a topic, or a piece pulls steady traffic in from search engines, that&rsquo;s relevance you didn&rsquo;t have to guess at.</p>

<h3 class="relative group">Blending the signals into one score
    <div id="blending-the-signals-into-one-score" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#blending-the-signals-into-one-score" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Here&rsquo;s the idea in a few lines of Python. It&rsquo;s an illustration, not a production system. Each signal is normalised to a 0 to 1 range so that one with big raw numbers can&rsquo;t drown the rest, a weighted sum turns them into a single score, and a time decay gives a new post a fair shot against last month&rsquo;s hit.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">WEIGHTS</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;read_depth&#34;</span><span class="p">:</span> <span class="mf">0.3</span><span class="p">,</span> <span class="s2">&#34;revisits&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;quality&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;early_comments&#34;</span><span class="p">:</span> <span class="mf">0.2</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">HALF_LIFE_HOURS</span> <span class="o">=</span> <span class="mi">48</span>  <span class="c1"># a post loses half its score every two days</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalise</span><span class="p">(</span><span class="n">values</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">lo</span><span class="p">,</span> <span class="n">hi</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">values</span><span class="p">),</span> <span class="nb">max</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">v</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">hi</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="k">if</span> <span class="n">hi</span> <span class="o">&gt;</span> <span class="n">lo</span> <span class="k">else</span> <span class="mf">0.0</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">rank</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;items: dicts holding raw signal values plus &#34;age_hours&#34;.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">norm</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="n">normalise</span><span class="p">([</span><span class="n">item</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">items</span><span class="p">])</span> <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">item</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">base</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">weight</span> <span class="o">*</span> <span class="n">norm</span><span class="p">[</span><span class="n">name</span><span class="p">][</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">weight</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="o">.</span><span class="n">items</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">base</span> <span class="o">*</span> <span class="mf">0.5</span> <span class="o">**</span> <span class="p">(</span><span class="n">item</span><span class="p">[</span><span class="s2">&#34;age_hours&#34;</span><span class="p">]</span> <span class="o">/</span> <span class="n">HALF_LIFE_HOURS</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">order</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">items</span><span class="p">)),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">i</span><span class="p">:</span> <span class="n">scores</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">items</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">order</span><span class="p">]</span></span></span></code></pre></div></div>
<p>The weights are guesses until the tests below say otherwise. Views and likes can still go in; they just become one input among several instead of the whole score. The half-life decides how quickly old posts sink, which is a topic of its own: I went into it in <a href="https://jared.lynskey.co.nz/en/posts/2023/2023-09-27-simulating-views/" >how feed ranking algorithms decide when to bury old posts</a>. Real systems usually learn the weights with a model rather than picking them by hand, but the shape is the same: many signals in, one number out, sort.</p>

<h3 class="relative group">Then test it honestly
    <div id="then-test-it-honestly" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#then-test-it-honestly" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>However clever the signals, A/B test them — show new content to a slice of users and compare its engagement against the incumbents. And watch how things spread: shares, mentions and embeds, on and off the platform, say more about value than a view counter ever will.</p>
<p>No single metric survives contact with reality. The rankings that actually work blend behaviour, intrinsic quality, timeliness and honest experiments, and no one signal gets the final word.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>How Feed Ranking Algorithms Decide When to Bury Old Posts</title>
      <link>https://jared.lynskey.co.nz/en/posts/2023/2023-09-27-simulating-views/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/en/posts/2023/2023-09-27-simulating-views/</guid>
      <pubDate>Wed, 27 Sep 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Content Ranking</category>
      <category>Decay</category>
      <category>Freshness</category>
      <category>Engagement</category>
      <description>How feed ranking algorithms use decay functions to trade freshness off against engagement, from someone who works on a content feed for a living.</description>
      <content:encoded><![CDATA[<p>I work on a content platform, so I spend more time than most people thinking about a question users never see: when should the feed stop showing something?</p>
<p>Content goes stale. People open the app wanting to know what&rsquo;s new, and the feed has to deliver that. But a great post from last month shouldn&rsquo;t vanish just because it&rsquo;s no longer new. Decay functions are the standard way to model that trade-off, and they&rsquo;re simpler than the name suggests.</p>

<h4 class="relative group">Freshness matters, but not everywhere
    <div id="freshness-matters-but-not-everywhere" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#freshness-matters-but-not-everywhere" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>Open a news site or a social feed and you&rsquo;re usually after the latest thing. In fast-moving domains like news or financial markets, last week&rsquo;s content is dead weight, so rankings there lean hard on recency.</p>
<p>Not every domain works like that, though. Academic writing and long-form journalism hold their value over time, sometimes even gain it. Age matters in most ranking systems, but how much weight it deserves depends entirely on what you&rsquo;re ranking.</p>

<h4 class="relative group">Modelling the fade with exponential decay
    <div id="modelling-the-fade-with-exponential-decay" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#modelling-the-fade-with-exponential-decay" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>The usual way to model fading relevance is exponential decay: the older an item gets, the faster its perceived value drops. In its simplest form:</p>
<p><code>relevance = e^(-decay_rate × age)</code></p>
<p>where relevance is the current value of the content, the decay rate controls how quickly it loses that value, and age is usually measured in days since publication.</p>
<p>What I like about this model is that it&rsquo;s one knob. Tune the decay rate and you decide whether content stays relevant for hours, days, or weeks. That&rsquo;s the entire configuration surface.</p>

<h4 class="relative group">Engagement keeps good content alive
    <div id="engagement-keeps-good-content-alive" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#engagement-keeps-good-content-alive" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>Age is only half the picture. Views, likes and comments tell you whether people actually care, and a ranking that ignores them buries good work along with the bad. The simple fix is to multiply the two together:</p>
<p><code>score = engagement × e^(-decay_rate × age)</code></p>
<p>Now an older post with strong engagement can still outrank a mediocre new one. It just needs more and more engagement to hold its spot as the decay term shrinks underneath it.</p>
<p>In practice the lifecycle looks like this: a new post gets a freshness boost and a fair shot at being seen. If people view it, like it, comment on it, the score climbs. As days pass, decay eats into it, and only posts with sustained engagement stay near the top. Fresh content gets its chance; genuinely good content earns its stay.</p>
<p>That&rsquo;s honestly the whole trick. One exponential, one engagement term, one knob to turn. The hard part is deciding what the decay rate should be for your particular content, and that part is all judgement.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-09-27-simulating-views/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
