Reducing Zero-Result Searches
A zero-result search is the only unambiguous failure a search system produces: the user told you what they wanted and you returned nothing. It is also the cheapest signal to act on, because it needs no relevance labels and every instance comes with the exact query that caused it. This page turns that signal into a prioritised work queue and applies the right fix to each cause. It sits under search analytics and click tracking in search frontend and UX patterns.
Prerequisites
- Search events recording the original result count, before any fallback widened the query.
- Query text or a stable hash, so identical failures can be grouped and counted.
- Access to the analyzer, so you can see how a failing query was tokenised.
- A synonym mechanism that can be changed without a reindex, per managing synonyms without reindexing.
Diagnosis: five causes with five different fixes
“Zero results” is a symptom, not a cause, and treating it as one leads to the wrong remedy — usually a fallback that hides the problem. Grouping failures by cause takes an afternoon and makes the work tractable.
Genuinely absent. The user asked for something the catalog does not contain. No search fix applies; this is merchandising or content information, and it is frequently the most valuable output of the whole exercise.
Misspelling. The intent exists in the catalog under a different spelling. Fixed by typo tolerance or a correction suggestion.
Vocabulary mismatch. The user’s word and the catalog’s word differ — “trainers” against “running shoes”. Fixed by synonyms, or by the vector retriever in a hybrid setup.
Over-constrained. The terms exist but the combination, or the active filters, match nothing. Fixed by query relaxation rather than by anything in the index.
Tokenisation failure. The analyzer mangled the query — a hyphenated model number split, a unit stripped, a language-specific form unhandled. Fixed in the analysis chain, and it is the cause most often missed because the query looks perfectly reasonable.
top zero-result queries, last 7 days count cause
"womens trainers size 6" 412 vocabulary + over-constrained
"gore-tex" 388 tokenisation (hyphen split)
"jaket" 241 misspelling
"tent for 6 people under £200" 156 over-constrained
"canoe" 143 genuinely absent
That single table converts a vague metric into five pieces of work with different owners.
Solution Steps
1. Record the pre-fallback result count
If a fallback widens the query when nothing matches, the logged count is never zero and the metric is destroyed. Record both numbers.
# search_handler.py — the honest count and the served count are different fields
results = engine.search(query, filters=filters)
original_count = results["total"]
fallback = None
if original_count == 0:
results, fallback = relax(query, filters)
analytics.emit("search.executed", {
"result_count_original": original_count, # this is the metric
"result_count_served": results["total"], # this is what the user saw
"fallback": fallback, # which relaxation fired, if any
})
2. Classify automatically where you can
Three of the five causes are detectable in code, which turns most of the triage into a report.
# classify.py — cheap automatic classification of a zero-result query
def classify(query: str, analyzer_tokens: list[str], catalog) -> str:
if not analyzer_tokens:
return "tokenisation" # analysis produced nothing
if any(t not in catalog.term_dictionary for t in analyzer_tokens):
near = catalog.fuzzy_terms(analyzer_tokens, max_edits=1)
return "misspelling" if near else "vocabulary_or_absent"
return "over_constrained" # all terms exist, combination does not
3. Relax over-constrained queries in a defined order
Relaxation should be principled rather than arbitrary: drop the least selective constraint first, and tell the user what you dropped.
# relax.py — ordered relaxation with an explicit, reportable trail
RELAX_ORDER = ["price_range", "colour", "size", "brand"] # least to most important
def relax(query, filters):
trail = []
remaining = dict(filters)
for key in RELAX_ORDER:
if key not in remaining:
continue
remaining.pop(key)
trail.append(key)
results = engine.search(query, filters=remaining)
if results["total"] > 0:
return results, {"dropped": trail}
# last resort: OR the terms instead of AND
return engine.search(query, filters={}, operator="or"), {"dropped": trail + ["all"]}
4. Offer a correction rather than applying one silently
Auto-correcting is right when confidence is very high and wrong otherwise, because it makes the user’s actual query unreachable. Offer the correction and show results for it, with a way back.
// "Showing results for jacket. Search instead for jaket."
render({
correctedQuery: "jacket",
originalQuery: "jaket",
results,
showRevertLink: true, // the escape hatch is what makes auto-correction acceptable
});
5. Feed the absent items to merchandising weekly
The queries with no plausible catalog match are demand data, and they are usually the most commercially valuable output of this work.
SELECT query_hash, any_value(sample_query) AS example, count(*) AS searches
FROM search_executed
WHERE result_count_original = 0
AND cause = 'vocabulary_or_absent'
AND ts > now() - interval '7 days'
GROUP BY query_hash
HAVING count(*) > 20
ORDER BY searches DESC
LIMIT 50;
Verification
Confirm the metric survives the fallback:
python3 report.py --metric zero_result_rate --days 7
# served zero-result rate: 0.4% (what users experienced)
# original zero-result rate: 6.1% (what search actually failed to match)
# => the gap is the fallback working; both numbers are needed
Confirm a fixed cause actually moved its share:
python3 report.py --by-cause --compare-weeks
# last week this week delta
# tokenisation 1.9% 0.3% -1.6 <- the hyphen fix landed
# misspelling 1.4% 1.3% -0.1
# vocabulary 1.8% 1.7% -0.1
# over_constrained 0.9% 0.9% 0.0
Confirm relaxation is not firing too eagerly:
python3 report.py --metric fallback_rate
# 5.8% of searches triggered relaxation
# => above about 10%, the base query is too strict and should be fixed instead
The empty state is part of the fix
Everything above reduces how often the failure happens; the empty state decides what it costs when it does. A page reading “No results found” with nothing else is the worst possible outcome, because the user has no path forward and no information about why.
Three elements make an empty state useful. State what was searched, so a typo is visible — a surprising number of zero-result searches are resolved by the user simply seeing their own query rendered back. Offer the specific relaxation that would have worked: “no results for waterproof jacket under £50 — 34 results without the price filter” is actionable in a way that a generic suggestion is not. And provide a genuine alternative route: popular items in the nearest category, or the category page itself.
That combination typically recovers a substantial share of otherwise-abandoned sessions, and it costs one additional query fired only on the zero-result path — no cost at all in the common case.
Common Pitfalls
Fixing the metric instead of the problem
A broad fallback — dropping all filters, ORing every term — drives the observed zero-result rate to almost nothing while serving results the user did not ask for. Users experience this as search ignoring them, which is worse than an honest empty state with suggestions. Keep the original count as the metric and judge fallbacks by whether their results get clicked.
Adding synonyms for one-off queries
Every zero-result query looks like a missing synonym, and adding hundreds of narrow entries produces a synonym file nobody can review and a slow degradation of precision. Apply a volume threshold — a query must fail repeatedly across distinct sessions before it earns an entry — and prefer fixing the analyzer when several failures share a root cause.
Ignoring the filters in the analysis
A query with terms that all exist and filters that exclude everything is not a search problem, and no amount of synonym or analyzer work will help. Always record the active filters alongside the query, or a substantial share of your zero-result queue will resist every fix you try.
Operational notes
Review the queue weekly rather than continuously. Zero-result queries accumulate in a stable pattern, and a weekly pass over the top twenty catches essentially everything worth fixing while keeping the work bounded and predictable.
Track the rate by surface as well as by cause. Autocomplete, the main result page and in-app search frequently have very different zero-result profiles, and a problem confined to one surface points immediately at that surface’s query construction rather than at the index. A blended number averages the distinct problems into one unactionable figure.
Set the goal deliberately rather than at zero. A healthy catalog search typically runs 3–8% zero-result on original counts, and driving it below that usually means the fallbacks have become too aggressive. The number to minimise is the unexplained share — failures whose cause you have not classified — because that is where fixes remain available.
Related
- Search analytics and click tracking — the event model that makes this metric available in the first place.
- Synonym and stopword management — the fix for the vocabulary-gap class, and where the curation loop lives.
- Search-as-you-type interfaces — where the empty state is designed and the recovery options are presented.