# API поиска и получения товаров для Shopping Assistant

Если Shopping Assistant должен подбирать товары через **поиск и ранжирование вашего магазина**, реализуйте HTTPS API по контракту на этой странице. Нужны две ручки: поиск товаров по текстовому запросу и получение одного товара по SKU. Gravity Field вызывает ваш API во время диалога и передаёт полученные карточки ассистенту. Подключение и параметры вызова согласуются с командой Gravity Field.

Для подключения используется **BYOA — Bring Your Own Algorithm**. В этом сценарии BYOA связывает Shopping Assistant с вашим поисковым API. Ваша система определяет состав и порядок товаров-кандидатов; ассистент выбирает из них карточки для ответа покупателю. Настройки рекомендательных стратегий Gravity Field не применяются к этому поисковому ответу автоматически.

Если собственный поиск не нужен, используйте стандартный поисковый контур Shopping Assistant. Формат клиентского API ниже тогда не требуется.

📖 Подробнее о продукте: [Как устроен Shopping Assistant](/blog/2026-08-20-how-shopping-assistant-works.md). [BYOA для рекомендаций](/blog/2026-07-15-byoa-recommendation-algorithms.md) описывает другой сценарий — подключение алгоритма к рекомендательной стратегии.

## Как устроено подключение

```text
Покупатель
   │  реплика в диалоге
   ▼
Shopping Assistant
   │  поисковый запрос или SKU товара, контекст
   ▼
Поисковый коннектор Gravity Field (BYOA)
   │  POST на нужную ручку вашего API — в момент запроса
   ▼
API клиента
   │  результаты поиска или карточка товара
   ▼
Shopping Assistant — ответ пользователю
```

Разделение ответственности:

| Уровень | За что отвечает |
| :--- | :--- |
| Shopping Assistant | Понимание диалога, формулировка поисковой задачи, выбор карточек из результатов и ответ покупателю |
| Gravity Field | Подключение поискового API к ассистенту, передача запроса и обработка ответа; управление показом ассистента и событиями через стандартную интеграцию платформы |
| API клиента | Поиск и ранжирование товаров-кандидатов, получение карточки по SKU с учётом наличия и ассортимента |

