Рекомендации при отсутствии результатов
Когда `found: 0`, не показывайте пустую страницу. Покажите подборку бестселлеров, подсказки «возможно, вы имели в виду» и поисковую рекомендацию, которая восстанавливает поток пользователя.
Пустая страница «ничего не найдено» — это тупик. Этот рецепт заменяет её тремя взаимодополняющими восстановлениями: исправлением «возможно, вы имели в виду», популярными категориями и списком бестселлеров в качестве запасного варианта.
Что вы создаёте
┌─────────────────────────────────────────┐
│ По запросу "shooes" ничего не найдено │
│ │
│ Возможно, вы имели в виду: shoes │
│ │
│ Популярные категории: │
│ [Кроссовки] [Ботинки] [Сандалии] │
│ │
│ ── Бестселлеры ── │
│ [...12 карточек товаров...] │
└─────────────────────────────────────────┘Компонент
"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 () => {
// Запускаем все три спасательных запроса параллельно
const [spell, sellers, cats] = await Promise.all([
// 1. Проверка орфографии неудачного запроса
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. Топ-12 бестселлеров
client.search({
q: "*",
sortBy: "sales_count:desc",
perPage: 12,
}),
// 3. Топ-5 категорий по количеству товаров (фасет от "*")
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>По запросу "{query}" ничего не найдено</h2>
{didYouMean && (
<p className="did-you-mean">
Возможно, вы имели в виду{" "}
<a href={`?q=${didYouMean}`}>{didYouMean}</a>?
</p>
)}
{popularCategories.length > 0 && (
<section>
<h3>Популярные категории</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>Бестселлеры</h3>
<div className="product-grid">
{bestsellers.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</section>
)}
</div>
);
}Отслеживание неудачного запроса
Отправьте событие zero_results, чтобы находить повторяющиеся неудачи и добавлять для них синонимы или контент:
useEffect(() => {
if (query) {
fetch("/api/events/track", {
method: "POST",
body: JSON.stringify({
event: "zero_results",
properties: { query, indexSlug: "products" },
}),
});
}
}, [query]);Открывайте Поиск → Аналитика → «Топ запросов с нулевым результатом» еженедельно для сортировки.
«Возможно, вы имели в виду»: на сервере, а не на клиенте
Если ваша страница поиска рендерится на сервере, выполните проверку орфографии до рендеринга и передайте подсказку как prop. Пользователь увидит исправленную подсказку при первой отрисовке вместо мигания «ничего не найдено» с последующей отложенной коррекцией.
// 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]} />;
}Эвристики для UX «возможно, вы имели в виду»
- Показывайте подсказку, только если она действительно возвращает результаты — перезапустите поиск с предложенным запросом и проверьте
found > 0. - Если подсказка отличается на один символ, автоматически перенаправляйте:
if (typoDistance === 1) router.replace(?q=${suggestion}). - Для нелатинских письменностей (кириллица, арабский, CJK) точность проверки орфографии варьируется. Тестируйте перед включением.
Пустая категория vs пустой поиск
Они требуют разной обработки:
| Поверхность | Причина | Лучший ответ |
|---|---|---|
| Результаты поиска | Пользователь ввёл запрос, который ничего не нашёл | «Возможно, вы имели в виду» + популярные категории + бестселлеры |
PLP (/c/foo) | Категория существует, но пуста | Показать «Скоро будет» или перенаправить в родительскую категорию |
| 404 | Категория вообще не существует | Стандартный 404 |
Различайте, проверяя по списку категорий во время сборки:
const categories = await fetch("/api/categories").then((r) => r.json());
const exists = categories.some((c) => c.slug === slug);
if (!exists) notFound();Связанные страницы
- Пустые результаты — диагностика неожиданных нулевых запросов
- Аналитика кликов — событие
zero_results - Дашборд → Настройка релевантности — синонимы и курирование
Аналитика кликов
Отслеживайте, по каким результатам пользователи действительно кликают и какие запросы приводят к конверсиям — данные поступают в тюнер релевантности AACsearch и ваши собственные дашборды воронки.
Резервный поиск
Когда AACsearch недоступен (сетевой сбой, текущий инцидент), корректно деградируйте до статического снимка каталога или запроса к базе данных — никогда не показывайте пустую страницу.