# Поиск упущенного спроса

Анализ существующей рекламной кампании Яндекс Директ для нахождения ключевых фраз, которые не покрыты текущей семантикой.

## Требования

- XLSX-выгрузка из Яндекс Директ (лист должен называться **«Тексты»** — стандартный формат экспорта)
- Формат файла: `.xlsx` (не `.xls`)
- Настроенный `YANDEX_WORDSTAT_TOKEN` в `config/.env`

## Ссылки на редактирование групп

В начале работы спроси у пользователя **логин Яндекс Директ** (ulogin). Это нужно для формирования прямых ссылок на редактирование групп.

ID кампании = имя файла без расширения (например, `54939351.xlsx` → `54939351`).

Формат ссылки:
```
https://direct.yandex.ru/dna/groups-edit?ulogin={login}&campaigns-ids={campaign_id}&groups-ids={group_id}
```

В отчёте по каждой группе добавляй ссылку, чтобы пользователь мог сразу перейти и добавить найденные фразы.

## Режим работы: последовательный vs параллельный

Анализ всех групп кампании занимает время (1 API-запрос ~ 1-3 сек, 2 запроса на группу).

**Если Tasks (субагенты) доступны** (например, Claude Code с Task tool):
- Можно запустить анализ всех групп параллельно, по 10-15 групп на субагента
- Сначала покажи пользователю все группы, дай выбрать scope
- Запускай 3-5 субагентов параллельно, каждый обрабатывает свой батч
- В конце — объединённый отчёт

**Если Tasks НЕ доступны** (Claude Web, API без субагентов):
- Работай последовательно, по 1-2 группы за раз
- После каждой группы обсуждай результаты с пользователем
- Спрашивай: «Продолжить со следующей группой или остановимся?»
- Это экономит контекст и даёт пользователю контроль

## Типы запросов и стратегии расширения

Перед расширением определи тип группы. Разные типы запросов расширяются по-разному.

### Транзакционные запросы

Группы с коммерческим интентом (купить, заказать, цена). Расширение:
- **Действия**: купить -> заказать, цена, стоимость, где купить, сколько стоит
- **Объекты**: синонимы, разговорные формы (проигрыватель -> вертушка, музыкальный центр -> муз центр)
- **Модификаторы**: стилевые (ретро -> винтажный, в стиле ретро, под ретро, старинный)
- **Дополнительные**: география (в москве -> в интернет магазине, недорого, с доставкой)

### Брендированные запросы

Группы по названию бренда (Crosley, Victrola, Muse). Расширение ограничено:
- **Транслит**: crosley -> кросли, victrola -> виктрола
- **Пробелы/дефисы**: playbox -> play box, roadstar -> road star
- **Расшифровка аббревиатур**: если бренд — аббревиатура
- **НЕ расширяй** брендовую компоненту на другие бренды или generic-слова
- Добавлять действия/модификаторы можно, но бренд — only transliteration

### Навигационные запросы

Группы с географическим интентом (адрес, как проехать, где находится). Расширение:
- **Метро/улицы**: автосалон белорусская, магазин беломорская
- **Районы**: автосалон юзао, магазин центр москвы
- **Проверь флаг**: навигационные запросы релевантны только для offline-точек. Для чисто онлайн-магазинов — не ищи адреса
- **Спроси пользователя**: «У вас есть офлайн-точка/шоурум? Стоит ли искать навигационные запросы (метро, улицы)?»

### Информационные запросы

Группы с информационным интентом (как выбрать, какой лучше, обзор, отзывы). Расширение:
- **Перефразировка задачи**: как выбрать проигрыватель -> какой проигрыватель лучше, рейтинг проигрывателей
- **Смежные задачи**: обзор проигрывателей -> сравнение проигрывателей, топ проигрывателей
- **НЕ добавляй** транзакционные слова к информационным группам (и наоборот)

## Workflow

### Шаг 1: Проверка подключения

```bash
bash scripts/quota.sh
```

### Шаг 2: Парсинг XLSX

```bash
uv run --script scripts/missed_demand.py parse-xlsx /path/to/export.xlsx
```

Выход — JSON со всеми группами. Покажи пользователю таблицу:

| ID группы | Название | Фраз |
|-----------|----------|------|
| 4302852986 | Ретро-телефоны | 52 |
| ... | ... | ... |

Спроси пользователя, какую группу (или все) анализировать.

### Шаг 3: Получение фраз группы

```bash
uv run --script scripts/missed_demand.py parse-xlsx /path/to/export.xlsx --group <group_id>
```

### Шаг 4: LLM-сегментация

Разбей фразы группы на слоты. Используй **plain слова без кавычек и операторов**. Операторы (`+` для стоп-слов) добавит `build-query` автоматически.

**Не используй знаки препинания в слотах.** Пиши `муз центр`, а не `муз. центр`. Скрипт автоматически удаляет точки, запятые и прочую пунктуацию, но лучше не давать её изначально.

