Содержание ↓
Зачем считать спрос кодом
Из всех сигналов о рынке поисковый спрос обходится дешевле всего: люди сами пишут, что им нужно, а Wordstat отдаёт это помесячно. Руками сбор выглядит так: вбил фразу в веб-интерфейс, посмотрел историю, скопировал в таблицу. На десятке фраз терпимо. Ядро категории — это сотни фраз, три региона и история за восемь лет, и ручной сбор перестаёт быть вариантом.
С чтением цифр не лучше. Частотности охотно складывают в «объём рынка» и сравнивают полные суммы месяцев, и оба приёма дают числа, которые расходятся с рынком. Раздел я строил вокруг того, чтобы таких чисел на экране не было.
Как устроен сбор
Фразы ядра я раскладываю по кластерам: общие запросы категории, уточнения по формату, запросы по конкретной специализации, брендовые и навигационные к другим игрокам. Строку ядра определяет id, текст ключом не служит. Скажем (пример условный), «ремонт айфона» и «ремонт айфонов» Wordstat кладёт почти в одну корзину, а в ядре это две строки со своей историей.
API Wordstat пускает 100 запросов в час, то есть один вызов раз в 36 секунд. Под этот потолок коллектор идёт партиями: сначала фразы без свежей динамики, потом остальные. Когда покрыто всё, прогон заканчивается без единого вызова. История за восемь лет по всему ядру догружается несколько суток.
Регионов три: Москва, Петербург и остальная Россия. Третью корзину Wordstat напрямую не отдаёт, поэтому код вычитает обе столицы из всей страны по каждой дате и не даёт результату уйти ниже нуля. Рядом с частотностью лежат слепки выдачи: дважды в неделю, в понедельник и субботу, раздел сохраняет HTML-слепок выдачи по фразам ядра и снимает из него, кто стоит в органике и в рекламе: рекламные блоки в этом слепке видны. Для остальной России выдачу снимаем в трёх региональных городах и усредняем их доли.
Веб-интерфейс Wordstat понимает кавычки и восклицательный знак для точной формы. API Wordstat в Yandex Cloud, через который работает раздел, на те же операторы отвечает ошибкой «Invalid query» — мы проверили это отдельным запросом. Через API доступна только частотность в широком соответствии, и от этого зависит всё, что показывает раздел.
Агрегаты считает Postgres. Раздел дважды укладывал рабочую базу, и оба раза из-за того, как читал данные: страница поднимала сырые строки, чтобы сложить их в Node. Та же агрегация одним SQL-запросом на сервере идёт 241 мс против 280–600 секунд старого прогрева. Витрину каждые шесть часов пересобирает ETL внутри самой базы. Если его сверка не сошлась, отметка обработанного не двигается, витрина честно отстаёт, а в лог уходит громкая строка.
Четыре ловушки в числах спроса
Сумма частотностей врёт об объёме
Слово в широком соответствии Wordstat считает в любом значении и вместе со всеми фразами, где оно встречается. Возьмём условную категорию: «стоматология» уже содержит «стоматология казань» и «детская стоматология», и сумма трёх строк посчитает часть спроса дважды. Порядок слов Wordstat не различает: «такси москва» и «москва такси» дают одно и то же число. Поэтому уровень категории одним числом я не показываю. На слайде стоит число фраз в ядре и объяснение метода, а тест-страж не даёт абсолюту вернуться.
Сравнивать можно только общий состав
Состав ядра меняется: фразы приходят и выпадают, а коллектор добирает месяц партиями. Разность полных сумм двух месяцев измерит как раз этот состав. У нас месяц однажды приехал недособранным, и плитка показала падение, хотя на общих для обоих месяцев фразах спрос вырос. Ошибся даже знак.
Теперь разность считается только по фразам, которые есть в обоих месяцах, а полнота меряется числом собранных фраз к прошлому месяцу. Если полнота ниже 0,9, месяц получает пометку «недособран», а полная сумма уходит в справку с подписью «состав ядра менялся». Длинный ряд код сшивает цепным индексом на сопоставимом ядре.
Последний месяц проваливается сам
Частоты текущего месяца оседают неделями, и свежая точка выглядит обвалом. Код отрезает хвост: точка считается неполной, если она ниже половины медианы шести предыдущих. Отрезается максимум две точки, чтобы настоящий сезонный спад не съел конец ряда. Ряд короче четырёх точек код не трогает вовсе. Тот же фикс убрал ложные падения на 90 с лишним процентов из списка фраз, которые сильнее всего просели за месяц.
Бренды меряются в разных границах
Брендовые запросы разных компаний нельзя складывать и делить друг на друга. Клинику с редким названием можно мерить по голому имени. Клинику с названием-словом, скажем условную «Улыбку», без уточнения «стоматология» мерить нельзя, в спрос попадут открытки и песни. Границы у двух брендов разные, и доля одного в их сумме систематически завышает того, у кого имя редкое. На графике каждый бренд идёт индексом к собственной базе, где первая точка окна равна 100, плюс изменение год к году. Базу индекса надо называть на слайде: база в сезонный пик всегда покажет спад, так что выбор базы и есть выбор вывода.
Модели здесь мало
Частотности, разности, индексы и доли в выдаче считает код — это правило у меня общее для всех разделов кабинета. Модель работает в двух местах. Она разбирает тексты рекламных объявлений из выдачи на тезисы и раскладывает их по категориям вроде «деньги», «условия», «локация». Каждое объявление уходит к ней в отгороженном блоке как недоверенный текст. Если в рекламу спрятали инструкцию для модели, она не сработает. Ещё модель пишет абзац про спрос в недельный отчёт, и в этом абзаце разрешены только посчитанные числа.
Что получает маркетолог
Маркетолог видит график спроса категории со знаком сезонности и разность к прошлому месяцу и к прошлому году на сопоставимой базе. Спрос раскладывается по сегментам до отдельной фразы и по трём регионам. Есть сводная таблица «запрос × регион», выгрузка в Excel и картина выдачи: кто стоит по фразам ядра в органике и в рекламе. Эти же числа уходят в недельный отчёт и в радар рынка.
Где мы спотыкались
Крон вызывал сбор с параметром limit=6. В коде это был размер пула кандидатов, и пул навсегда сжался до шести верхних фраз: 144 вызова Wordstat в сутки уходили на пересбор одних и тех же фраз, остальное ядро стояло. Лечение — гейт «всё покрыто»: если фраз без свежей динамики нет, прогон делает ноль вызовов.
Когда ядро пересобирали, удаление старых фраз каскадом унесло их историю динамики за восемь лет. Агент трижды предупредил и предложил безопасную альтернативу — деактивацию, и жёсткую замену я выбрал сам. Бэкап остался в базе. Замена ядра трогает историю, так что цену операции надо считать до запуска.
С витриной набралось ещё несколько правил. В ней нет средних, только числители и знаменатели, деление идёт на чтении: среднее средних неверно, когда у дней разное число замеров. Счётчики слепков зависят от частоты съёма: в месяце, где сбор шёл реже, записей в разы меньше, хотя доли не изменились. Счётчики как динамику рынка мы не показываем. И постраничный вызов функции через API базы выполнял её заново на каждую страницу: 21 страница превратила одну секунду в двадцать.
Что понадобится
Ядра на сотню фраз, API Wordstat и таблицы, где разность считается по общему составу, хватит, чтобы увидеть первую честную динамику. Трудное начнётся на слайде для руководителя. Какие числа из ядра можно показывать без оговорок, удобнее решать вдвоём на консультации. А что происходит с запросами вокруг ИИ в поиске, я разбирал в заметке «ИИ для SEO».
- Claude Code
- TypeScript
- Next.js 15
- Supabase (Postgres)
- API Wordstat
- Yandex Search API
- cron на сервере
