Google Keyword Intelligence

Сопоставимый спрос через якорь 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 должен присутствовать в наборе после нормализации.

Четыре оговорки

  1. Значения относительные и абсолютным объёмом поиска не являются.
  2. Шкала привязана к якорю: намного более слабый ключ упирается в разрешение шкалы.
  3. Пакеты сшиты через якорь; ошибка сшивки накапливается от пакета к пакету.
  4. Значения ниже порога покрытия не выпускаются числом; порог назван 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. В обоих есть «ипотека» и контрольный ключ «новостройки».

ПакетИндекс якоря «ипотека» в ответе«новостройки», % от якоря
A228.17680
B028.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