Несколько поисков в одном запросе
Как выполнить несколько поисков одновременно — для автодополнения, поиска одновременно в нескольких индексах.
Несколько поисков в одном запросе (мульти-поиск)
Если нужно выполнить несколько поисков одновременно (например, основной поиск + быстрые подсказки), сделайте это в одном HTTP-запросе. Это быстрее, чем отправлять несколько запросов подряд.
Основная идея
Вместо отправки двух запросов:
Запрос 1: ищу товары по "wireless"
Запрос 2: ищу подсказки по "wireless"Отправляю один запрос с двумя поисками одновременно, получаю оба результата вместе.
Пример: автодополнение (подсказки)
Когда пользователь печатает в поле поиска, вам нужно одновременно:
- Показать полный поиск (20 товаров с фасетами)
- Показать быстрые подсказки (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 запрос | 20 — searches.array().min(1).max(20) в public-handler.ts. |
Max perPage | 100 |
Max page | 1000 |
Max numTypos | 3 |
Max rangeFacets на search | 20 |
Превышение лимита батча — 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_found | indexSlug не существует или переименован. |
invalid_filter | filterBy не распарсился — обычно неэскейпленное значение. |
not_authorized | Scoped 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, его
scopedFilterAND-комбинируется в каждый sub-search (Инвариант 4). Token сprice:<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 keystrokes | 200 мс | 150–300 мс |
| Минимум символов | 2 | 2–3 |
| Размер клиентского LRU | 30 записей | 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-бакета — учитывайте.
Ограниченные токены для поиска
Как создать краткосрочный токен доступа для поиска определённых товаров — только товары в наличии, только доступная цена, только для одного пользователя.
Фильтры, сортировка и пагинация
Как настроить поиск товаров — фильтры по цене и бренду, сортировка по цене и дате, пошаговый просмотр результатов.