Резервный поиск
Когда AACsearch недоступен (сетевой сбой, текущий инцидент), корректно деградируйте до статического снимка каталога или запроса к базе данных — никогда не показывайте пустую страницу.
Строка поиска, возвращающая «Ошибка сети, попробуйте позже», хуже, чем строка поиска, возвращающая правильный ответ чуть медленнее. Этот рецепт подключает AACsearch как основной путь и резервный вариант как страховочную сетку, прозрачно для пользователя.
Что вы создаёте
Хелпер searchWithFallback(), который:
- Сначала вызывает AACsearch.
- При сетевой ошибке или 5xx переключается на локальный снимок каталога или запрос к Postgres.
- Логирует случай использования резервного варианта, чтобы вы могли отслеживать деградацию.
- Никогда не выбрасывает исключение в UI — всегда возвращает результат установленной формы.
Хелпер
import { SearchClient, AacSearchError } from "@aacsearch/client";
import { fallbackQuery } from "@/lib/fallback";
const client = new SearchClient({
baseUrl: process.env.AACSEARCH_BASE_URL!,
apiKey: process.env.AACSEARCH_SEARCH_KEY!,
indexSlug: "products",
});
type SearchOptions = Parameters<typeof client.search>[0];
type SearchResult = Awaited<ReturnType<typeof client.search>>;
export async function searchWithFallback(
opts: SearchOptions,
): Promise<SearchResult & { fallback?: true }> {
try {
const result = await client.search(opts);
return result;
} catch (err) {
const isFallbackable =
err instanceof AacSearchError &&
(err.code === "search_failed" ||
err.code === "service_unavailable" ||
err.code === "network_error");
if (!isFallbackable) {
// Аутентификация / лимит запросов / квота — это исправимые ошибки, не преходящие
throw err;
}
console.warn("[search] AACsearch unavailable, using fallback:", err.code);
// Отправьте метрику, чтобы видеть, как часто это происходит
metric.increment("search.fallback", { tags: { reason: err.code } });
const fallbackHits = await fallbackQuery(opts);
return {
hits: fallbackHits,
found: fallbackHits.length,
page: opts.page ?? 1,
perPage: opts.perPage ?? 10,
facetCounts: [],
searchTimeMs: 0,
fallback: true,
};
}
}Две стратегии резервного поиска
Стратегия A: ночной снимок каталога (рекомендуется для ecommerce)
Ночной cron экспортирует доступный для поиска каталог в статический JSON-файл в вашем CDN:
// scripts/snapshot-catalog.ts (запуск через cron в 02:00 UTC)
import { AdminClient } from "@aacsearch/client";
import { writeFile } from "node:fs/promises";
const admin = new AdminClient({
baseUrl: process.env.AACSEARCH_BASE_URL!,
apiKey: process.env.AACSEARCH_ADMIN_KEY!,
projectId: process.env.AACSEARCH_ORG_ID!,
});
const all = await admin.exportDocuments("products"); // потоковая выгрузка всех документов
await writeFile(
"public/catalog-snapshot.json",
JSON.stringify(
all.map((d) => ({
id: d.id,
title: d.title,
brand: d.brand,
price: d.price,
categories: d.categories,
})),
),
);Затем fallbackQuery выполняет сканирование по подстроке:
let cachedCatalog: Product[] | null = null;
async function loadCatalog() {
if (cachedCatalog) return cachedCatalog;
const res = await fetch("/catalog-snapshot.json");
cachedCatalog = await res.json();
setTimeout(() => (cachedCatalog = null), 30 * 60 * 1000); // кеш на 30 мин
return cachedCatalog;
}
export async function fallbackQuery(opts: SearchOptions) {
const catalog = await loadCatalog();
const q = opts.q?.toLowerCase() ?? "";
const matches = catalog.filter(
(p) =>
p.title.toLowerCase().includes(q) || p.brand.toLowerCase().includes(q),
);
const start = ((opts.page ?? 1) - 1) * (opts.perPage ?? 24);
return matches
.slice(start, start + (opts.perPage ?? 24))
.map((document) => ({ document }));
}Поиск по подстроке гораздо хуже ранжирования AACsearch, но он возвращает хоть что-то релевантное. Приемлемо для 5-минутного отключения.
Стратегия B: запрос к базе данных (для self-hosted или B2B)
Если вы контролируете исходные данные, запрашивайте Postgres напрямую:
import { sql } from "@vercel/postgres";
export async function fallbackQuery(opts: SearchOptions) {
const q = opts.q ?? "";
const { rows } = await sql`
SELECT id, title, brand, price, categories
FROM products
WHERE title ILIKE ${`%${q}%`}
OR brand ILIKE ${`%${q}%`}
ORDER BY popularity DESC
LIMIT ${opts.perPage ?? 24}
OFFSET ${((opts.page ?? 1) - 1) * (opts.perPage ?? 24)}
`;
return rows.map((document) => ({ document }));
}Это точнее, чем стратегия со снимком, потому что данные актуальные, но создаёт нагрузку на основную базу данных — жизнеспособно, только если у БД есть запас мощности.
UI: отображение деградации
Сообщите пользователю, когда он видит резервные результаты, чтобы он понимал, почему фильтры или фасеты могут отсутствовать:
{
result.fallback && (
<div className="degraded-banner">
Поиск в данный момент использует резервный индекс. Некоторые фильтры и
сортировка недоступны.
<a href="https://status.aacsearch.com">Проверить статус</a>
</div>
);
}Мониторинг
Строка metric.increment("search.fallback") выше критически важна. Без неё вы можете неделями работать на резервном пути, не зная об этом. Подключите её к вашему мониторингу:
// Datadog / Cloudwatch / Honeycomb
metric.increment("search.fallback", { tags: { reason: err.code } });
// Или структурированный лог, если у вас нет метрик
logger.warn({ event: "search.fallback", reason: err.code });Добавьте алерт: «вызов дежурного, если search.fallback > 100/час».
На что НЕ переключаться
- 401 / 403 — ваша аутентификация сломана. Резервный вариант скрывает проблему.
- 429 — у вас реальный всплеск нагрузки или проблема с квотой. Решение — масштабирование, а не маскировка.
- 400 — сам запрос некорректен. Резервный вариант вернёт неверно выглядящие результаты.
Эти ошибки должны доходить до UI, чтобы дежурный инженер их видел.
Резервный вариант — это страховочная сетка, а не замена. Если вы обнаруживаете, что полагаетесь на него для обычного трафика, это сигнал к масштабированию AACsearch (повышение лимитов запросов, обновление тарифа, обращение в поддержку) — а не к дальнейшим инвестициям в резервный путь.
Связанные страницы
- Ошибки и лимиты запросов — какие коды ошибок можно повторять
- Медленный поиск — когда переключаться на резерв vs ждать
- Инструкция по DR-восстановлению — для общесервисных отключений
Рекомендации при отсутствии результатов
Когда `found: 0`, не показывайте пустую страницу. Покажите подборку бестселлеров, подсказки «возможно, вы имели в виду» и поисковую рекомендацию, которая восстанавливает поток пользователя.
B2B-каталог с ограниченным доступом
Ценовые уровни для каждого клиента, видимость SKU по клиентам и каталоги на основе контрактов — всё обеспечивается на серверной стороне через токены поиска с ограниченной областью действия.