AACsearch
Поисковый API

Несколько поисков в одном запросе

Как выполнить несколько поисков одновременно — для автодополнения, поиска одновременно в нескольких индексах.

Несколько поисков в одном запросе (мульти-поиск)

Если нужно выполнить несколько поисков одновременно (например, основной поиск + быстрые подсказки), сделайте это в одном HTTP-запросе. Это быстрее, чем отправлять несколько запросов подряд.

Основная идея

Вместо отправки двух запросов:

Запрос 1: ищу товары по "wireless"
Запрос 2: ищу подсказки по "wireless"

Отправляю один запрос с двумя поисками одновременно, получаю оба результата вместе.

Пример: автодополнение (подсказки)

Когда пользователь печатает в поле поиска, вам нужно одновременно:

  1. Показать полный поиск (20 товаров с фасетами)
  2. Показать быстрые подсказки (5 товаров, только названия)
const [mainResults, suggestions] = await client.multiSearch([
	{
		// Основной поиск
		indexSlug: "products",
		q: "wireless head",
		queryBy: "title,brand,description",
		perPage: 20,
		facetBy: "brand,categories",
	},
	{
		// Быстрые подсказки
		indexSlug: "products",
		q: "wireless head",
		queryBy: "title",
		perPage: 5,
		includeFields: "id,title",  // Только названия для скорости
	},
]);

// Показать основные результаты в главной части
mainResults.hits.forEach(item => console.log(item.title));

// Показать подсказки в дропдауне
suggestions.hits.forEach(item => console.log(item.title));

Один HTTP-запрос, два набора результатов — быстро и эффективно.

Пример: поиск в разных индексах

Одновременный поиск товаров и статей:

const [products, articles] = await client.multiSearch([
	{
		indexSlug: "products",
		q: "sustainability",
		queryBy: "title,description",
		perPage: 10,
	},
	{
		indexSlug: "blog",
		q: "sustainability",
		queryBy: "content,title",
		perPage: 5,
	},
]);

console.log(`Товаров: ${products.found}, Статей: ${articles.found}`);

После получения результатов вы можете объединить и переранжировать их на стороне клиента — показать сначала товары, потом статьи.

Параметры поиска — что вы можете настроить

q — текст поиска

Что ищет пользователь:

{ "q": "wireless headphones" }   // Обычный поиск по названию и описанию
{ "q": "*" }                     // Все товары (обычно с фильтрами: filterBy)

queryBy — какие поля искать

Перечислите поля через запятую. Поля в начале получают больший приоритет:

{ "queryBy": "title,description,brand" }

Поля в начале — более релевантные. Например, если ищут "Apple", товар с "Apple" в названии (title) окажется выше, чем товар с "Apple" в описании.

filterBy — ограничивающие условия

Дополнительно ограничьте результаты (см. раздел Фильтры выше):

{ "filterBy": "availability:=in_stock && price:[50..500]" }

facetBy — статистика по полям

Получите количество товаров по каждому бренду, категории и т.д.:

{ "facetBy": "brand,categories" }

sortBy — порядок результатов

Как упорядочить (см. раздел Сортировка выше):

{ "sortBy": "price:asc" }

page и perPage — пошаговый просмотр

Выводите товары постранично (см. раздел Пагинация выше):

{ "page": 1, "perPage": 20 }

includeFields / excludeFields — нужные поля

Вернуть только конкретные поля (для скорости):

{ "includeFields": "id,title,price" }

Полезно для автодополнения — не отправляйте описание и картинки, если их не показываете.

Автоматическое исправление опечаток

AACsearch автоматически исправляет опечатки при поиске:

"наушнеки" → найдёт товары "наушники"
"ноутбук" → найдёт товары "ноутбуки" и "notebook"

Это работает по умолчанию — вам не нужно ничего настраивать.

Подсвечивание совпадающих слов

Если нужно показать, какие слова в названии товара совпали с поиском, используйте highlight:

{
	"q": "wireless",
	"highlightFields": "title",
	"highlightStartTag": "<em>",
	"highlightEndTag": "</em>"
}

Ответ:

{
	"highlights": [
		{
			"field": "title",
			"snippet": "Sony <em>Wireless</em> Headphones"
		}
	]
}

В приложении покажите эту подсвеченную версию вместо обычного названия.

Советы по скорости

  • Для автодополнения: используйте perPage: 5 и includeFields: "id,title" (только названия, без описаний)
  • Для больших поисков: добавьте дебаунс 150–300 мс, чтобы не отправлять запрос на каждый символ
  • Для фасетов: если их много, проверяйте их не на каждый поиск, а только когда показываете фильтры
  • Мультипоиск: если нужны одновременно основной результат и подсказки, используйте один мультипоиск вместо двух отдельных запросов

Лимиты

ЛимитЗначение
Max searches на multi-search запрос20searches.array().min(1).max(20) в public-handler.ts.
Max perPage100
Max page1000
Max numTypos3
Max rangeFacets на search20

