Skip to content

Blog ·

A tag that half the corpus carries

One tag sat on 75 of 127 articles, so the related block fell back to the publication date and 71 pages received no incoming tile at all.

By Provibr

Under every article on this site there is a block of four tiles headed read next. It is the only automatic internal linking we have; everything else is a link somebody wrote into a sentence.

The block had been in place for months and nobody had asked what it chose. When we measured it, on the rendered pages of a production build rather than on the function that fills them, the answer was that it chose almost nothing. Of the 127 English articles, 71 received zero incoming tiles. The median article was linked to by nobody. Four articles received 304 of the 508 tiles that exist, at 88, 76, 72 and 68 each.

A block that never names 71 pages is not internal linking. It is a top four list with a heading over it.

The tag was doing no work

The old selection worked in three family layers: same family with a shared tag, other family with a shared tag, same family without one. Inside each layer it sorted by publication date and took four.

That reads like a sensible ordering, and on this corpus it was a machine for producing orphans, for a reason that has nothing to do with the sorting: proxmox sits on 75 of the 127 English articles, or 59 per cent. Almost every technical note this project writes happens on Proxmox, so almost every note got the tag.

A tag that half the corpus carries does not distinguish anything. Layer one was therefore nearly always full, and inside a full layer by date means the four newest articles. 300 of the 508 neighbour pairs shared that one tag and nothing else, and for 75 articles that was true of all four neighbours at once. The clearest symptom was in the comparison pages: the four alternatives articles about competing products tiled the same four recent notes, and so never each other and never their own head to head comparison.

A second measurement says the same thing from the other side, and it decided how we tagged the articles you are reading now. Of the 75 English articles carrying proxmox, 43 do not contain the word Proxmox anywhere in the body. The tag had stopped meaning this is about Proxmox and started meaning this is one of ours.

One score, three kinds of term, no manual bonuses

The replacement is a single score. Two articles are neighbours to the degree that they share terms, and a term is worth ln(N / df): the classic inverse document frequency, with N the number of articles in that language and df the number carrying the term.

Three kinds of term live in one space, prefixed so they cannot collide: tag: from the frontmatter, vorm: from the article's shape as read off its slug and family, and over: from the subject words in the slug.

The weighting falls out rather than being set. On the corpus we measured, tag:proxmox is worth 0.53 and tag:dedicated 2.45. Nobody typed either number, and nobody has to remember to lower the first one when the sixteenth Proxmox note lands.

The subject vocabulary is derived rather than typed. It is the union of the integration slugs the site already keeps and the about values of the competitor claim blocks, which is where names like solusvm, tenantos, virtfusion and virtualizor come from: exactly the words that are the subject of a page and are not an integration. A third hand written list would have been the fourth place the same name lives, and the first to go stale.

Weighting the tags alone is worse, not half better

The tempting small change is to keep the tag as the only signal and just weight it. We measured that variant before choosing, because it is what somebody will propose the next time this comes up.

It produces 74 orphans instead of 71, and of the 22 alternatives and comparison pages exactly one kept two neighbours of its own shape, down from nine. Weighting a signal that does not discriminate does not make it discriminate. The discrimination comes from vorm: and over:; the inverse document frequency only decides how the weight is divided.

If a feature has almost no variance, no weighting scheme rescues it, and a clever formula over a bad feature looks like progress because the numbers move.

The tie break is half the repair

Here is the part we would not have predicted. On 75 articles that share nothing but the note shape and the common tag, the score is identical. Every one of those pairs is a tie, so the tie break decides everything, not the edge cases.

Sorting ties by date is the old behaviour wearing a new coat: the four newest again. The tie break is now the least tiled article first, with date and slug behind it as a stable finish.

That single change is what took the orphan count to zero, and it costs something real in shape: it cannot be computed per article. You have to walk the whole language once, in a fixed order, counting what has already been handed out.

In English: median 0 becomes 4, maximum 88 becomes 12, orphans 71 become 0. Every other language lands on the same median and the same zero, with a maximum of 13. Of the 46 neighbour pairs that share no tag at all, all 46 now share the shape, where before 37 pairs shared nothing whatsoever.

We also measured a penalty inside the score itself, subtracting a quarter of an article's existing tile count. It flattens slightly further, minimum two instead of one, and it buys that with relevance: the share of neighbours with nothing in common goes from 9.1 to 12.2 per cent. A tie break can by construction only reorder candidates that already scored the same, so it can never push a better neighbour out. A penalty can.

What a guard can honestly assert here

The existing test for this block checked that the block was full. It passed throughout, and it was right to: four tiles were rendered on every page. It had no opinion about whether they were about anything.

The obvious strict rule for the replacement is wrong. Every alternatives and comparison page must have at least two neighbours of its own shape sounds like the property we want, and it would force worse neighbours: one of those pages is about cPanel alternatives, and its four best neighbours are four cPanel articles with zero shape agreement. That is correct, and a rule demanding otherwise would break it. The guard therefore names the nine pages the round was actually about.

And the property you want may simply not exist on your data. No pair may share only the most common tag is unsatisfiable here. The honest form is comparative: no unchosen candidate scores higher than the weakest chosen neighbour. That is checkable, it is the actual definition of correct, and it does not pretend the corpus is something it is not.

What we would write down

  1. Measure the rendered pages, not the function. The distribution is the defect; the function looked fine.
  2. A tag on 59 per cent of a corpus is not a signal. It is a house style marker.
  3. Check whether your tag still means what its name says. 43 of 75 did not.
  4. Layered selection with a sort inside each layer is a sort. The first layer was nearly always enough.
  5. Sorting ties by date reintroduces recency bias wherever ties are common, and on a uniform corpus ties are the normal case.
  6. The tie break can be the whole repair. Ours took orphans from 71 to zero.
  7. Weighting a feature with no variance changes nothing. Measure the tempting half fix before rejecting it by argument.
  8. Prefer a tie break to a penalty. A tie break cannot displace a better candidate; ours would have cost 3.1 points of relevance if it had.
  9. Derive your vocabulary from a register you already keep. A fourth copy of a product name is the first to go stale.
  10. If the property you want is unsatisfiable, write the comparative one. Nothing unchosen beats the weakest chosen is checkable and true.

For clarity about what this is: the before and after distributions were read out of the DOM of two production builds, 20 of 20 pages agreed with the selection function directly, two separate builds of the same commit were byte identical over 18 tile sections, a crawl of 1758 pages found no orphans and no dead links, the new guard carries 13 tests of which 5 were proven red first, the web suite finished at 4703 green and the content check at 923 of 923. Not proven: the ranking effect of any of this, which is measurable in four to six weeks and not before, against the recorded list of the 71 pages that used to receive nothing.

From the platform work on Provibr. What a title and the links pointing at it owe each other is in three faults no script could see, why the same article can end up with two names is in two names for the same article, and the case where a heading and a page title must differ is in when the title cannot be the heading.

Start with one host and one agent

Create an account, install the agent with a single command and connect your first Proxmox host. The trial runs for 14 days and asks for no payment details.