Counting tokens without a tokenizer
I wanted every page to show what it costs a model to read, and the wiki as a whole to show its total. No tokenizer dependency: this project keeps three runtime dependencies and a vocabulary file is many megabytes.
So, an estimate. I benchmarked three candidates against hand-counted samples spanning English prose, heavy markdown with tables and links, and source code.
| Estimator | Mean absolute error |
|---|---|
| characters ÷ 4 | 11.5% |
| subword-chunk heuristic ÷ 5 | 17.2% |
| words × 1.33 | 21.3% |
The simplest one won, and not narrowly. That result is stable enough to just take: characters ÷ 4, rounded up.
Why the cleverer ones lost
The word-based estimator fails on exactly the content a technical wiki is full of. Punctuation, code, URLs, indentation and table pipes are all tokens and none of them are words, so it undercounts markdown badly while looking fine on prose. Its error is not just larger, it is biased — and a biased estimator on a budget is worse than a noisy one, because you can carry a noisy estimate with a margin and you cannot carry a systematically low one at all.
The chunk heuristic lost for a subtler reason: it was tuned on my intuitions about how words split, and those intuitions are wrong in a specific direction. Common words are single tokens no matter how long, and short unusual strings fragment. Any rule I invent by introspection encodes my sense of what looks complicated, which is uncorrelated with what a learned vocabulary found frequent.
Character count sidesteps all of it. It has no theory, so it has no wrong theory.
More precision would have been fake
The obvious objection is: ship a real tokenizer and be exact. But exact against what? Every model family tokenises differently, and the same text differs across them by well over the error of the estimate. A precise number would be precise about one model and silently wrong about the rest, while looking far more authoritative than the honest approximation.
So the display says ~816 tokens, the tilde is not decoration, and the page
explaining the statistic says which rule produced it. An estimate labelled as an
estimate is more useful than a false exact, because it tells the reader how much
to lean on it.
The general rule: precision that exceeds the accuracy of your model is a lie with error bars removed. Round to the significant figures you can defend.
The part that actually mattered: what to measure
This is where I nearly shipped something misleading, and it had nothing to do with the estimator.
Pages are stored with frontmatter — title, tags, timestamps, provenance — that the reader never receives. Counting the stored file would have added a hundred tokens of bookkeeping to every page and inflated the wiki total by more than ten percent, and every number would have been defensible and useless, because nobody is ever billed for it.
So the count is taken on the body only, and there is a test asserting the whole-wiki total comes in under the naive estimate of the stored bytes, specifically to catch anyone later "fixing" it to measure the file.
The lesson generalises past tokens. When you add a measurement, the choice of estimator is usually the easy part and gets all the attention. The question that decides whether the number is any good is what the number is of — and the right answer is almost always the thing the consumer actually pays for, not the thing that is convenient to measure. Response bytes on the wire, not before compression. Wall-clock the user waits, not CPU in the handler. Body tokens, not file bytes.
Two implementation notes worth stealing
Store it, do not compute it. The count lives in the index as a column, so a
whole-corpus total is one SUM rather than opening every file. Derived values
that get aggregated should be computed once at write and stored, or the
aggregate becomes the slowest thing in the system exactly when the corpus is
worth aggregating.
Publish it before the fetch, not after. The count appears in listings and search results, not only on the page. A per-page number you can only obtain by fetching the page is useless for its main purpose, which is deciding whether to fetch the page. This is the single most useful property of the whole feature and it comes entirely from where the number is shown.