Every guide tells you to store a popularity score in a rank_feature field rather than an ordinary number. The reason has nothing to do with reading numbers faster.
Part 1 works out why a popularity signal is added to the text score and capped at a few points rather than multiplied into it. Part 2 is what that cap buys you on top: the engine gets to skip most of your index without reading it.
The insight: the engine needs a ceiling
Say you are boosting a user search by follower count, added to the text score as a bounded number of points and capped at three — the shape part 1 settles on.
A search engine can only skip a document if it knows the most that document could possibly score.
A cap of three is exactly that number. No document can gain more than three points from being popular, ever, whatever its follower count is — that is what bounding it means.
Compute the same adjustment in a script, or multiply it in, and no such number exists. The engine has to assume any document could score anything, so it looks at all of them.
What "skipping" actually skips
The list of documents behind a term is not one long run. It is cut into fixed blocks of 128src, and written next to every block is a handful of bytes recording the best score any document inside it could produce. Lucene calls those the block's impacts, and their stated job is to skip score calculation for uncompetitive documentssrc.
So a search for the top 10 goes like this.
- Score documents until ten are in hand. The tenth-best score so far becomes the bar — say it settles at 12.5.
- Arrive at the next block. Read its recorded maximum before touching a single document in it: the best this block could possibly manage is 11.
- 11 is under 12.5, so the block is never opened. 128 documents, not decoded, not scored, not counted — one comparison and a seek straight past them.
There is a coarser tier above that one. The same maximum is recorded again for every run of 32 blockssrc, so when a whole stretch of the list is hopeless the engine steps over 4,096 documents on a single check.
This is block-max WAND, the algorithm Lucene added for exactly thissrc, and OpenSearch is built on Lucene.
Note what is being skipped, because the word misleads. The documents that get skipped are matches. They satisfy the query. Nobody ever looks at them, so nobody ever finds out by how much.
Are the good blocks first?
That walkthrough invites a tidy story, and I told it to myself for a day: rank_feature must keep the follower counts in descending order, so the million-follower accounts land together in block 1, the bar shoots up on the first read, and everything after is cheap.
It doesn't. Nothing here is sorted by popularity at all.
A block is a run of 128 consecutive document IDs, not a run of similar scores. Its recorded maximum is simply the best of whatever happened to land in it, and a 900,000-follower account can sit in the last block on disk. The skipping does not come from the good documents being first. It comes from the engine knowing a block's ceiling before it opens the block.
Where that ceiling comes from
The answer is stranger and better than sorting. The feature value is encoded as a term frequencysrc. The field is one term in an ordinary posting list, and the value rides in the slot BM25 would otherwise use for how many times a word occurs here.
Impacts are only written for fields whose frequencies are indexedsrc. Dress your follower count as a frequency and it qualifies, so every block of it gets a ceiling written beside it by the machinery that was already making text queries fast. The feature inherits all of it for nothing.
Why a plain integer can't do this
Store followers as a long and the number lands in doc values: a column you can read only once you already hold the document. There is no per-block maximum to consult, nothing to compare against the bar, and so nothing can be skipped. Elasticsearch says the same of function_score in a single sentence: unlike rank_feature, it does not skipsrc.
The problem was never that an integer is slow to read. It is that an integer arrives with no promise about how large it might turn out to be.
Why top 10 skips more than top 1,000
The bar is the Nth-best score so far, so N sets its height. Ask for ten and the bar is the tenth-best score: high, and reached after a few hundred documents.
Ask for a thousand and the bar is the thousandth-best. Far lower, and far fewer blocks can be proved hopeless against it. Deep paging and skipping pull in opposite directions — every extra result you ask for lowers the bar that was doing the work.
The flat version of the same rule: asking for an exact hit count skips nothing at all, because counting every match means visiting every matchsrc. OpenSearch stops counting at 10,000 by defaultsrc.
That approximation you have been ignoring in hits.total is what pays for the speed. Set track_total_hits to true and the rank_feature query quietly stops skippingsrc.
“A search engine can only skip a document if it knows the most that document could possibly score.” Knowing the ceiling is only worth having if you have also given the engine permission to stop climbing. An exact hit count withdraws it.
What it costs you
The field keeps only the top 9 significant bits of the value, held on 16 bits internally, for a relative precision of about 0.4%src. Two accounts whose follower counts differ by a tenth of a percent can tie.
For ranking that is noise. If you need the real number on the page, read it out of _source like any other field.
If you arrived here first: part 1 is the argument for capping the boost at all, which is the assumption everything above rests on.
The one-paragraph version
A rank_feature field stores your popularity value as a term frequency, which means Lucene writes down, beside every block of 128 documents, the best score any document in that block could reach. Ask for the top ten and the engine keeps the tenth-best score so far as a bar, then seeks past any block whose recorded ceiling sits under it, without decoding a thing. None of that works without a bounded boost, because a boost with no maximum makes every ceiling infinite and every block worth opening. The bound you added for ranking reasons is what buys the speed.