Слоты:
- **objects** — объект рекламирования (телефон, проигрыватель, приёмник). Это то, ЧТО продают. Не используй абстрактные слова вроде «аппарат» — проверяй, ищут ли так реально.
- **actions** — действия (купить, заказать, продажа, ремонт)
- **modifiers** — свойства/определения (ретро, винтажный, старинный, дисковый, телефонный, настенный). Сюда идут прилагательные и характеристики объекта.
- **additional** — дополнительные (в москве, недорого, срочно)

Пример для группы «Ретро-телефоны»:
```json
{
  "objects": ["телефон"],
  "actions": ["купить", "заказать"],
  "modifiers": ["ретро", "винтажный", "старинный", "телефонный"],
  "additional": ["в москве"]
}
```

Правила:
- `objects` обязателен (если не ясен — фолбэк на частотное ядро фраз)
- Остальные слоты опциональны
- Дефис внутри слова допустим (санкт-петербург, б/у)
- НЕ ставь `+`, `-`, `!`, `"`, `|`, `(`, `)` — build-query сделает сам
- Синонимы объекта должны быть самостоятельными поисковыми словами. «Аппарат» без контекста — не ищут. «Телефонный аппарат» — ищут, но «телефонный» лучше в modifiers.
- **Определи тип группы** (транзакционная/брендированная/навигационная/информационная) и применяй соответствующую стратегию расширения
- **Убирай избыточные варианты в OR-группе.** Если есть однословный вариант «ретро», то многословные «под ретро», «в стиле ретро» избыточны — они **буквально содержат** слово «ретро» и уже покрыты им. Общее правило: многословный вариант, содержащий слово из однословного варианта в той же OR-группе, всегда является его подмножеством и не добавляет спроса. **Но семантические синонимы с другими словами — всегда добавляют**: «состаренный», «под старину», «старинный» — это другие леммы, не покрытые словом «ретро», и их надо включать

### Шаг 4.1: Батчинг для больших групп (20+ фраз)

Если в группе **больше 15-20 фраз**, сегментируй батчами, чтобы не потерять редкие модификаторы и дополнительные конструкции (например, «до 5000», «из кожи»):

1. Раздели фразы на батчи по **10-15 штук**
2. Для каждого батча выполни сегментацию (Шаг 4) и получи JSON слотов
3. Объедини слоты из всех батчей:
   - Добавь все уникальные варианты (дедупликация по нормализованной форме)
   - Убери подмножества (как в правилах Шага 4)
   - Проверь покрытие: все ли исходные фразы представлены хотя бы одним токеном в слотах?

Для автоматизации объединения используй `merge-slots`:

```bash
echo '<input_json>' | uv run --script scripts/missed_demand.py merge-slots
```

Формат входа (stdin JSON):
```json
{
  "phrases": ["купить телефон ретро", "заказать трубку винтажную", "..."],
  "batches": [
    {
      "phrase_indexes": [0, 1],
      "slots": {"objects": ["телефон"], "actions": ["купить", "заказать"], "modifiers": ["ретро"], "additional": []}
    },
    {
      "phrase_indexes": [2],
      "slots": {"objects": ["телефон", "трубка"], "actions": [], "modifiers": ["винтажный"], "additional": []}
    }
  ]
}
```

Выход включает:
- `slots_pre_trim` / `slots_post_trim` — слоты до и после обрезки по лимитам
- `query` — собранный OR-запрос
- `coverage` — отчёт: `uncovered_phrases`, `uncovered_tokens`, `additional_patterns`
- `debug` — какие батчи породили каждый вариант

Если в `coverage.uncovered_phrases` есть фразы — сегментация возможно их потеряла (coverage эвристический, без лемматизации возможны ложные срабатывания). Проверь и добавь пропущенные токены в слоты при необходимости.

### Шаг 4.2: Валидация сегментации с пользователем

**ОБЯЗАТЕЛЬНО** покажи результат сегментации пользователю перед запросом Wordstat:

```
Группа «Ретро-телефоны» (транзакционная):
  objects: телефон
  actions: купить, заказать
  modifiers: ретро, винтажный, старинный, телефонный
  additional: в москве

Всё верно? Что добавить/убрать?
```

Это экономит API-квоту и предотвращает ошибки сегментации (например, «аппарат» вместо «телефонный»).

### Шаг 5: Сборка текущего OR-запроса

```bash
uv run --script scripts/missed_demand.py build-query '<slots_json>'
```

### Шаг 6: Запрос текущего спроса (X)

Сформируй `full_phrase`:
1. Возьми `query` из build-query
2. Добавь `group_minus` из parse-xlsx
3. **Best-effort для campaign_minus**: если `len(query + group_minus + campaign_minus) <= 4096` — добавь campaign_minus; иначе — пропусти с дисклеймером

```bash
bash scripts/query_total.sh --phrase "<full_phrase>" --regions "<region_id>"
```

Если API вернёт ошибку с campaign_minus — повтори без него, добавь дисклеймер в отчёт.

### Шаг 7: LLM-расширение

**ВАЖНО: Сохраняй структуру запроса.** Если в оригинале нет actions (действий) — не добавляй их как обязательный слот. Добавление обязательного `(купить|заказать)` к запросу без actions **сужает** запрос, а не расширяет.

