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

  1. Search events recording the original result count, before any fallback widened the query.
  2. Query text or a stable hash, so identical failures can be grouped and counted.
  3. Access to the analyzer, so you can see how a failing query was tokenised.
  4. 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.

Five causes of zero results and their fixes Absent items need merchandising, misspellings need typo tolerance, vocabulary gaps need synonyms, over-constrained queries need relaxation, and tokenisation failures need analyzer work. genuinely absent → merchandising signal misspelling → typo tolerance vocabulary gap → synonyms or vector over-constrained → query relaxation tokenisation → analyzer chain each has a different owner a universal fallback treats all five identically and fixes none of them it also destroys the metric, because nothing returns zero any more
Five causes, five fixes, five owners. The classification is the work; the individual remedies are mostly straightforward once the cause is named.
Triaging a zero-result query in three checks Check whether analysis produced tokens, whether those tokens exist in the index, and whether the filters are the constraint. 1. did analysis emit tokens? no → tokenisation 2. do the terms exist? no → spelling or vocabulary 3. remove the filters? results → over-constrained Three API calls classify almost any zero-result query, and the classification determines the owner.
Three checks in order, each ruling out one cause. Running them by hand on a sample teaches the pattern well enough to automate it.

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.

What a healthy zero-result profile looks like A few per cent of searches legitimately match nothing, a smaller share is fixable, and the unclassified remainder is the part to drive toward zero. healthy genuinely absent fixable unclassified unhealthy unclassified — the work is here The target is not zero failures but zero failures whose cause you have not identified.
Some zero-result searches are correct answers. The share worth attacking is the part nobody has explained yet.