Клиентский сайт или приложение продолжает работать с обычным [`POST /shopping/generate`](./api_reference.md#post-shoppinggenerate). Формат вашего поискового API относится к серверному вызову Gravity Field → API клиента; браузер покупателя этот endpoint не вызывает.

## Общие требования к API

- HTTPS, метод `POST`, тело и ответ в JSON.
- Вызов идёт server-to-server, со стороны Gravity Field. Публичный доступ из браузера покупателя не предусматривается.
- Авторизация: заголовок `Authorization: Bearer <API key>`. Ключ доступа к вашему API согласуется при подключении и хранится в защищённой конфигурации Gravity Field; в запросе он передаётся только в заголовке и не должен попадать в логи или ответ.
- Обе ручки только читают данные и не меняют состояние каталога, корзины или профиля.
- Идемпотентность: повторный запрос с тем же `requestId` не должен приводить к побочным эффектам.
- Устойчивость к неизвестным полям в запросе: новые опциональные поля могут добавляться без смены версии контракта.

## Поиск товаров

Адрес ручки задаёт клиент и фиксирует при подключении. В примерах ниже — `https://search.example.com/api/v1/assistant/search`.

```http
POST https://search.example.com/api/v1/assistant/search
Content-Type: application/json
Authorization: Bearer <API key>
X-Request-Id: <uuid>
```

```ts
type SearchFilter =
  | { field: string; operator: "eq"; value: string | number | boolean }
  | { field: string; operator: "in"; value: Array<string | number | boolean> }
  | { field: string; operator: "gte" | "lte"; value: number };

type SearchQuery = {
  id: string;
  query: string;
  limit: number;
  filters?: SearchFilter[];
};

type AssistantSearchRequest = {
  requestId: string;
  sectionId: string;
  lng?: string;
  user?: { uid?: string; session?: string };
  queries: SearchQuery[];
};
```

### Поля запроса

| Поле | Обязательно | Описание |
| :--- | :--- | :--- |
| `requestId` | да | Сквозной идентификатор вызова. Возвращается в ответе и используется для корреляции логов клиента и платформы. Продублирован в заголовке `X-Request-Id`. |
| `sectionId` | да | ID секции Gravity Field, в которой работает Shopping Assistant. Это не название страницы, экрана или товарного слота. |
| `lng` | при наличии региональных или складских данных | Код региона, магазина или склада из `runtimeContext.ctx.lng` в запросе Shopping Assistant. Используйте то же значение, что в контексте Gravity Field и региональных полях фида. По нему выбираются ассортимент, наличие и цена. |
| `user.uid` | нет | Идентификатор пользователя Gravity Field. Передаётся внешнему API только после согласования персонализации и доступа к этому идентификатору. Без него выдача должна быть корректной и обезличенной. |
| `user.session` | нет | Идентификатор сессии пользователя. |
| `queries[]` | да | Текстовые поисковые задачи в одном вызове. **До 12 элементов в батче.** |
| `queries[].id` | да | Идентификатор задачи для сопоставления с ответом. Уникален внутри запроса. |
| `queries[].query` | да | Поисковый запрос, 1–500 символов. |
| `queries[].limit` | да | Сколько кандидатов вернуть. **До 100.** |
| `queries[].filters[]` | нет | Жёсткие ограничения каталога, в том числе по цене. Имена полей, типы и значения должны соответствовать товарному фиду Gravity Field. |

Каждый элемент `queries[]` содержит `query` и `limit`. Для получения конкретного товара по SKU используйте [отдельную ручку](#получение-товара-по-sku).

`lng` — код зоны обслуживания, **не** координата долготы. Если наличие или цена зависят от склада, клиентский канал передаёт код склада в [`runtimeContext.ctx.lng`](./api_reference.md#как-заполнять-ctx), а Gravity Field передаёт тот же код в ваш поисковый API. Все задачи одного батча выполняются для одного `lng`; для разных складов нужны отдельные вызовы. Если для каталога `lng` обязателен, при его отсутствии или неизвестном значении возвращайте `400` и не выбирайте произвольный склад. `ctx.location` содержит адрес страницы или ID экрана и не заменяет `lng`.

Если поиску нужен контекст конкретной страницы, экрана или товарного слота, согласуйте для него отдельное поле при подключении. Не подменяйте им `sectionId`: это идентификатор секции Gravity Field.

### Batch и single

Предпочтительный формат — **batch**: один HTTP-запрос, до 12 текстовых запросов в `queries[]`. Это меньше раунд-трипов и проще соблюсти общий бюджет вызова.

Если ручка клиента умеет обрабатывать только одну поисковую задачу за раз, допустим **single-режим**: `queries` содержит ровно один элемент, а платформа вызывает ручку для нескольких задач параллельно. Режим согласовывается при подключении и не меняется без уведомления.

В batch-режиме задачи обрабатывайте **параллельно**, а не последовательно: бюджет вызова общий.

### Фильтры

Gravity Field формирует фильтры на основе [товарного фида](/Integration/products_catalogues/general_reqs.md). Поэтому `field` совпадает с именем поля в фиде. Для `eq` и `in` используются канонические значения этого поля из фида; для `gte` и `lte` — числовая граница в тех же единицах, что и в фиде. API поиска должен понимать эти же имена полей, типы и значения. Список фильтруемых полей и операторов согласуется при подключении.

- Операторы: `eq`, `in`, `gte`, `lte`.
- Одно поле — один фильтр в рамках задачи.
- Для `eq` передавайте одно значение, для `in` — массив значений, для `gte` и `lte` — число.
- Если поля нет в фиде или значение для `eq`/`in` нельзя однозначно сопоставить со значением фида, Gravity Field оставляет условие в `query` и не добавляет фильтр.

## Контракт ответа

```ts
type SearchProduct = {
  sku: string;
  name: string;
  price?: number | null;
  old_price?: number | null;
  brand?: string;
  url?: string;
  image_url?: string;
  features?: string;
  attributes?: Record<string, string | number | boolean | Array<string | number | boolean>>;
  category?: string;
  category_id?: string;
  group_id?: string;
  in_stock?: boolean;
};

type SearchQueryResult = {
  id: string;
  products: SearchProduct[];
  totalHits?: number;
};

type AssistantSearchResponse = {
  requestId: string;
  results: SearchQueryResult[];
};
```

### Поля карточки

| Поле | Обязательно | Описание |
| :--- | :--- | :--- |
| `sku` | да | Идентификатор товара. Должен совпадать с `sku` в [товарном фиде](/Integration/products_catalogues/general_reqs.md). |
| `name` | да | Название товара для карточки и для ответа ассистента. |
| `price` | нет | Текущая цена. `null` допустим, если цены нет в каталоге. |
| `old_price` | нет | Цена до скидки. |
| `brand` | нет | Бренд. |
| `url` | нет | Ссылка на карточку товара. |
| `image_url` | нет | Ссылка на изображение. |
| `features` | нет | Краткое описание ключевых характеристик одной строкой. |
| `attributes` | нет | Дополнительные характеристики товара с именами полей вашего каталога, например `ram_gb` или `storage_gb`. Их можно передавать, даже если этих полей нет в товарном фиде Gravity Field; ассистент использует их при сравнении товаров. Поля, которых нет в фиде, не передаются в `filters`. |
| `category` | нет | Название категории или путь. |
| `category_id` | нет | Идентификатор категории каталога. |
| `group_id` | нет | Группа товарных вариантов для дедупликации. |
| `in_stock` | для товара, полученного по SKU | Наличие для переданного `lng`. В результатах поиска не возвращайте товары с `in_stock: false`. При запросе по SKU укажите `true` или `false` для найденного товара. |

### Поля ответа

| Поле | Обязательно | Описание |
| :--- | :--- | :--- |
| `requestId` | да | Эхо `requestId` из запроса. |
| `results[]` | да | Результат по каждой задаче из `queries[]`. |
| `results[].id` | да | `id` исходной задачи. Задача без результата всё равно присутствует, с пустым `products`. |
| `results[].products[]` | да | Товары-кандидаты **в порядке ранжирования вашего поиска**. |
| `results[].totalHits` | нет | Общий размер выдачи по задаче, если он известен. |

### Правила ответа

- **Порядок `products` отражает ранжирование вашего поиска.** Сохраняйте его в ответе API: он помогает ассистенту оценить кандидатов. Ассистент может выбрать часть товаров и сгруппировать их в ответе, поэтому позиция товара в поисковой выдаче не гарантирует ту же позицию в интерфейсе покупателя.
- **Дедупликация вариантов — на вашей стороне.** Если используете `group_id`, возвращайте один карточный вариант на группу. Дубли, которые придётся схлопывать уже после ответа, искажают и порядок, и размер выдачи.
- **Наличие и ассортиментские ограничения — на вашей стороне.** В результатах поиска отдавайте товары, доступные для переданного `lng`: постфильтрация на платформе укорачивает страницу и ломает связь с `totalHits`.
- Не возвращайте карточки без `sku` или `name`: они не проходят проверку контракта и не могут использоваться в ответе ассистента.
- `results[]` обязан покрывать все `id` из запроса, порядок элементов не важен.
- Не возвращайте в карточках внутренние счётчики, скрытые признаки ранжирования и отладочные поля.

## Получение товара по SKU

Для запроса сведений о конкретном товаре реализуйте вторую ручку. Её адрес также задаётся при подключении; в примере — `https://search.example.com/api/v1/assistant/product`. Передавайте точный `sku` из товарного фида и код склада `lng`, если от него зависят цена и наличие. Ручка ищет точное совпадение SKU и не выполняет текстовый поиск.

```http
POST https://search.example.com/api/v1/assistant/product
Content-Type: application/json
Authorization: Bearer <API key>
X-Request-Id: <uuid>
```

```ts
type AssistantProductRequest = {
  requestId: string;
  sectionId: string;
  sku: string;
  lng?: string;
};

type AssistantProductResponse = {
  requestId: string;
  product: (SearchProduct & { in_stock: boolean }) | null;
};
```

`requestId`, `sectionId` и `lng` имеют тот же смысл, что в поисковом запросе. `requestId` дублируется в `X-Request-Id` и возвращается в ответе. `sku` обязателен; пустое значение — ошибка запроса (`400`). Если для каталога обязателен `lng`, его отсутствие или неизвестное значение также возвращайте как `400`. Если SKU не найден, верните `200` с `product: null`. Для найденного товара обязательно укажите `in_stock` с учётом переданного `lng`. Если он недоступен, верните карточку с `in_stock: false`: ассистент сможет распознать товар, не предлагая его как доступный к покупке.

### Пример запроса товара

```bash
curl --request POST \
  --url 'https://search.example.com/api/v1/assistant/product' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <API key>' \
  --header 'X-Request-Id: 192aff63-c11a-421f-a8cd-f66ed4431a67' \
  --data '{
    "requestId": "192aff63-c11a-421f-a8cd-f66ed4431a67",
    "sectionId": "670ccaae56afcafaf808c146",
    "sku": "LAPTOP-015",
    "lng": "msk-01"
  }'
```

### Пример ответа о товаре

```json
{
  "requestId": "192aff63-c11a-421f-a8cd-f66ed4431a67",
  "product": {
    "sku": "LAPTOP-015",
    "name": "Ноутбук Vector 14",
    "price": 74990,
    "brand": "Vector",
    "url": "https://shop.example.com/products/laptop-015",
    "image_url": "https://shop.example.com/images/laptop-015.jpg",
    "features": "Экран 14 дюймов; 8 ГБ RAM; SSD 512 ГБ",
    "category": "Ноутбуки",
    "category_id": "laptops",
    "group_id": "vector-14",
    "in_stock": true
  }
}
```

## SKU и товарный фид

`sku` в ответах вашего API и `sku` в [товарном фиде](/Integration/products_catalogues/general_reqs.md) должны обозначать один и тот же товар. Это нужно для связки карточек с каталогом и корректной интерпретации показов, кликов и покупок в Gravity Field. Если у вас несколько идентификаторов товара, согласуйте правило сопоставления до запуска.

Даже когда карточки целиком приходят из вашего API, товарный фид синхронизировать нужно:

- он обеспечивает единый каталог для остальных контуров Gravity Field: рекомендаций, аудиторий, аналитики и A/B-тестов;
- без него показы, клики и покупки не сойдутся с товарами в отчётах;
- атрибуты фида могут использоваться в других сценариях платформы; применение правил к результатам именно вашего поиска согласовывается отдельно.

## Ошибки, пустой ответ и fallback

| Ситуация | Ответ вашего API | Как интерпретируется ответ |
| :--- | :--- | :--- |
| Нашлись товары | `200`, непустой `results[].products` или объект `product` | Обычный сценарий |
| Подходящих товаров нет или SKU не найден | `200`, `results[].products: []` или `product: null` | Ассистент уточняет параметры или перефразирует запрос |
| Невалидный запрос, обязательный `lng` отсутствует или неизвестен | `400` | Ошибка входных данных, которую нужно исправить до запуска |
| Ошибка авторизации | `401` | Ошибка подключения; проверьте ключ и настройки доступа |
| Истёк бюджет вызова | ответ не используется | Таймаут фиксируется в диагностике |
| `5xx`, битый JSON, сеть недоступна | — | Технический сбой фиксируется в диагностике |

Отдельные правила:

- **Отсутствие товаров — это `200` с пустым списком для поиска или `product: null` для SKU, а не ошибка.** Ошибка означает «API не ответил», пустой результат — «по вашей логике подходящего нет». Ассистент обрабатывает эти ситуации по-разному.
- **Резервный сценарий согласовывается при подключении.** При техническом сбое ассистент может использовать другой источник товаров или сообщить, что поиск временно недоступен. Не рассчитывайте на автоматическое переключение без согласованной настройки. Публичный ответ [`/shopping/generate`](./api_reference.md#post-shoppinggenerate) не содержит признака `fallback: true`; проверяйте источник выдачи и ошибки по диагностике запроса.
- **Бюджет вызова ограничен.** Ориентир для API поиска — около 5 секунд; точный таймаут и SLO для обеих ручек фиксируются при подключении. Целевое время ответа должно оставлять запас для формирования ответа ассистента.
- **Технический сбой не равен пустой выдаче.** Если задачу батча не удалось обработать, верните ошибку всего вызова (`5xx`), чтобы ассистент не счёл её результатом без товаров.

## Подключение и проверка

После реализации API передайте команде Gravity Field адреса обеих тестовых ручек и согласуйте способ авторизации, секцию, режим batch или single для поиска, фильтры и таймаут. Команда настроит вызовы через BYOA и проверит их вместе с вами.

- **Вызов выполняется во время диалога.** API должен выдерживать согласованную нагрузку и возвращать актуальные товары в пределах таймаута.
- **Проверяйте интеграцию целиком.** Сначала проверьте ручку напрямую, затем проведите тестовые диалоги через [`POST /shopping/generate`](./api_reference.md#post-shoppinggenerate) с `trafficType: "test"`. Режим `debug` возвращает заглушку и не вызывает поиск.
- **Сверяйте результаты по `requestId`.** Проверьте, какие задачи получил ваш API, какие кандидаты он вернул и какие карточки вошли в ответ ассистента. Число и порядок карточек могут отличаться после выбора ассистентом.
- **Согласовывайте изменения контракта.** Новые опциональные поля можно добавлять обратно совместимо. Удаление полей, смена типов и уменьшение допустимого `limit` требуют согласования с командой Gravity Field.

## Пример

### Запрос

В примере предполагается, что товарный фид содержит поле `ram_gb` со значением объёма памяти в гигабайтах.

```bash
curl --request POST \
  --url 'https://search.example.com/api/v1/assistant/search' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <API key>' \
  --header 'X-Request-Id: 8f2b1c0a-6c1d-4a0e-9f3b-2d4e5a6b7c8d' \
  --data '{
    "requestId": "8f2b1c0a-6c1d-4a0e-9f3b-2d4e5a6b7c8d",
    "sectionId": "670ccaae56afcafaf808c146",
    "lng": "msk-01",
    "queries": [
      {
        "id": "q1",
        "query": "ноутбук для работы 16 ГБ до 90 000 рублей",
        "limit": 20,
        "filters": [
          { "field": "ram_gb", "operator": "gte", "value": 16 },
          { "field": "price", "operator": "lte", "value": 90000 }
        ]
      }
    ]
  }'
```

### Ответ

```json
{
  "requestId": "8f2b1c0a-6c1d-4a0e-9f3b-2d4e5a6b7c8d",
  "results": [
    {
      "id": "q1",
      "totalHits": 24,
      "products": [
        {
          "sku": "LAPTOP-101",
          "name": "Ноутбук Nova 14",
          "price": 79990,
          "old_price": 84990,
          "brand": "Nova",
          "url": "https://shop.example.com/products/laptop-101",
          "image_url": "https://shop.example.com/images/laptop-101.jpg",
          "features": "Экран 14 дюймов; 16 ГБ RAM; SSD 512 ГБ",
          "attributes": { "ram_gb": 16, "storage_gb": 512 },
          "category": "Ноутбуки",
          "category_id": "laptops",
          "group_id": "nova-14",
          "in_stock": true
        },
        {
          "sku": "LAPTOP-102",
          "name": "Ноутбук Orion 15",
          "price": 85990,
          "brand": "Orion",
          "url": "https://shop.example.com/products/laptop-102",
          "image_url": "https://shop.example.com/images/laptop-102.jpg",
          "features": "Экран 15,6 дюйма; 16 ГБ RAM; SSD 1 ТБ",
          "category": "Ноутбуки",
          "category_id": "laptops",
          "group_id": "orion-15",
          "in_stock": true
        }
      ]
    }
  ]
}
```

## Чек-лист перед подключением

- [ ] Обе ручки доступны по HTTPS, принимают `POST` с JSON и отвечают JSON.
- [ ] Реализована авторизация `Authorization: Bearer <API key>`.
- [ ] Принимается `requestId`, он же продублирован в `X-Request-Id` и возвращается в ответе.
- [ ] Поддерживается batch до 12 запросов, либо заявлен single-режим.
- [ ] `limit` на запрос принимает значения до 100.
- [ ] Поисковая ручка принимает `query` с `limit`; отдельная ручка возвращает товар по точному `sku`.
- [ ] Для выдачи с региональными ценами и остатками учитывается `lng` из контекста Gravity Field; товары, цена и наличие соответствуют этому складу или региону.
- [ ] Поисковый API принимает имена полей и канонические значения из товарного фида Gravity Field; числовые границы использует в единицах фида.
- [ ] Неизвестные поля и значения для `eq`/`in` не превращаются в фильтр: условие остаётся в `query`.
- [ ] Возвращаются полные карточки с обязательными `sku` и `name`.
- [ ] `sku` совпадает с `sku` товарного фида Gravity Field.
- [ ] Если используется `group_id`, варианты дедуплицируются до ответа; недоступные товары исключаются из поиска.
- [ ] Порядок `products` отражает ранжирование вашего поиска; допускается последующий выбор карточек ассистентом.
- [ ] Отсутствие товаров отвечается `200` с пустым `products`, неизвестный SKU — `200` с `product: null`.
- [ ] `results[]` покрывает все `id` запроса, включая пустые.
- [ ] Время ответа укладывается в согласованный бюджет с запасом.
- [ ] `requestId` пишется в логи для корреляции с платформой.
- [ ] Секреты не возвращаются в ответах и не попадают в логи.
- [ ] Есть тестовая среда для smoke-проверки через `POST /shopping/generate`.

## FAQ

#### Кто определяет порядок товаров?

Ваш API ранжирует кандидатов внутри каждой поисковой задачи. Shopping Assistant выбирает из них карточки для ответа покупателю и может сгруппировать их под разные части задачи. Поэтому порядок в ответе ассистента не обязан совпадать с полным списком кандидатов.

#### Обязательно ли синхронизировать товарный фид, если карточки приходят из моего API?

Да. Фид обеспечивает единый каталог Gravity Field и позволяет сопоставить карточки из вашего API с товарами в событиях. Для корректной атрибуции также настройте [трекинг Shopping Assistant](./tracking.md). Подробнее о фиде: [Требования к товарному фиду](/Integration/products_catalogues/general_reqs.md).

#### Нужна ли пагинация?

Нет. Ассистент работает с top-N результатов по каждой поисковой задаче. Поле `totalHits` возвращайте, если размер выдачи известен: он помогает диагностике, но от него не зависит рендеринг.

#### Как получить выдачу для конкретного склада?

Передайте код склада в `runtimeContext.ctx.lng` при вызове `/shopping/generate`. Gravity Field передаст тот же код в поле `lng` нужной ручки вашего API. Например, при `lng: "msk-01"` поиск вернёт товары, доступные на складе `msk-01`, а запрос по SKU — его цену и наличие на этом складе. Значение кода должно совпадать с настройками региональных данных [товарного фида](/Integration/products_catalogues/multi_language.md) или с согласованным справочником складов вашего поиска.

#### Как получить товар по SKU?

Используйте [ручку получения товара](#получение-товара-по-sku): передайте `sku` и при необходимости `lng`. Она вернёт одну карточку в `product`, `null` для неизвестного SKU или карточку с `in_stock: false`, если товар найден, но недоступен для указанного склада.

#### Что делать, если часть каталога временно недоступна?

Если по конкретной задаче товаров действительно нет, верните для неё пустой `products`. Если задачу не удалось обработать из-за технического сбоя, верните `5xx` для всего вызова; дальнейшее поведение ассистента зависит от резервного сценария, согласованного при подключении. Не подменяйте сбой пустой выдачей или выдуманными товарами.

#### Как проверить подключение до боевого трафика?

Разверните API на тестовой среде, подключите его к тестовой секции Gravity Field и прогоните сценарии через `POST /shopping/generate` с `trafficType: "test"`. По `requestId` сверяйте запросы и ответы вашего API с диагностикой ассистента. Убедитесь, что ассистент использует ожидаемые товары; он может показать не все найденные карточки. Режим `debug` для этого не подходит: он возвращает заглушку и к каталогу не обращается.

## Связанные материалы

[!ref icon="comment-discussion"](./shopping_assistant.md)

[!ref icon="terminal"](./integration.md)

[!ref icon="terminal"](./api_reference.md)

[!ref icon="graph"](./tracking.md)
