AACsearch
SDKsRecetario

No-results recommendations

When `found: 0`, do not show a blank page. Show curated bestsellers, did-you-mean suggestions, and a search-tip that recovers the user's flow.

A blank "no results" page is a dead end. This recipe replaces it with three complementary recoveries: a did-you-mean correction, popular categories, and a fallback bestseller list.

What you build

┌─────────────────────────────────────────┐
│ No results for "shooes"                  │
│                                          │
│ Did you mean: shoes                      │
│                                          │
│ Try a popular category:                  │
│ [Sneakers] [Boots] [Sandals]             │
│                                          │
│ ── Bestsellers ──                        │
│ [...12 product cards...]                 │
└─────────────────────────────────────────┘

Component

"use client";

import { useEffect, useState } from "react";
import { SearchClient } from "@aacsearch/client";

const client = new SearchClient({
	baseUrl: process.env.NEXT_PUBLIC_AACSEARCH_BASE_URL!,
	apiKey: process.env.NEXT_PUBLIC_AACSEARCH_SEARCH_KEY!,
	indexSlug: "products",
});

export function NoResults({ query }: { query: string }) {
	const [didYouMean, setDidYouMean] = useState<string | null>(null);
	const [bestsellers, setBestsellers] = useState<Product[]>([]);
	const [popularCategories, setPopularCategories] = useState<string[]>([]);

	useEffect(() => {
		(async () => {
			// Fire all three rescue queries in parallel
			const [spell, sellers, cats] = await Promise.all([
				// 1. Spell-check the failing query
				fetch(`/api/v1/projects/${process.env.NEXT_PUBLIC_AACSEARCH_ORG_ID}/spell-check`, {
					method: "POST",
					headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_AACSEARCH_SEARCH_KEY}` },
					body: JSON.stringify({ q: query }),
				}).then((r) => r.json()),

				// 2. Top 12 bestsellers
				client.search({
					q: "*",
					sortBy: "sales_count:desc",
					perPage: 12,
				}),

				// 3. Top 5 categories by product count (facet of "*")
				client.search({
					q: "*",
					perPage: 0,
					facetBy: "categories",
					maxFacetValues: 5,
				}),
			]);

			if (spell.suggestions?.[0] && spell.suggestions[0] !== query) {
				setDidYouMean(spell.suggestions[0]);
			}
			setBestsellers(sellers.hits.map((h) => h.document));
			const catFacet = cats.facetCounts.find((f) => f.field === "categories");
			setPopularCategories(catFacet?.counts.map((c) => c.value) ?? []);
		})();
	}, [query]);

	return (
		<div className="no-results">
			<h2>No results for &quot;{query}&quot;</h2>

			{didYouMean && (
				<p className="did-you-mean">
					Did you mean <a href={`?q=${didYouMean}`}>{didYouMean}</a>?
				</p>
			)}

			{popularCategories.length > 0 && (
				<section>
					<h3>Try a popular category</h3>
					<div className="category-chips">
						{popularCategories.map((cat) => (
							<a key={cat} href={`/c/${cat.toLowerCase()}`} className="chip">
								{cat}
							</a>
						))}
					</div>
				</section>
			)}

			{bestsellers.length > 0 && (
				<section>
					<h3>Bestsellers</h3>
					<div className="product-grid">
						{bestsellers.map((p) => (
							<ProductCard key={p.id} product={p} />
						))}
					</div>
				</section>
			)}
		</div>
	);
}

Track the failed query

Send a zero_results event so you can find recurring failures and add synonyms / content for them:

useEffect(() => {
	if (query) {
		fetch("/api/events/track", {
			method: "POST",
			body: JSON.stringify({
				event: "zero_results",
				properties: { query, indexSlug: "products" },
			}),
		});
	}
}, [query]);

Open Search → Analytics → "Top zero-result queries" weekly to triage.

Did-you-mean: server-side, not client

If your search page is server-rendered, do the spell-check before render and pass the suggestion as a prop. The user sees the corrected suggestion on first paint instead of a flash of "no results" followed by a delayed correction.

// app/search/page.tsx
const result = await client.search({ q: searchParams.q, queryBy: "title" });

if (result.found === 0) {
	const spell = await fetch(`...spell-check`, { ... }).then((r) => r.json());
	return <NoResults query={searchParams.q} suggestion={spell.suggestions?.[0]} />;
}

Heuristics for "did you mean" UX

  • Only show the suggestion if it actually returns results — re-run a search with the suggested query and check found > 0.
  • If the suggestion is a single character difference, auto-redirect: if (typoDistance === 1) router.replace(?q=${suggestion}).
  • For non-Latin scripts (Cyrillic, Arabic, CJK), spell-check accuracy varies. Test before enabling.

These warrant different treatments:

SurfaceCauseBest response
Search resultsUser typed a query that matched nothingDid-you-mean + popular cats + bestsellers
PLP (/c/foo)Category exists but is emptyShow "Coming soon" or redirect to parent category
404Category does not exist at allStandard 404

Distinguish by checking against the category list at build time:

const categories = await fetch("/api/categories").then((r) => r.json());
const exists = categories.some((c) => c.slug === slug);
if (!exists) notFound();

On this page