Personalized & e-commerce / marketplace search
A web query asks "what is the best answer?" An e-commerce query asks "what should I buy, in stock, right now?" Those are different objectives — and personalization is a scalpel, not a sledgehammer.
- When to personalize — the senior judgement: head/navigational queries look the same for everyone; ambiguous and long-tail queries are where personalization pays. Always by re-ranking the top set, never by re-retrieving.
- E-commerce as multi-objective ranking — blended score s = α·relevance + β·p(convert) + γ·margin, why each component must be calibrated before you add them, and why the weights are a product decision.
- Hard constraints & facets — structured filters from query tagging, facet-count computation, and the post-filter recall problem (an ANN top-1000 collapsing to 6) with its fix.
- Marketplace two-sidedness — availability, geo/dates, inventory freshness, and seller fairness / cold-start, and why a marketplace cannot purely maximize buyer relevance.
- Governance of the thumb on the scale — tie-breaking, business boosts and pins, and how to keep "we promoted this" auditable.
- A blended-ranking explorer widget: slide α/β/γ and watch a clearly-relevant item get buried — and read the trust cost.
This lesson depends on three earlier ones. Query understanding tagged the query into attributes (brand, price, size) in 41; we consume those tags as hard constraints. Learning-to-rank in 44 already folds user/context features into a ranker; here we make the multi-objective blend explicit and decide when personalization is even appropriate. And the post-filter recall problem we hit was first introduced for ANN in 03 — we make it concrete with facet arithmetic. It sets up evaluation in 48, because once relevance is blended with business value, "is this result good?" stops being a single number.
When to personalize — the key judgement
The reflex of a junior engineer is "personalization is good, apply it everywhere." That is wrong, and the reason it's wrong is the single most valuable thing in this lesson. Personalization helps when the query is ambiguous and hurts when the query is clear.
Think about facebook login. There is exactly one correct answer and everyone wants the same one. Personalizing it does nothing useful (the top result is already locked) and risks doing harm (you push the canonical login page down to surface something "for you"). Now think about jaguar — animal, car, NFL team, guitar amp, OS version. Or python — language, snake, Monty. Here the query under-specifies intent, and a signal about who is asking resolves it: a user who searched django orm ten minutes ago means the language; a user browsing a zoo site means the animal. This is the partition that matters:
| Query class | Example | Personalize? | Why |
|---|---|---|---|
| Navigational | facebook login, chase bank | No | Single correct target. Personalizing only risks demoting it. Everyone gets the same SERP. |
| Head / unambiguous | iphone 15 case | Lightly | Intent is clear; only tie-break personalization (size, prior brand) on an already-relevant set. |
| Ambiguous | jaguar, python, mercury | Yes | Intent under-specified. User/session/profile signal disambiguates. Highest payoff. |
| Informational / exploratory | best running shoes | Yes | No single answer. Prior behaviour (trail vs road, budget) genuinely improves the set. |
| Long-tail / sparse | vintage mid-century walnut credenza | Yes | Few results, weak relevance signal — personalization fills the gap that text matching can't. |
How do you know which class a query is in? You measure query entropy — the spread of clicks across results, aggregated over all users. A navigational query has near-zero entropy (95%+ of clicks go to one URL); an ambiguous query has high entropy (clicks spread across many results). Concretely, if a query's top result earns a click-through rate above ~0.7 across the population, treat it as navigational and turn personalization off. This is a per-query gate, learned offline, not a global switch.
The signals you personalize with, roughly in order of how much they move the ranking and how risky they are:
| Signal | Latency to acquire | Strength | Risk |
|---|---|---|---|
| Session (last N queries/clicks this visit) | milliseconds, in-request | strongest for disambiguation | low — reflects current intent |
| Short-term history (last few days) | cached per-user | strong for category affinity | medium — intent decays |
| Long-term profile (segments, brand affinity) | offline, see 31 | weak but stable | medium — stereotyping / filter bubble |
| Location (geo, store proximity) | in-request | strong for availability / shipping | low when it's a real constraint |
Session beats profile almost always: what someone did sixty seconds ago is a far better predictor of the next click than a coarse segment built from months of history. The honest senior framing is that long-term profile mostly sets a weak prior, and session signal does the real disambiguation work. We built the segments themselves in 31; here they are just one input feature among several.
E-commerce search as multi-objective ranking
On the open web, "best result" is roughly one thing: relevance. In commerce it is emphatically not. The platform makes money when items sell, and not all sales are equal — a £400 item at 40% margin is worth more than a £20 item at 5% margin. So the score that orders the SERP is a blend:
s(item) = α·relevance + β·p(convert) + γ·margin + (other terms…)
where relevance is the LTR score from lesson 44, p(convert) is the predicted probability the user buys given a click, and margin is the platform's expected profit (or a proxy: price, take-rate, sponsored bid). The weights α, β, γ are not learned end-to-end against a single label — they are a product decision, dialled to trade user satisfaction against revenue, then validated by A/B test (lesson 33).
Worked numbers — how the weights reorder the page
Five candidate items for the query wireless headphones. Relevance and p(convert) are calibrated to [0,1]; margin is expected profit in £ (already on a comparable per-impression scale after we divide by a normalizer of £20, so margin column below is margin/£20).
| Item | relevance | p(convert) | margin (norm.) | in stock? |
|---|---|---|---|---|
| A — Sony WH-1000 (top match) | 0.95 | 0.30 | 0.40 | yes |
| B — house-brand buds (high margin) | 0.55 | 0.45 | 1.00 | yes |
| C — Bose mid-range | 0.80 | 0.35 | 0.55 | yes |
| D — budget no-name (cheap, converts) | 0.45 | 0.60 | 0.25 | yes |
| E — premium audiophile set | 0.85 | 0.15 | 0.90 | no |
Pure relevance (α=1, β=0, γ=0). Scores are just the relevance column:
A 0.95 > E 0.85 > C 0.80 > B 0.55 > D 0.45 → order: A, E, C, B, D
The best text match leads. But E is out of stock — a pure-relevance ranker happily ranks a thing you can't buy second. Hold that thought.
Balanced commercial (α=0.5, β=0.3, γ=0.2). Compute each:
| Item | 0.5·rel | 0.3·p(conv) | 0.2·margin | s |
|---|---|---|---|---|
| A | 0.475 | 0.090 | 0.080 | 0.645 |
| B | 0.275 | 0.135 | 0.200 | 0.610 |
| C | 0.400 | 0.105 | 0.110 | 0.615 |
| D | 0.225 | 0.180 | 0.050 | 0.455 |
| E | 0.425 | 0.045 | 0.180 | 0.650 |
order: E 0.650 > A 0.645 > C 0.615 > B 0.610 > D 0.455
Reasonable — but E (out of stock) still leads. We have not yet applied the availability constraint; the blend treats stock as a soft factor it never saw. Constraints are not weights; they are a gate. More on that in the next section.
Revenue-heavy (α=0.3, β=0.2, γ=0.5) — the configuration a revenue-chasing PM proposes:
| Item | 0.3·rel | 0.2·p(conv) | 0.5·margin | s |
|---|---|---|---|---|
| A | 0.285 | 0.060 | 0.200 | 0.545 |
| B | 0.165 | 0.090 | 0.500 | 0.755 |
| C | 0.240 | 0.070 | 0.275 | 0.585 |
| D | 0.135 | 0.120 | 0.125 | 0.380 |
| E | 0.255 | 0.030 | 0.450 | 0.735 |
order: B 0.755 > E 0.735 > C 0.585 > A 0.545 > D 0.380
Here is the cost, in one line. The house-brand buds B — relevance only 0.55, the fourth-best match — now lead the page, ahead of the Sony WH-1000 (A, relevance 0.95) which fell to fourth. Margin-weighting promoted a clearly-worse match to the top slot. Short-term: revenue per search rises. Long-term: the user who typed wireless headphones sees a mediocre house-brand first, learns the search "shows me junk," and trust erodes — which shows up later as lower repeat-search rate and higher bounce, metrics no single experiment cleanly attributes back to γ. The art of commerce ranking is finding the γ that captures margin without visibly burying the obvious right answer. That is exactly what the widget below lets you feel.
Hard constraints & facets
The blend above handles things that are matters of degree. But commerce queries also carry things that are matters of fact: red nike running shoes size 10 under $80 in stock. Size 10 is not a soft preference — a size 9 is simply wrong. These are hard constraints, extracted as structured attributes by query tagging (lesson 41): brand=nike color=red category=running shoes size=10 price<80 in_stock=true.
A constraint is a filter, not a feature. It partitions candidates into eligible / ineligible; ineligible items are removed regardless of how high they'd score. The UI surfaces these as facets — the clickable refinement rails ("Brand", "Price", "Size") with counts next to each value.
Facet count computation
A facet count is "how many results would remain if the user also selected this value, given everything currently selected." The subtlety: when computing the count for a value within a facet, you exclude that facet's own current selection (otherwise selecting "Nike" would show "Nike (240), Adidas (0), …" and make the rail useless). So for facet F, counts are computed against the result set filtered by all other active facets but not F itself.
Worked: query matches 2,400 items. User has selected category=running shoes (leaves 600) and price<80 (of those, 350 remain). To render the Brand facet, drop the Brand selection (there is none yet) but keep category and price filters, then bucket the 350 by brand:
Mechanically this is a multi-dimensional aggregation: for each facet you maintain a count by value over the appropriately-filtered set. Inverted-index engines (Lucene/Elasticsearch, the index of lesson 42) compute these with per-field doc-value columns and bitset intersections; the cost is roughly one pass over the matching doc-id set per facet.
Pre- vs post-filtering, and the post-filter recall trap
Where you apply the constraint relative to retrieval is the whole ballgame for recall, and it's the e-commerce face of the ANN filtering problem from lesson 03.
| Pre-filter | Post-filter | |
|---|---|---|
| Order | filter first, then rank/ANN over the eligible set | retrieve/ANN top-K, then drop ineligible |
| Recall | safe — every eligible item is considered | can collapse — survivors of a tight filter may be near zero |
| Cost | needs a filterable index (partitioned / faceted) | cheap, reuses the plain ANN index |
wireless earbuds. The user then applies in_stock=true price<$50 brand=Sony. We post-filter the 1000. Suppose, independently, ≈30% are in stock, ≈25% are under $50, and ≈8% are Sony. The survivors are roughly
1000 × 0.30 × 0.25 × 0.08 ≈ 6 items
Six. The page renders a near-empty SERP even though the catalogue has hundreds of in-stock Sony earbuds under $50 — they just weren't in the vector top-1000, which was dominated by the most semantically central earbuds (mostly other brands, many over $50). This is post-filter recall collapse: the filter is fine, retrieval just never surfaced the eligible items.The fixes, in order of preference:
- Filtered / partitioned index. Maintain the constraint as a filterable field inside the index (FAISS
IDSelector, ScaNN masked search, Lucene'sfilteredquery, pgvector with a WHERE clause + HNSW filter) so the ANN search itself only ever traverses eligible vectors. This is pre-filtering and it's the right answer when the filter is high-cardinality and common (in-stock, geo). It costs more index machinery. - Over-fetch. If filters are mild, retrieve top-10,000 instead of top-1000 so enough survive: 10000 × 0.006 ≈ 60. Cheap, but a band-aid — a tighter filter just needs a bigger K, and latency grows with K.
- Partition the index by the common hard filter. Separate in-stock vs out-of-stock indices (or per-region shards), so the dominant constraint is free. Combine with over-fetch for the rest.
- Fall back to lexical. When dense + filter collapses, run the BM25 path (lesson 42) over the filtered field set, which filters natively on structured fields, and fuse.
Marketplace search: two-sidedness changes the objective
A retailer owns its inventory; a marketplace matches independent buyers with independent sellers — Airbnb (guests↔hosts), Uber (riders↔drivers), eBay/Etsy (buyers↔sellers), Upwork (clients↔freelancers). Three structural facts make marketplace search a different problem from web or even retail search.
1. Availability is a first-class constraint, computed at query time. An Airbnb listing isn't "relevant" if it's booked on the requested dates — availability depends on the (listing × date-range) pair, which can't be a static index field. Uber must match the supply of drivers physically near the rider right now. eBay must hide sold-out listings. So the constraint set includes dynamic, perishable facts (dates, real-time geo supply, current inventory) that change minute to minute — you cannot bake them into the offline index; they gate at request time, and they make the post-filter recall problem above the normal case, not the edge case.
2. Inventory freshness dominates. A marketplace's catalogue churns constantly — listings appear and sell out within hours. A nightly-rebuilt index (lesson 03) would show stale, already-sold results. Marketplaces lean hard on the two-tier freshness pattern: a small near-real-time index for recent/changed listings merged with the large base index.
3. You cannot purely maximize buyer relevance — you must keep sellers alive. If ranking always showed the single most-relevant, most-reviewed, cheapest listing, a handful of incumbent sellers would win every impression, new sellers would get zero traffic, never accumulate reviews, and churn off the platform. Then buyers see less selection, and the marketplace dies from the supply side. This is the cold-start-seller problem, and it's why marketplaces deliberately inject seller fairness into ranking:
- New-listing exploration. Reserve some impression share for fresh listings that have no engagement history yet, to gather the click/conversion data ranking needs (the exploration half of explore/exploit; see diversity & fairness, lesson 20).
- Per-seller impression caps / fairness constraints. Prevent any one seller from monopolizing the first page, even if their items score highest, to spread demand across supply.
- Geo / supply balancing. Uber-style: rank to balance where drivers are against where demand is, not just by nearest match — sometimes routing a rider to a slightly-further driver keeps the overall system liquid.
The senior framing: a marketplace ranker optimizes a long-run, two-sided objective (marketplace liquidity and GMV — gross merchandise value — over months) that can locally trade away buyer relevance for supply health. That tension — buyer relevance vs seller fairness vs marketplace liquidity — is the defining feature, and it's another term you'd add to the blended score from earlier, governed by the fairness machinery of lesson 20.
The e-commerce search stack
Putting it together, here is where each mechanism lives in the pipeline. Note that constraints gate before ranking, and personalization + business blend ride on top of relevance at the very end — exactly as the "personalize the rerank, not the retrieval" rule demands.
Governing the thumb on the scale
Beyond the learned blend, every commerce search system grows a layer of explicit, hand-placed overrides — and how you govern them separates a trustworthy system from a mess.
- Tie-breaking. When two items score within ε, you need a deterministic, defensible tiebreak — recency, then a stable hash of item-id (never random, or pagination breaks and the same query reorders on refresh). Pick something auditable.
- Business boosts. "Promote our private-label brand by +0.1," "boost items shipping free." A multiplicative or additive nudge on the blended score. Each one is a small, deliberate relevance sacrifice for a business reason.
- Pins / merchandising. "For query
valentines gift, hard-pin this curated collection to slots 1–3 for the campaign." A manual override that ignores ranking entirely for specific slots.
boost brand=X by 1.4 exists, they interact unpredictably, and the "learned" ranker is actually a pile of hand-tuned overrides nobody can reason about. The governance answer a senior gives: every boost/pin is (1) logged with an owner and a reason, (2) time-boxed with an expiry, (3) attributable in logs — when an item ranks #1, you can answer "did it earn that or was it pinned?", and (4) measured — boosts are A/B-tested like any change, not waved in. The thumb on the scale is legitimate; an unaccountable thumb on the scale is how search systems lose user trust and engineering sanity.
Interactive · blended ranking explorer
Six candidate items for a commerce query, each with calibrated relevance, p(convert), margin, and an in-stock flag. Slide the weights α (relevance), β (conversion), γ (margin), and toggle in-stock-only. The list reorders live, the KPIs show which objective is dominating the top slot, and a note fires when a clearly-relevant item (relevance ≥ 0.85) gets buried below rank 3 by business weighting. The model is a toy — calibrated, comparable inputs, a linear blend — but the reordering and the trust-cost dynamics are exactly the real thing.
Interview prompts you should be ready for
- "When should you personalize a search query, and when shouldn't you?" (The whole judgement: navigational/head queries look the same for everyone — personalizing
facebook loginis pointless or harmful; ambiguous/informational/long-tail queries are where it pays. Gate per-query on query entropy / top-result CTR. A junior says "personalize everything.") - "Where in the pipeline does personalization go, and why not in retrieval?" (Re-rank the top set, never re-retrieve. Re-retrieving on personal history kills recall for narrow-history users — you can't rerank what you didn't fetch — and compounds into a filter bubble. Retrieve broadly, reorder the top 50–200.)
- "You blend α·relevance + β·p(convert) + γ·margin and bumping γ makes the ranker show junk. What's the first thing you check?" (Calibration. The three terms must be on comparable scales — relevance/p(convert) mapped to probabilities, margin as expected revenue in currency — before weighted addition is meaningful. Uncalibrated scale mismatch, not weighting, is the usual culprit.)
- "ANN returns 1000 candidates, the user applies in-stock + under-$50 + brand=X, and the page is nearly empty. Diagnose and fix." (Post-filter recall collapse: 1000 × the product of independent filter pass-rates ≈ a handful. It's a retrieval-stage problem, not a ranking bug. Fix with a filtered/partitioned index (pre-filter), or over-fetch, or partition by the dominant filter, or fall back to lexical which filters structured fields natively.)
- "Why can't a marketplace just rank by buyer relevance?" (Two-sidedness: always showing the top incumbent starves new/cold-start sellers, supply churns off, selection collapses, the marketplace dies from the supply side. Need new-listing exploration, per-seller impression caps, geo/supply balancing. Optimize long-run two-sided liquidity, not single-query relevance.)
- "Your PM wants to hard-boost the private-label brand. How do you let them without wrecking the system?" (Boosts are legitimate but must be governed: logged with owner + reason, time-boxed with expiry, attributable in logs (did it earn #1 or was it pinned?), and A/B-measured like any change. The failure mode is unaccountable boost-rot — a pile of overrides nobody can reason about.)