Boosting Search by stock
Related to: Racing the clock on seasonal stock - A deadline-aware boost that knew where to stop, Fading out of stock from Search, instead of yanking It
A popular sofa had five units left in the warehouse. By any normal accounting, that’s “in stock” — the listing page said so, the relevance model gave it full weight, and it sat near the top of search results for “grey sofa.” What the ranking didn’t know was that this particular sofa was selling fifteen units a day. Five units of inventory against that pace is closer to “gone by tonight” than “available.”
The first instinct was to work with the number already sitting in the product database: total units on hand. Penalize anything below some threshold, say fewer than five in stock. It didn’t hold up. Five units of that fast-selling sofa was basically already gone. Five units of a slow-moving accent chair that sells twice a week was weeks of comfortable buffer. Same number, opposite urgency — raw unit count says nothing about how fast those units disappear, and any threshold drawn on that number is really a guess about a category’s typical pace, dressed up as a fact about one item.
What the ranking actually needed wasn’t a count of units. It was a measure of time: given how fast an item is currently selling, how many days would its current stock last? Call that expected_days_of_stock. A sofa with five units and heavy daily demand might have less than a day of runway left, even while the stock count alone still reads "available." An accent chair with the same five units and light demand might have weeks. Once stock is expressed in days instead of units, it becomes directly comparable across an entire catalog, regardless of how fast or slow any individual category normally moves.
With that number available per item, the boost itself is a single ratio, folded into the existing score:
$$final_score = base_relevance \cdot boost$$
$$boost = 1 + \frac{K \cdot expected_days_of_stock}{expected_days_of_stock + half_life}$$
At expected_days_of_stock = 0 — no runway left — the fraction is zero and boost = 1: no bonus, the item ranks on relevance alone. As expected_days_of_stock grows, the fraction climbs toward 1 and boost climbs toward 1 + K: a comfortably-stocked item gets up to a K-sized lift over an otherwise identical item that's about to run out. K sets the ceiling on how much stock can matter relative to relevance — a small K means stock only breaks close ties; a larger one lets it move items further. Framed as a boost for abundance rather than a penalty for scarcity, an item never gets punished below its relevance score for being low on stock; it simply stops earning the bonus that better-stocked competitors get.
half_life is what keeps that curve from being a step function. It's the number of expected days of stock at which an item has earned exactly half the maximum boost — the point where the fraction equals 0.5. Below it, gains come fast: going from half a day of runway to two days moves the needle a lot. Above it, gains taper off — going from thirty days to sixty barely changes the boost, because thirty days was already comfortably safe. One knob, tuned per category to whatever "comfortably stocked" means for how fast that category typically sells.
Computing expected_days_of_stock itself comes down to dividing stock by a sales rate:
$$expected_days_of_stock = \frac{total_stock}{a \cdot M + (1 - a) \cdot N}, 0 < a < 1$$
M is the item's own average daily sales — the most direct signal, when there's enough history behind it. N is the average daily sales among the category's top sellers, standing in for "how fast an item like this typically moves." a weights how much to trust the item's own number against that category benchmark. A brand-new listing has no real M to speak of — zero or one lucky sale tells you almost nothing — so leaning on N avoids treating an unproven item as either doomed or immortal. An item with months of steady sales history has an M worth trusting on its own.
That argues for a itself moving with how much history an item actually has. A simple version: count the number of days in the last 30 that the item was available for sale at all, and scale a by that fraction —
$$a = \frac{days_available_last_30}{30}$$
A listing available for all 30 days gets a = 1: its own sales history fully determines its rate, no category blending needed. A listing available for 3 of the last 30 days gets a = 0.1: almost entirely deferring to the category benchmark, since three days of data barely qualifies as a track record yet. As the item accumulates more selling days, a rises on its own, and the formula gradually shifts its trust from the category's average to the item's own proven pace — no separate rule needed for "new" versus "established" items, just the same fraction recalculated as history accumulates.
It’s still an estimate, not a guarantee. M and N are both historical averages, and a sudden spike — a listing that goes viral overnight — can burn through stock far faster than its recent history predicts, so the formula reacts a beat behind reality rather than ahead of it. half_life is a good feature for category responsible to manage. K is still tuned by hand but it can be learned. A formula built from sales counts and stock counts the system already tracked did more to keep search results buyable than the demand-forecasting model that would have been the "proper" way to solve it.
Comments
Join the discussionNo comments yet. Be the first to comment.