AACsearch
SDKРецепты

Резервный поиск

Когда 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 (повышение лимитов запросов, обновление тарифа, обращение в поддержку) — а не к дальнейшим инвестициям в резервный путь.

Связанные страницы

On this page