Zero-Result Recovery in Agentic Search: Progressive Filter Relaxation
Handle zero results in agentic RAG search by relaxing hard payload filters progressively, then falling back to a broad semantic query.
Contents
Short answer: Don't return "no results." Most zero-result cases come from over-narrow hard filters on sparse metadata fields, not a genuine absence of relevant items. Relax the hard filters progressively — dropping the least intent-critical ones first — retry after each drop, and only fall back to a broad semantic-only query as a last resort.
Why hard filters on sparse fields cause most zero-result cases
In a hybrid vector search, a zero-result response almost never means "nothing relevant exists." It means the payload pre-filter eliminated every candidate before similarity scoring ran. This is especially common on sparse fields — attributes that are populated for only a fraction of your corpus.
If you AND together three or four hard filters and even one of them touches a poorly-populated field, you can zero out the entire result set while an obviously good match sits one filter away. The similarity search never gets a chance.
The design lesson I've landed on: not every user-mentioned attribute deserves to be a hard filter. In my own restaurant search, atmosphere and amenities are treated as semantic query enrichment rather than hard payload filters, precisely to avoid over-constraining. A "cozy" cue nudges the embedding instead of demanding an exact metadata match on a field that's rarely filled in.
Distinguishing 'no match' from 'too-narrow filter' before giving up
Before you conclude there's genuinely nothing to show, separate the two failure modes:
- Too-narrow filter: Re-run the same query with the hard filters removed. If results appear, the filters were the problem — not the query. This is the case worth recovering from.
- Genuine no-match: Even the unfiltered semantic query returns weak or empty results. Only here does a "no results" message make sense, ideally with a suggestion to broaden the search.
A cheap way to detect this in practice: run the filtered search, and if the count is zero, run a filter-free semantic probe. The probe's outcome tells you which branch you're in.
Ordering which filters to drop first
Relaxation is only as good as its ordering. Drop filters least-to-most important to the user's intent:
- Sparse "nice-to-have" attributes first — amenities, ambiance, secondary tags. Rarely populated, rarely central.
- Numeric-range constraints next — price bands, ratings. Widen the band before removing it entirely.
- Soft categorical filters — a specific sub-cuisine can relax to its parent category.
- Core intent filters last — the dish type, the primary cuisine, the hard dietary requirement (e.g. a vegan constraint you must never silently drop for safety reasons).
The principle: keep the filters central to the request alive the longest, so a partial result still honors what the user actually asked for.
The implementation in my Qdrant search tool
My search_dishes tool uses progressive filter relaxation for its zero-result retry. The pipeline is a single Qdrant call — vector similarity plus payload pre-filters — hydrated from PostgreSQL and post-sorted. When that call comes back empty, the tool doesn't return; it re-enters with a smaller filter set.
async def search_with_relaxation(query, filters):
# filters ordered least -> most intent-critical
relax_order = ["amenities", "price_band", "sub_cuisine"]
active = dict(filters)
for _ in range(len(relax_order) + 1):
hits = await qdrant.search(query, filters=active)
if hits:
return hits
# drop the next least-important filter and retry
for key in relax_order:
if key in active:
del active[key]
break
else:
break # only core filters remain; stop dropping
# final fallback: semantic-only, no hard filters
return await qdrant.search(query, filters=None)This matches what the agentic-RAG literature finds: comparisons of agentic RAG designs report that enhanced routing and query relaxation are more reliable than rigid retrieval in noisy domains. Real user queries are noisy — under-specified, over-specified, mismatched against sparse metadata — and a system that adapts its constraints degrades far more gracefully than one that fails hard.
Never let a filter you added silently return nothing — relax it, retry, and fall back to semantic search before you ever show an empty page.
Related: more writing.
Frequently asked
- Why not just return 'no results'?
- Most zero-result cases come from over-narrow hard filters on sparse fields, not a genuine absence of relevant items. Relax and retry first.
- Which filters should I relax first?
- Drop the least intent-critical filters first — sparse nice-to-have attributes and wide numeric ranges — keeping core intent and safety filters (like dietary constraints) longest.
- How do I tell a too-narrow filter from a genuine no-match?
- Re-run the query with no hard filters. If results appear, the filters were the problem; if it's still empty, it's a genuine no-match worth surfacing to the user.
- What's the final fallback?
- A broader semantic-only query with all hard filters removed, so the user gets a useful if less precise result set instead of an empty page.
Keep reading
Why Payload Filtering Is Qdrant's Killer Feature (and How Filterable HNSW Actually Works)
Qdrant applies payload filters during HNSW graph traversal, not after — so filtered queries stay fast and always return a full top-k.
ReadCosine vs Dot Product vs Euclidean: Which Similarity Metric Should You Use?
On normalized embeddings all three metrics rank identically, so pick dot product for speed; magnitude only matters with unnormalized vectors.
ReadFrom Cloud Qdrant RAG to On-Device RAG: What Actually Transfers and What Doesn't
Moving a production cloud RAG pipeline on-device: the shape transfers, but index scale, embedder size, and re-ranking budgets do not.
ReadGet new essays by email
Occasional field notes on production RAG and LLM systems. No spam, unsubscribe anytime.