search_ads_recsys / 41 · query understanding search · 2 / 10

Query understanding — from raw string to structured intent

The funnel from lesson 40 starts with a string the user typed. Before a single posting list is touched, that string is normalized, tokenized, corrected, expanded, segmented, and tagged. Every one of those transforms trades recall against precision — your job is to name the trade for each.

The plan

The one engineering line to carry through every section: each transform either widens the set of documents that can match (recall) or narrows it toward the user's true intent (precision), and a senior engineer can state which, and the failure mode, for every stage. Normalization and expansion buy recall; phrase detection and tagging buy precision; spelling correction and segmentation are recall-then-precision in one move. Get the direction wrong and you either return nothing or return everything.

The analysis pipeline, in order

Analysis (the IR term) is the deterministic transform from a raw query (and, symmetrically, from each document at index time) into the sequence of terms that the index keys on. The cardinal rule: the query analyzer and the document analyzer must agree. If the index stored running stemmed to run but the query analyzer leaves running alone, the term never matches and recall silently dies. Most production bugs in this layer are an asymmetry between the two paths.

raw query: "Café RUNNING-shoes,纽约" │ ┌─────────────────▼─────────────────┐ │ 1. Unicode normalize (NFKC, │ fold width/compat, casefold, │ casefold, strip diacritics) │ drop accents → recall↑ └─────────────────┬─────────────────┘ │ "cafe running-shoes,纽约" ┌─────────────────▼─────────────────┐ │ 2. Tokenize │ split on space/punct; │ (ws + lang-aware + CJK seg) │ segment 纽约→[纽约]; subword OOV └─────────────────┬─────────────────┘ │ [cafe][running][shoes][纽约] ┌─────────────────▼─────────────────┐ │ 3. Stopword policy (mostly KEEP) │ drop only in legacy/precision modes └─────────────────┬─────────────────┘ │ [cafe][running][shoes][纽约] ┌─────────────────▼─────────────────┐ │ 4. Stem OR lemmatize │ running→run → recall↑ (over-stem risk) └─────────────────┬─────────────────┘ │ [cafe][run][shoe][纽约] ← analyzed TERMS ▼ feed inverted index (lesson 42)

1 · Unicode normalization

A user typing café on a Mac and another pasting café from a PDF can produce different byte sequences for the same visible string: one is the precomposed code point U+00E9, the other is e + a combining acute accent U+0301. Byte-equality fails; the term won't match. NFKC (Normalization Form KC — Compatibility Composition) is the standard fix: it canonicalizes composed/decomposed forms and folds compatibility variants (full-width ABCABC, the ligature fi, 1). Then casefold (a more aggressive lowercase that also handles e.g. German ßss) and optional diacritic folding (cafécafe, naïvenaive).

The trade. Every fold is a recall-for-precision swap. Diacritic folding lets resume match résumé — great for English users who skip accents. But in French, marché (market) and marche (walks/step) are different words; fold them and you inject ambiguity. Failure mode: a blanket accent-strip applied to a French or Vietnamese corpus collapses distinct words and tanks precision. The senior move is to make folding locale-aware, not global.

2 · Tokenization

Tokenization splits the normalized string into tokens. Three regimes you must distinguish:

RegimeMethodWhy it exists / breaks
Whitespace + punctuation Split on spaces and most punctuation. running-shoesrunning, shoes. Fine for English-like scripts. Breaks on C++, node.js, 3.5", product SKUs — punctuation carries meaning. Token filters/whitelists patch this.
Language-aware / CJK segmentation Chinese/Japanese/Thai have no spaces. Need a segmenter: dictionary + Viterbi (jieba), CRF, or BPE. 纽约时报纽约(New York) 时报(Times). Segmentation is ambiguous: 北京大学 = 北京+大学 (Beijing University) or +京大+. Wrong cut = wrong terms = wrong matches. Common fallback: index character bigrams so partial matches survive a bad cut.
Subword / BPE Break rare/unknown words into known fragments. antifragilityanti frag ility. Handles out-of-vocabulary (OOV) terms and morphologically rich languages. Trade: subword fragments are noisier match units; a fragment match isn't a word match. Mostly used in neural retrieval (lesson 43), less in classic BM25.

The trade. Aggressive splitting (hyphens, dots) raises recall (node.js finds docs that wrote node js) but lowers precision (C++ queries match every doc containing a bare c). CJK segmentation is recall-vs-precision in the cut itself: under-segmenting keeps long phrases (precise, low recall), over-segmenting into characters maximizes recall but matches noise.