Правило: расширяй только те слоты, которые уже есть в оригинальном запросе. Новые слоты (например, actions для запроса без действий) предлагай как **отдельные дополнительные запросы**, не смешивая с основной OR-схемой.

Пример:
- Оригинал: `(муз центр|муз центры) ретро` — нет actions, нет additional
- Расширение основного: `(муз центр|музыкальный центр|музцентр|аудиоцентр) (ретро|винтажный|+в стиле ретро)` — расширяем objects + modifiers
- Дополнительный запрос (опционально): `(купить|заказать) (муз центр|музыкальный центр) ретро` — отдельно оценить транзакционный спрос

Предложи новые варианты для каждого слота **в зависимости от типа группы** (см. раздел «Типы запросов»):
- Синонимы (телефон -> трубка, телефонный аппарат)
- Написание по-разному (volkswagen -> фольксваген, фольцваген)
- Опечатки (тигуан -> тигуанн)
- Жаргон и разговорные формы (проигрыватель -> вертушка)
- Сокращения без пунктуации (муз центр, а не муз. центр)
- Новые действия (купить -> заказать, цена, стоимость) — **только если actions уже есть**
- Новые модификаторы (ретро -> антикварный, раритетный)

**Post-фильтр**: проверь предложенные термины на совпадение с `campaign_minus`. Совпадающие — исключи (они заминусованы не просто так).

### Шаг 8: Запрос расширенного спроса (Y)

Повтори шаги 5-6 с расширенными слотами -> получи Y.

### Шаг 8.1: Проверка мусора при большой дельте

**Если дельта > 200% И Y > 100**, запроси развёрнутый ответ Wordstat (topRequests) по расширенному OR-запросу. Посмотри фразы в выдаче:
- Есть ли нерелевантный мусор? (например, добавили «вертушка» и получили «вертушка рыболовная»)
- Если мусор найден — предложи изолировать с помощью `!` (принудительная словоформа) или дополнительных минус-слов
- Покажи пользователю топ-10 фраз из выдачи с комментарием

Пример:
```
Дельта большая (+478%). Проверяю, что стоит за расширенным запросом...

Топ-фразы в Wordstat:
  1. вертушка для пластинок купить — OK
  2. вертушка рыболовная — МУСОР (нужен минус: -рыболовная)
  3. граммофон ретро купить — OK
  ...

Рекомендация: добавить минус-слова: -рыболовная -рыбалка
```

### Шаг 9: Отчёт

Для каждой группы выведи:

```
Группа «Ретро-телефоны» (транзакционная):
Редактировать: https://direct.yandex.ru/dna/groups-edit?ulogin={login}&campaigns-ids={campaign_id}&groups-ids={group_id}

OR-схема ДО:
  (купить|цена) (телефон|телефоны|трубка) (ретро|старинный|винтаж) +в москве

OR-схема ПОСЛЕ:
  (купить|цена|заказать|стоимость) (телефон|телефоны|трубка|телефонный аппарат) (ретро|старинный|винтаж|винтажный|антикварный) +в москве

Новые термины:
  actions: +заказать, +стоимость
  objects: +телефонный аппарат
  modifiers: +винтажный, +антикварный

Спрос: 651 -> 679 (+4.3%)
[!] Без campaign_minus — реальный спрос может быть ниже
[!] Дельта < 10% — расширение минимальное, можно пропустить
```

Где `{login}` — логин Яндекс Директ (спросить в начале сессии), `{campaign_id}` — имя файла без `.xlsx`, `{group_id}` — ID группы из parse-xlsx.

Спроси пользователя:
- Принять расширение?
- Доработать (убрать/добавить термины)?
- Перейти к следующей группе?

### Шаг 10: Следующая группа

Повтори с шага 3 для другой группы.
В последовательном режиме — спроси перед каждой группой.
В параллельном режиме — обработай все группы и покажи сводный отчёт.

## Стоп-слова

Предлоги **на, в, к, за, с, по, из, от, до, для, без, при, под, над, между, через, об, перед** являются стоп-словами в Wordstat — они игнорируются без оператора `+`.

Скрипт `build-query` автоматически добавляет `+` к стоп-словам в слотах. Ставить `+` вручную не нужно.

## Ограничения

- **Лист «Тексты»**: скрипт работает только с XLSX-экспортом, где лист называется «Тексты». При другом названии — ошибка с перечислением доступных листов.
- **Campaign_minus**: кампанейные минус-фразы часто превышают 4096 символов и не помещаются в один Wordstat-запрос. В этом случае метрика X/Y учитывает только group_minus.
- **OR-операторы**: если Wordstat API неожиданно не поддерживает `(a|b)` синтаксис — fallback: запросить каждый вариант отдельно и суммировать (upper bound — пересечения дадут оверкаунт).
- **Квота API**: 10 запросов/сек, 1000 запросов/день. При анализе множества групп следить за расходом.
- **Пунктуация**: скрипт автоматически удаляет точки, запятые и прочие знаки из слотов (`муз. центр` -> `муз центр`). Это безопасно — Wordstat всё равно игнорирует пунктуацию.