Превышение лимита батча — 400 invalid_input с Zod-путём searches. Используйте два последовательных запроса вместо попыток батчить больше 20 — стоимость rate-limit per HTTP-вызов, так что два батча по 20 стоят столько же, сколько стоил бы один батч на 40.

Частичные сбои

Multi-search-запрос атомарен на HTTP-уровне — сам запрос либо 200 со всеми sub-результатами, либо 4xx/5xx для всего батча. Sub-search-и могут провалиться независимо внутри 200: каждая envelope несёт свой статус.

Shape ответа:

{
  results: Array<{
    // Success:
    hits?: SearchHit[];
    found?: number;
    page?: number;
    facetCounts?: FacetCount[];
    searchTimeMs?: number;

    // Failure (per sub-search):
    error?: string;        // напр., "collection_not_found", "invalid_filter"
    code?: number;         // HTTP-style, 4xx для клиент-ошибок
  }>;
}

Идите по results в порядке, любая запись с error — провал sub-search-а. Частые причины:

errorПричина
collection_not_foundindexSlug не существует или переименован.
invalid_filterfilterBy не распарсился — обычно неэскейпленное значение.
not_authorizedScoped token фиксирует индекс, а sub-search идёт в другой.
rate_limit_exceededБакет org-а опустел между sub-search-ами в одном батче.
internal_errorНеожиданный сбой upstream. Оригинальная engine-ошибка нормализована (Инвариант 6 — сырой не пишем).

Сбой одного sub-search-а не откатывает остальные. Рендерите успешные, проваленный покажите в UI, а не убивайте всю страницу.

Общая auth и квота

Аутентификация — per request, не per sub-search. Тот же Authorization: Bearer ss_search_* (или ss_scoped_*) валидирует весь батч:

  • Bearer-ключ проверяется один раз на границе запроса.
  • Если ключ — scoped token, его scopedFilter AND-комбинируется в каждый sub-search (Инвариант 4). Token с price:&lt;100 применится ко всем, даже если вы забыли его повторить.
  • Origin / referer — один раз на границе запроса.

Квота и rate-limit считают батч как одну единицу, но тарифицируют per sub-search: один multi-search-запрос тратит searches.length units из SearchRateLimitBucket (units: parsed.data.searches.length в public handler). Батч на 20 = 20× стоимость одиночного запроса. Держитесь под cap-ом окна — иначе 429.

Tenant isolation (Инвариант 5) — на уровне sub-search-а: каждый sub-search получает AND-комбинированный tenantId автоматически из verified-токена. Multi-search не может прочитать чужие данные.

Группировка в аналитике

Каждый multi-search-ответ несёт:

  • queryId на уровне запроса — родительский id всего батча.
  • queryId на каждом sub-результате — per-search id; полезно, когда один sub-search — подсказки, другой — основной список.

Click-события через POST /events/track должны нести детский queryId, чтобы per-search CTR считался корректно:

const [main, suggestions] = await client.multiSearch([…, …]);

trackEvent({
  type: "result_click",
  queryId: main.queryId,            // ← конкретный sub-search, по которому был клик
  productId: clickedHit.id,
  position: clickedHit.position,
});

Conversion следует тому же правилу. Для session-level дашбордов, которые атрибутируют конверсию всему батчу, родительский queryId тоже пишется и доступен в SearchUsageEvent.metadata.parentQueryId (см. Обзор Analytics).

Частый баг — писать клики против queryId первого sub-search-а независимо от того, по какому списку был клик. Это раздувает CTR подсказок и сдувает CTR основного списка. Всегда пробрасывайте конкретный child queryId.

Кэширование и debouncing

В виджете есть клиентский LRU для suggestions; выравнивайте кастомные интеграции на те же цифры:

ПараметрПо умолчанию в виджетеРазумный диапазон
Debounce keystrokes200 мс150–300 мс
Минимум символов22–3
Размер клиентского LRU30 записей20–50, scope indexSlug
TTL популярных запросов10 мин5–15 мин

Серверный кэш — дефолтные Hono response cache headers; не растягивайте без rate-limit-aware инвалидации — устаревшие suggestions выглядят ок, пока curation-правка не убьёт товары, и они продолжают вести в удалённые страницы.

Для empty-state «популярные запросы» кэшируйте результат search.topQueries на сервере с тем же TTL, что и кэш главной — фоновое обновление, не на каждый focus.

Пример федеративных подсказок

Основной поиск товаров + suggestion категорий + suggestion статей, в один round-trip:

const [products, categories, articles] = await client.multiSearch([
  {
    indexSlug: "products",
    q: "wireless head",
    queryBy: "title,brand,description",
    perPage: 20,
    facetBy: "brand,categories",
  },
  {
    indexSlug: "categories",
    q: "wireless head",
    queryBy: "name,aliases",
    perPage: 3,
    includeFields: "id,name,slug",
  },
  {
    indexSlug: "articles",
    q: "wireless head",
    queryBy: "title,excerpt",
    perPage: 3,
    includeFields: "id,title,url,published_at",
  },
]);

Товары — в основную сетку, категории — quick-links над сеткой, статьи — under как related-контент. Три sub-search-а = три units из rate-limit-бакета — учитывайте.

On this page