3 · Stopwords — why modern BM25 mostly keeps them

Stopwords are extremely frequent function words (the, a, of, to). Classic IR dropped them: in a 1990s memory-and-disk-constrained index, the posting list for the covers ~every document and costs a fortune to store and intersect, while contributing almost nothing to ranking. So dropping them was a pure win on cost.

Modern systems mostly keep stopwords. Two reasons. First, BM25 (lesson 42) already down-weights them automatically via IDF — a term in every document has near-zero inverse document frequency, so it barely moves the score even if present. The cost-saving motivation is gone now that storage is cheap and skip-lists make long posting lists fast. Second, dropping them destroys phrase queries and changes meaning: to be or not to be becomes empty; the office (the show) becomes office; vitamin a loses the a that is the query. Failure mode of dropping: the query flights to paris and flights from paris become identical. The senior position: keep stopwords for matching, rely on IDF to neutralize their weight, and special-case them only inside phrase detection.

4 · Stemming vs lemmatization

Both collapse morphological variants so run matches running/ran/runs — a recall move. They differ in how, and in what they cost.

Stemming (e.g. Porter)Lemmatization
MethodRule-based suffix chopping. No dictionary, no part-of-speech. Fast, language-specific rule sets.Dictionary + morphological analysis + part-of-speech. Maps to the true base form (lemma).
Example winsconnection, connected, connectingconnectbettergood; was, is, arebe (stemming can't do these)
Over-stemming (false merge)university and universeunivers; organization and organorgan; newsnewRare — POS and lexicon prevent it
Under-stemming (missed merge)data / datum not mergedHandled via lexicon
CostMicroseconds, no model10–100× slower; needs POS tagger + dictionary per language; latency in the analyzer hot path

The trade. Stemming over-merges, hurting precision: a query for universe retrieves university docs because both became univers. Lemmatization is precise but pays POS-tagging latency on every query and needs per-language resources. The pragmatic default in web/e-commerce search is a light stemmer (Porter or KStem, which is more conservative) for high-recall languages, and skipping stemming entirely where precision dominates (legal, code, exact-SKU). Whatever you pick, apply the identical transform at index time — the asymmetry bug again.

Spelling correction — the noisy-channel model

Roughly 10–15% of web queries contain a typo. Correcting them is a large recall lever (the misspelled term has an empty or wrong posting list). The standard framing is the noisy-channel model: the user intended a correct word c but a noisy channel (fat fingers, bad spelling) emitted the observed string w. We invert it with Bayes:

ĉ = argmaxc ∈ candidates P(c | w) = argmaxc P(w | c) · P(c)

Two factors, each with a name and a job:

Weighted edit distance: Damerau-Levenshtein, worked

Plain Levenshtein distance counts the minimum insertions, deletions, and substitutions to turn one string into another. Damerau-Levenshtein adds a fourth operation — transposition of two adjacent characters — because swapping neighbours (tehthe) is one of the most common real typos and Levenshtein would charge it as two edits. Keyboard-adjacency weighting further says a substitution of two physically-adjacent keys (sd) is cheaper than distant keys (sp), because the former is a far more probable slip.

Worked example: distance from typed teh to candidate the.

"" t h e (candidate "the" across top) "" 0 1 2 3 t 1 0 1 2 e 2 1 (1) 2 ← cell (e,h): not equal; min(diag 0, up 1, left 1)+1 h 3 2 1 (1) ← Damerau: 'eh' vs 'he' transposition → cost 1 Levenshtein DP fills min(sub, ins, del); Damerau adds a transposition move: if s[i],s[i-1] == t[j-1],t[j] then also consider d[i-2][j-2]+1.

Plain Levenshtein tehthe = 2 (substitute position 2 eh, substitute position 3 he). With the transposition rule, the adjacent pair ehhe is a single operation, so Damerau-Levenshtein = 1. That single-edit candidate gets a much higher P(w|c) than any 2-edit alternative — which is exactly why transposition matters: teh should correct to the (1 edit, transposition), not to tea (1 substitution) once the prior is folded in.

Ranking candidates with a query-log language model

Concretely: user types teh. Candidate generator (all dictionary/log words within Damerau distance ≤ 2, via a BK-tree or a deletion-index like SymSpell) yields the, tea, ten. Toy numbers — channel probability from edit cost, prior from query-log frequency (per million queries):

candidate ceditP(w=teh | c) channelP(c) prior (log freq)P(w|c)·P(c) ∝
the1 transposition0.200.050 (50,000 / M)0.01000
tea1 substitution h→a0.080.0008 (800 / M)0.000064
ten1 substitution h→n0.060.0010 (1,000 / M)0.000060

Even though tea and ten are also single edits, the wins by ~150× — almost all of it from the prior (the is enormously more frequent in the log than tea/ten). This is the key intuition: the channel model narrows to plausible typos; the query-log prior picks the winner. A spelling corrector with no prior corrects nkie to a dictionary word like nice; a corrector with a shoe-store query-log prior corrects it to nike. The prior is where your domain lives.

Autocorrect vs "did you mean" — do not conflate
Two products, two risk profiles. Autocorrect silently rewrites the query and searches the corrected form. Use it only when confidence is very high (the corrected form dominates by orders of magnitude, like above) — a wrong silent rewrite is infuriating because the user sees results for a word they didn't type. "Did you mean <X>?" searches the original but offers a one-click suggestion; use it when confidence is moderate. The deciding number is the posterior margin P(c₁|w) / P(c₂|w) and whether the original even has results. The full suggestion/autocomplete machinery is lesson 46; here it is enough to know the decision is a calibrated-confidence threshold, not a coin flip.

Query expansion — buy recall, watch the drift

Expansion adds terms to the query so documents that used different words can still match. It is the single biggest recall lever and the single biggest precision risk. The terms are added as optional (SHOULD) clauses with lower weight, not mandatory ones, so they boost recall without forcing every expansion to appear.

Classic expansion

Rocchio reweighting, worked

In the vector-space view a query is a weighted term vector q. Rocchio moves it toward the centroid of (pseudo-)relevant docs and away from non-relevant ones:

q' = α·q + β·(1/|Dr|)·Σd∈Dr d − γ·(1/|Dnr|)·Σd∈Dnr d

Toy worked numbers. Original query jaguar speed over a 4-term vocabulary {jaguar, speed, animal, car}. Initial q = (1, 1, 0, 0). PRF: the top-2 returned docs (treated as relevant) happen to be about the car, with term vectors d₁ = (1, 1, 0, 1) and d₂ = (1, 0, 0, 1); one non-relevant doc (about the animal) d₃ = (1, 0, 1, 0). Use α = 1, β = 0.75, γ = 0.25.

relevant centroid = ((1+1)/2, (1+0)/2, 0, (1+1)/2) = (1.0, 0.5, 0.0, 1.0) nonrel centroid = (1, 0, 1, 0) q' = 1·(1,1,0,0) + 0.75·(1.0, 0.5, 0.0, 1.0) - 0.25·(1.0, 0.0, 1.0, 0.0) jaguar : 1 + 0.75·1.0 - 0.25·1.0 = 1 + 0.75 - 0.25 = 1.50 speed : 1 + 0.75·0.5 - 0.25·0.0 = 1 + 0.375 = 1.375 animal : 0 + 0.75·0.0 - 0.25·1.0 = -0.25 → clamp to 0 car : 0 + 0.75·1.0 - 0.25·0.0 = 0.75 ← NEW expansion term q' = (1.50, 1.375, 0.0, 0.75)

The query learned to add car (weight 0.75) and suppress animal — it disambiguated jaguar toward the car sense because the first-pass results leaned that way. That is also the danger: if the first-pass top-k were actually about the animal, Rocchio would amplify the wrong sense. PRF amplifies whatever bias the first pass had — query drift.

Neural expansion

Gating expansion — the precision cost
Expansion drift is real money: expand apple to fruit on an electronics store and you bury the iPhones. Gates a senior names: (1) add expansions as weighted optional clauses, never mandatory; (2) cap the number of expansions and their total weight; (3) gate on query confidence — don't expand a query that already has plenty of high-precision results, expand the long-tail / zero-result queries where recall is the bottleneck; (4) gate on head ambiguity — never expand a navigational query (facebook login) where the user knows exactly what they want. The frame: expansion helps the tail and hurts the head.

Query segmentation & phrase detection

A bag-of-terms analysis throws away word order. For many queries that is fine; for some it is fatal. new york times as three independent terms (new AND york AND times) matches a document about "the new times in York" — wrong. The query means the newspaper, a single concept. Query segmentation groups adjacent tokens into phrases (multi-word expressions) so they are matched as a unit.

Why it matters for the inverted index (lesson 42): a phrase match requires positional postings — the index must store where in each document a term appears, so new york times only matches when the three terms are adjacent and in order. That is more expensive than a plain term match, so you only promote high-confidence phrases. Detection signals: query-log co-occurrence / pointwise mutual information (the trigram appears together far more than chance), a known-entities gazetteer, or a learned segmenter. The trade: phrase matching is a precision tool — it shrinks the matching set to documents with the exact adjacency, raising precision but risking recall if the document phrased it slightly differently (the New York Times vs NY Times). The standard hedge is to issue both: the phrase as a strong-weighted optional clause plus the loose AND, so the phrase match floats to the top without excluding the rest.

Intent classification, NER, and structured tagging

Everything above operated on words. This stage operates on meaning: classify what the user wants and extract the structured fields a backend can act on.

Intent classification — the Broder types

From lesson 40, the classic taxonomy of web-search intent (Broder 2002):

Intent is usually a lightweight classifier (logistic regression / small transformer) over the query plus log features. It gates the rest of the pipeline: a navigational query turns off expansion and diversity; a transactional one turns on price sorting and inventory filters. Misclassifying navigational as informational is the classic precision failure — the user wanted exactly one site and you handed them a topic page.

Named-entity recognition & entity linking

NER tags spans with types: in cheap iphone charger, iphone is a PRODUCT/BRAND span. Entity linking then resolves the span to a canonical entity in a knowledge base / catalog: the surface form iphone → entity Apple iPhone (brand=Apple, product-line=iPhone). Linking is what lets iphone, i-phone, and apple phone all resolve to the same node — disambiguation against a catalog, not string matching.

Structured tagging → facets

For e-commerce especially, the goal is to decompose the query into the catalog's structured fields. Worked: cheap iphone charger

fieldvaluesourcebackend effect
intenttransactionalintent classifiercommerce ranking on
brandApple / iPhoneNER + entity linkingfilter brand_compat=Apple
categorycharger / cables & chargersNER → category mapfilter category=charger
price-intentcheap → sort ascending / price ≤ p25modifier lexiconre-sort or filter on price

Those fields are exactly the facets that structured / e-commerce search exposes — the filter-and-sort controls in lesson 47. Query understanding is what auto-populates them from free text, so cheap iphone charger behaves like the user had clicked Brand: Apple, Category: Chargers, Sort: price-low-to-high. The trade: every extracted facet that you turn into a hard filter trades recall for precision — tag cheap as a hard price ≤ p25 filter and you exclude the $9 charger priced at p26. Senior practice is to apply tags as ranking boosts/soft-sorts rather than hard filters unless the field is unambiguous (a confidently-linked brand is safe to filter; a fuzzy modifier like cheap is safer as a sort signal).

The LLM era — rewrite power, latency tax

An LLM (lesson 26) can do much of this pipeline in one shot and better: query rewriting (chrgr for iphniPhone charger, fixing spelling, expanding abbreviations, and normalizing all at once), conversational rewriting (resolving and a cheaper one? against prior turns), and semantic expansion (generating intent-faithful synonyms and related terms with far less drift than embedding-neighbour expansion, because the model reasons about sense). It can also emit the structured tags directly as JSON.

The latency/cost caveat
Query understanding sits in the blocking path before retrieval — every millisecond is added to every search. An LLM call is 100–1000 ms and a per-query cost; classic analysis is sub-millisecond and free. So the LLM-in-the-loop pattern is reserved for where it pays: long-tail / zero-result queries, conversational sessions, hard ambiguity — gated, not default. The high-QPS head is served by the cheap deterministic pipeline above, often distilled from offline LLM labels (run the LLM offline to build synonym tables, doc2query expansions, and tagging training data; serve the cheap student online). The senior framing matches the rest of the lesson: the LLM is another recall/precision lever, and its extra knob is latency/cost — use it on the tail where the deterministic pipeline is weakest, not on the head where it is already good and fast.

One query through every stage

Putting it together — the user types café RUNNING-shose nyc (note the typo shose):

RAW : "café RUNNING-shose nyc" NORMALIZE : casefold + diacritic-fold + NFKC → "cafe running-shose nyc" TOKENIZE : split on space + hyphen → [cafe] [running] [shose] [nyc] SPELL : [shose] not in lexicon; Damerau-1 candidates {shoes(prior hi), shore, hose} noisy-channel → "shoes" (transposition s↔e), confidence high → autocorrect → [cafe] [running] [shoes] [nyc] SEGMENT : "nyc" is an entity surface form (gazetteer), not 3 tokens — keep as unit STEM : running→run, shoes→shoe (cafe, nyc unchanged) → [cafe] [run] [shoe] [nyc] EXPAND : synonym nyc ⇒ "new york city" (optional, weighted 0.5) embedding shoe ≈ sneaker (optional, weighted 0.4) → core:[cafe][run][shoe][nyc] opt:[new york city]^0.5 [sneaker]^0.4 TAG : intent=informational/transactional(mixed); location=NYC(entity); category=running-shoe; modifier=café? (likely typo'd brand or place → low conf) → facets {location:NYC, category:running_shoe} OUT : analyzed terms + weights + facets → inverted index (lesson 42)

Trace the recall/precision ledger: normalization, spelling, stemming, and expansion all widened the matchable set (recall up); segmentation of nyc and the location/category facets narrowed toward intent (precision up). Every arrow had a direction, and a senior can name it.

Interactive · query-understanding pipeline

Type a query (or pick an example) and watch it transform stage by stage. The lexicon, synonym map, and typo model below are a small built-in toy — deterministic, no network — so the exact corrections are illustrative, but the stages and their order are real.

Run a query through the analysis pipeline
Toy lexicon/synonym/typo maps in JS — the corrections are illustrative, the pipeline order is the real lesson. Try a typo like iphn chrgr or a phrase like new york times.
core terms
corrections
expansions
intent
Stage-by-stage

Interview prompts you should be ready for

  1. "You stem at index time but not at query time. What happens, and how would you catch it?" (The analyzer asymmetry bug: the query term never matches the stemmed index term, recall silently collapses with no error. Catch it by asserting query-analyzer == doc-analyzer in tests, and by alerting on a recall/zero-result-rate jump after any analyzer change.)
  2. "Walk me through correcting teh to the. Where does the domain knowledge live?" (Noisy channel: P(w|c)·P(c). Damerau transposition makes it a 1-edit correction so the channel model is high; but the query-log prior P(c) is what beats tea/ten — the prior is where domain lives. A corrector with no prior corrects to dictionary words, not to what your users actually search.)
  3. "When do you autocorrect silently vs show 'did you mean'?" (A calibrated-confidence threshold on the posterior margin and whether the original has results. Silent rewrite only when the corrected form dominates by orders of magnitude and the original is near-zero-result; otherwise suggest, because a wrong silent rewrite shows results for a word the user didn't type.)
  4. "Query expansion doubled recall but precision dropped and revenue fell. Diagnose." (Query drift: expansion fired on head/navigational queries that didn't need it, or PRF amplified a wrong first-pass sense, or expansions were mandatory instead of weighted-optional. Fix: gate expansion to long-tail/zero-result queries, cap weight, never expand navigational, soft-clause not hard-clause.)
  5. "Why must new york times be a phrase, and what does that cost the index?" (Bag-of-terms matches scrambled/unrelated docs; the query is one concept. Phrase matching needs positional postings — more storage and a more expensive intersection — so you only promote high-confidence phrases (PMI/gazetteer) and usually issue phrase-as-boost alongside the loose AND to protect recall. Sets up positional index in lesson 42.)
  6. "Where would you put an LLM in query understanding, and where would you refuse to?" (It's a blocking pre-retrieval stage — 100–1000 ms and per-query cost on every search. Use the LLM on the tail: long-tail/zero-result/conversational/ambiguous queries, or offline to distill synonym tables, doc2query, and tagging labels for a cheap online student. Refuse it on the high-QPS head where the deterministic pipeline is already good and fast.)
Takeaway
Query understanding turns a raw string into analyzed terms, corrections, expansions, phrases, and structured facets — and every transform is a recall-vs-precision trade you must be able to name. Normalization, spelling correction, and expansion buy recall; phrase detection and tagging buy precision; spelling correction is noisy-channel (P(w|c)·P(c), with the query-log prior carrying the domain); expansion is gated to the tail because it drifts on the head; the analyzer must be identical at index and query time or recall dies silently; and the LLM is a tail-only lever whose extra knob is latency and cost. The terms and facets you produce here are exactly what the inverted index in lesson 42 keys on.