Сопоставимый спрос через якорь Google Trends
gkai demand и MCP-инструмент rank_keyword_demand ранжируют от 2 до 50
уникальных непустых ключей на общей относительной шкале. Креды Google Ads не нужны.
Этот же расчёт доступен в трёх сценариях через gkai research … --demand и
параметр MCP research_keywords(demand=True): см.
интеграцию в исследование.
В исследовании niche сид (запрос пользователя) исключается из ранжирования
и не может стать якорем: широкий запрос сжимает длинный хвост до измеренного
нуля. В competitor и site такого сида нет: верхний ключ по Ads или по
показам GSC остаётся кандидатом. Первый пакет собирается вслепую; после ответа
якорем становится ключ с наибольшим измеренным средним среди участников этого
пакета, список пакета переупорядочивается, остальные пакеты идут уже с
выбранным якорем. Явный --demand-anchor главнее автоматического выбора,
включая случай, когда у заданного якоря среднее меньше. Число ключей
ограничено оставшимся бюджетом пакетов. Это правило отбора относится к
research; отдельная команда demand сохраняет явный набор пользователя.
gkai demand "ипотека" "купить квартиру" "снять квартиру" "новостройки" \
"квартиры в москве" "жк москва" --anchor "ипотека" \
--language ru --country RU --timeframe "today 12-m" --format json
Метод
Один ответ Google Trends нормирован на 0–100 внутри своего запроса. Складывать или сравнивать значения из разных запросов напрямую нельзя. Здесь один общий якорь входит первым в каждый пакет вместе с максимум четырьмя участниками. Всего получается до 13 последовательных запросов сравнения для 50 ключей. Используются существующие кеш, троттлинг и circuit breaker провайдера Trends.
Для каждого ключа берётся среднее только по измеренным целым неделям:
is_partial=true исключается, has_data=false исключается отдельно для
каждого ключа. Позиция ключа определяется его именем в TrendsResult.keywords.
Далее для каждого участника:
relative_demand = measured_mean(keyword) / measured_mean(anchor in this batch) * 100
Якорь равен ровно 100.0, участник может быть выше 100. Якорь появляется в
результате один раз: с покрытием первого пригодного пакета; если ни один пакет
не пригоден — с null и причиной первого пакета. Участники сохраняют номер
своего пакета (с 1). Строки отсортированы по убыванию числа, затем идут null
в порядке входа. При равных числах порядок тоже сохраняется.
normalize_keyword нормализует регистр, Unicode и пробелы. Дубликаты удаляются
с сохранением порядка первого появления, пустые строки исключаются. Лимит
2–50 проверяется после этого. По умолчанию якорь — первый переданный ключ;
явный --anchor должен присутствовать в наборе после нормализации.
Четыре оговорки
- Значения относительные и абсолютным объёмом поиска не являются.
- Шкала привязана к якорю: намного более слабый ключ упирается в разрешение шкалы.
- Пакеты сшиты через якорь; ошибка сшивки накапливается от пакета к пакету.
- Значения ниже порога покрытия не выпускаются числом; порог назван
demand_min_coverage.
Эти четыре оговорки всегда возвращаются в data.caveats.
Не выбирайте слишком широкий якорь для узкой ниши. В живом прогоне
постановщика 06.09.2026 рядом с «пхукет» ключи «аренда байка пхукет»,
«аренда авто пхукет», «отели пхукет» и «виза таиланд» получили 53 недели
value=0, has_data=false. Это ниже разрешения якоря, а не нулевой спрос.
Для такой строки команда возвращает relative_demand: null и объяснение.
Измеренный ноль (has_data=true) остаётся числом 0.0.
Проверка сшивки на живых ответах
Два ответа, полученных постановщиком 06.09.2026, сохранены без изменений в
tests/fixtures/demand/batch_a.json и batch_b.json. В обоих есть
«ипотека» и контрольный ключ «новостройки».
| Пакет | Индекс якоря «ипотека» в ответе | «новостройки», % от якоря |
|---|---|---|
| A | 2 | 28.17680 |
| B | 0 | 28.12500 |
Относительное расхождение — 0.18382%: две независимые нормировки Google сходятся после пересчёта через якорь. Тест проверяет расхождение менее 0.5%, а также сами значения. Это наблюдение на двух пакетах, а не гарантия точности для любого набора. На тонких нишах (включая ru-запросы в гео TH) может быть всего 0–2 измеренных недели из 53; покрытие видно в каждой строке.
Порог покрытия
Доля измеренных целых недель measured_weeks / weeks должна быть не ниже
demand_min_coverage (переменная GKAI_DEMAND_MIN_COVERAGE, по умолчанию
0.25). Равенство порогу проходит. При weeks == 0 доля считается нулевой,
деления на ноль нет. 0.0 — законное значение и выключает порог.
Порог применяется только к строке, у которой иначе было бы число. Уже пустую строку (ниже разрешения якоря, обвал якоря, упавший пакет) он не трогает и её причину не перезаписывает. Якорь порогом не отсекается: без него пакет не встаёт на общую шкалу, и этот случай уже описан обвалом якоря.
Отсечённая строка получает relative_demand: null, status: low_coverage и
причину, которая называет сколько недель измерено из скольких и какой порог
не пройден. Это недостаточное покрытие, а не низкий спрос. Низкий спрос — по
прежнему число, в том числе 0.0 у измеренного нуля.
Живой прогон постановщика 06.09.2026:
gkai demand "пхукет" "аренда байка пхукет" "аренда скутера пхукет" \
"аренда виллы пхукет" --anchor "пхукет" --language ru --country TH
вернул «аренда виллы пхукет» = 1.91 при measured_weeks = 1 из 53 рядом с
якорем, у которого 53 из 53. Два других ключа уже ехали null из-за нуля
измеренных недель. Без порога одна неделя выглядела в столбце так же уверенно,
как пятьдесят три.
Поля и неполные ответы
data содержит provider, anchor, language, country, timeframe,
min_coverage, rows, batches_requested, batches_failed, caveats,
notices. min_coverage — порог, по которому отсекали. Каждая строка
содержит keyword, relative_demand, status, is_anchor, batch,
measured_weeks, weeks, reason. weeks — число целых недель в полученном
ряду; measured_weeks — сколько из них измерено для этого ключа. Для полностью
неполученного пакета оба счётчика равны 0.
Ветвиться по отсутствующему числу нужно по status, а не по тексту reason:
status | Что значит | relative_demand |
|---|---|---|
measured | число получено и покрытие достаточно | число |
low_coverage | измеренных недель меньше порога | null |
below_resolution | у ключа ноль измеренных недель рядом с якорем | null |
anchor_collapsed | якорь пакета не встал на шкалу | null |
batch_failed | пакет не получен, ошибка провайдера | null |
При нулевом покрытии выигрывает below_resolution: смысл точнее, и порог не
перезаписывает уже пустую строку.
reason остаётся человеческой прозой. Три причины, которые существовали до
порога, те же:
- Ни одной измеренной недели у участника: ниже разрешения якоря, не ноль спроса.
- У якоря нет измеренных недель или его среднее равно нулю: обвал якоря, весь пакет нельзя поставить на общую шкалу.
- Ошибка пакета: причина передаётся из слоя провайдера дословно. Обход остальных
пакетов продолжается. Ошибка виджета
TIMESERIESтоже сохраняет свою причину.
complete означает числа у всех ключей без упавших пакетов; partial — есть
null или упавший пакет; empty — все пакеты упали или ни у одного ключа нет
числа. Отказ из-за количества ключей или отсутствия явного якоря возвращается
как empty с data: null, без обращения к Google.
Сообщения провайдера про GEO_MAP и RELATED_QUERIES лежат в data.notices,
отдельно от настоящих warnings конверта, и не понижают полноту: команда читает
только временной ряд. CLI печатает их на stderr после конверта, включая его
предупреждения. JSON на stdout остаётся единым валидным документом.
CLI поддерживает --anchor, --language, --country, --timeframe (по
умолчанию today 12-m), --format json|table. Код выхода 0 соответствует
complete, 1 — partial или empty. Формат таблицы — общий формат полей
конверта, принятый в CLI. MCP возвращает тот же конверт без дополнительного
ключа result.
Чистый модуль google_keyword_ai.demand не обращается к сети или настройкам.
plan_batches возвращает списки ключей; combine принимает DemandBatch
(план с якорем первым, result или reason) и необязательный min_coverage
(по умолчанию 0.0 — порог выключен). Настройку читает оркестровка, не сам
модуль. Все входные пакеты должны иметь один якорь, рынок и период.
Повторяющиеся участники между пакетами сохраняются для проверки сшивки;
обычный run_demand дедуплицирует их до запросов.
Edit on GitHub · 2026-09-07