Skip to content
writing
RankingProduct31 August 2026 · 5 min read

If a ranking term is unbounded, your weights are decoration

A feed score with a small weight on engagement can still be almost entirely engagement. Bounding every term is what makes the numbers in the formula mean anything at all.

Here is the shape of the feed ranking in D Plus+, with example weights:

score = 0.40 * interest
      + 0.20 * level_fit
      + 0.10 * community
      + 0.10 * engagement
      + 0.20 * recency

Five signals, weights summing to one. It reads like a statement of priorities: interest matters most, engagement matters least.

That statement is only true because of a rule that is nowhere in the formula.

Every term is bounded

Not "roughly normalised". Not "usually in that range". Bounded, by construction, to the same interval, with no input that can push it out.

Take engagement. Suppose it were a raw count — likes plus comments. A post with four hundred likes contributes 40 to its score. Every other signal put together contributes at most 0.90.

The weight still says 0.10. The ranking is now entirely engagement. Nothing in the code is wrong; nothing throws; the formula still sums to one on paper. The weights became decoration the moment one term could leave the interval.

That is the whole failure mode, and it is easy to ship because the bug has no symptom except a feed that slowly turns into a popularity chart. So engagement is squashed against a ceiling — a good post and a viral one stop being different at the point where the difference stopped telling you anything — and freshness decays smoothly instead of being a timestamp.

The other half: what you refuse to learn from

Interests are updated by what the learner does, which raises the question of which events count.

Impressions are the tempting one. They are free, plentiful, and every analytics tutorial treats them as signal. They are also poison here, because they close a loop: show a topic, the interest rises, the topic is shown more, the interest rises again. Nothing in that loop involves the learner wanting the topic. The ranker converges on whatever it happened to show first. It will look like it is learning. It is only listening to itself.

So impressions carry no weight at all. Only deliberate engagement moves an interest, and interests fade over weeks so a shifting focus moves the feed without any setting to change.

Pagination is where live scores punish you

A score that depends on the current time, on interests that change mid-scroll and on live counts has no stable sort key. The standard cursor trick — remember the last score, ask for everything below it — compares against a value that no longer exists by page three, serving some posts twice and skipping others permanently.

The fix is to stop pretending the ordering is stable and track delivery instead: remember what was handed out, exclude it next time. Calling the feed again is the next page. There is no cursor to be wrong.

The rule underneath all three

Each of these is the same rule wearing different clothes: be explicit about the range of every number you multiply by a weight, and about which events are allowed to teach the system anything.

A ranking function is a sentence about priorities. Bounds are what stop it being a lie.