Files
portal/docs/superpowers/specs/2026-07-06-avtopodbor-render-cache-design.md
T
Дмитрий c83b599524 docs(autopodbor): дизайн кэша отрисованных карточек (5 дней) + хендофф
Решение владельца: общий Redis-кэш карточек 2ГИС/Яндекс на 5 дней, единый для шага 1
(поиск) и шага 2 (изучение) и повторных прогонов. Кэшируем ТОЛЬКО карточки (/firm/,
/maps/org/); поиск (/search/) и ленты Яндекса — всегда свежие (добывание новых фирм не
ограничиваем). Кэш ограничивает свежесть только у уже найденных карточек — редко меняются.

Замер на САПС (счётчик скачиваний): 12 повторных открытий / ~54с внутри одного сбора
(карточка Метрополиса тянулась 3× — ingest+поиск+разворот); повторный сбор в течение 5 дней
даёт ~200с+ и резкое падение нагрузки на 2ГИС/Яндекс/прокси.

План реализации — через TDD за флагом AUTOPODBOR_RENDER_CACHE (ВЫКЛ по умолчанию), TTL
AUTOPODBOR_RENDER_CACHE_TTL_DAYS=5. Плюс промт-хендофф для следующей сессии.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:07:47 +03:00

12 KiB
Raw Blame History

Автоподбор: кэш отрисованных карточек (2ГИС/Яндекс) — дизайн + хендофф

Дата: 2026-07-06 · Ветка: worktree-avtopodbor (НЕ прод) · Статус: решение принято, НЕ реализовано.


0. Где мы (контекст сессии 06.07)

Ветка worktree-avtopodbor (воркстри: .claude/worktrees/avtopodbor). Сегодня закоммичено (0131ecfc, НЕ запушено):

  1. Качество шага 2 (якорь по данным клиента). Привязанные клиентом карточки 2ГИС/Яндекс используются как «земля» и якорь холдинга; слепой поиск по имени берётся только при наличии якоря; AnchorGate пустой якорь → drop. Чужие фирмы по широкому имени («Притяжение» → стоматология, салоны) больше НЕ попадают в карточку. Доказано на САПС (comp 743): свежий сбор — 0 салонов. Спека: docs/superpowers/specs/2026-07-06-avtopodbor-site-anchor-search-fix.md.
  2. Скорость: параллельный разворот за флагом AUTOPODBOR_PHONE_REVERSE_PARALLEL (сейчас ВКЛ в dev .env). 2ГИС- и Яндекс-разворот идут разом (max, не сумма). Сверка: данные один в один, потерь нет. Таймаут подпроцесса 600с (драйвер process иначе берёт дефолт 60с и убивает разворот). ParallelPhoneReverse + CompositePhoneReverse (последовательный, для отката).

Флаги в dev .env: deep_study, self_render, phone_reverse, phone_reverse_yandex, phone_reverse_parallel — все ВКЛ. Тесты Autopodbor: 464/464. Виртуалка рендера: 51.250.1.97 / https://51-250-1-97.sslip.io/render (секреты — в gitignored .env, В ГИТ НЕ ВЫНОСИТЬ).

🔴 Правило прокси (доказано): несколько ПАРАЛЛЕЛЬНЫХ лент Яндекса через один прокси теряют данные (44→30). 2ГИС-initialState пачкой — безопасно. Виртуалка держит глобальный потолок MAX_CONCURRENCY (8). Яндекс-разворот НЕ параллелить внутри без 2-го прокси-IP.


1. Решение владельца: кэш отрисованных карточек

Задача: снизить нагрузку на 2ГИС/Яндекс/прокси и ускорить, кэшируя отрисованные карточки.

  • TTL — до 5 дней (настраиваемый). Обоснование владельца: поле и карточки конкурентов меняются редко («раз в квартал кто-то что-то поменяет»), поэтому 5 дней — с запасом, свежесть не страдает.
  • Общий кэш для шага 1 (поиск) и шага 2 (изучение) и для повторных прогонов. Карточка, отрисованная на шаге 1, на шаге 2 берётся из кэша. Ключ — по URL, хранилище Redis (общий между процессами/прогонами).
  • Кэш ограничивает свежесть ТОЛЬКО у «уже найденного» (контент карточек), НЕ у поиска новых. Это ключевое уточнение владельца.

Что кэшируем, что нет

Запрос Кэш? Почему
Карточка 2ГИС …/firm/{id} 5 дней «уже найденное», телефоны/сайт стабильны
Карточка Яндекс …/maps/org/…/{id} 5 дней то же
Поиск 2ГИС …/search/… всегда свежий ДОБЫВАНИЕ новых фирм — кэш убил бы находки
Лента Яндекса (yandex-list) всегда свежий добывание; идёт отдельным путём (RenderServiceYandexListSource)
Сайт конкурента (CurlPlaywrightFetcher) ⏸ follow-up отдельный интерфейс Fetcher, не BatchPageFetcher; можно добавить потом

Итог для повторного сбора (в течение 5 дней): поиск идёт ЗАНОВО → новые фирмы находятся; известные карточки — из кэша, мгновенно, без обращений к 2ГИС/Яндекс. Ограничение свежести — только у контента уже найденных карточек. Явная «Собрать ещё раз» тоже из кэша (вариант B) — свежесть добывания обеспечивает свежий поиск, а не сброс кэша.


2. Доказательство экономии (замер 06.07, САПС, счётчик скачиваний)

Прогон САПС (3 элемента, последовательно, 670с), обёртка-счётчик над фетчером:

Всего скачиваний карточек/страниц: 57
Уникальных URL:                    45
ПОВТОРНЫХ открытий:                12   (~54с впустую)

Топ повторов (подтверждает код):

  • Карточка Метрополиса /firm/700000010450677583× (ingest + поиск + разворот).
  • Яндекс-карточки Метрополиса/Новой панорамы/Слободы/Притяжения — по 2-3×.
  • (Поиск «Слобода Весны» гонялся 4× — это цикл-ретрай пустого поиска в collect2gis, НЕ кэш-цель.)

Экономия:

  • Внутри одного сбора: ~45-54с (повторы карточек /firm/, /maps/org/).
  • Повторный сбор / шаг 2 после шага 1 в течение 5 дней: почти все ~45 карточек из кэша → ~200с+ и резкое падение обращений к внешним сервисам. Здесь главный выигрыш.

Причина повторов (по коду): в DeepStudyCollector::collect() фазы ingestClientCards / collect2gis / collectYandex / разворот тянут независимо; SelfRenderClient дедупит только внутри одного htmlBatch-вызова, между фазами — нет. Плюс шаг 1 и шаг 2 не делят кэш вообще.


3. План реализации (через TDD, за флагом)

  1. CachingPageFetcher (новый, app/app/Services/Autopodbor/Agent/Fetch/) — декоратор implements BatchPageFetcher, оборачивает реальный фетчер (SelfRenderClient ИЛИ XfetchClient):
    • cacheable($url) = str_contains($url,'/firm/') || str_contains($url,'/maps/org/'); НЕ /search/.
    • html($url): если cacheable → Cache::get(key); хит вернуть; промах → inner->html, если непусто Cache::put(key, $v, ttl). Не-cacheable → passthrough. Пустое НЕ кэшируем.
    • htmlBatch($urls): cacheable-URL проверить в кэше по одному; промахи добрать inner->htmlBatch; непустое положить; не-cacheable — passthrough. Вернуть map url→content.
    • Ключ: 'autopodbor:render:'.sha1($url). Хранилище — дефолтный Cache (Redis на dev/prod).
    • TTL: config('autopodbor.render_cache_ttl_days', 5) дней.
  2. Флаг: config/autopodbor.php'render_cache' => env('AUTOPODBOR_RENDER_CACHE', false) + 'render_cache_ttl_days' => env('AUTOPODBOR_RENDER_CACHE_TTL_DAYS', 5). По умолчанию ВЫКЛ.
  3. Вайринг в AutopodborServiceProvider: когда render_cache ВКЛ — обернуть фетчер CachingPageFetcher-ом ОДИН раз и использовать в ОБОИХ местах:
    • шаг 1 buildLiveFind: $catPages / $enrichPages (сейчас $sr или LivePageFetcher/RoutingBatchFetcher);
    • шаг 2: $pages (deep collector), разворот buildPhoneReverse ($pages + яндекс card reader). Так как ключ по URL в Redis — даже разные экземпляры декоратора делят один кэш. Проще всего обернуть $sr/$xfetch в самом верху register() и дальше передавать обёрнутый. ⚠️ RoutingBatchFetcher/яндекс-Playwright и RenderServiceYandexListSource (ленты) — НЕ кэшировать (это /search/-путь и ленты). Оборачивать только загрузчик КАРТОЧЕК.
  4. Тесты (TDD): CachingPageFetcher:
    • cacheable карточка: 2-й вызов — из кэша (inner дёрнут 1 раз);
    • /search/: всегда passthrough (inner каждый раз);
    • пустой ответ: НЕ кэшируется (inner дёрнут снова);
    • htmlBatch: часть из кэша, часть добирается. Использовать Cache::store('array') или Cache::spy() + фейковый inner-фетчер (спай вызовов).
  5. Регресс: весь Autopodbor-набор зелёный. Pint. Не коммитить диагностику/.env/чужое.

4. Ограничения / грабли

  • Пустое не кэшировать — иначе разовый сбой отравит кэш на 5 дней.
  • Кэшировать только карточки (/firm/, /maps/org/), НЕ поиск — иначе новые фирмы не найдутся.
  • Ключ строго по нормализованному URL (как приходит в фетчер). 2ГИС карточка → initialState-JSON, Яндекс карточка → HTML; тип неявно определяется URL, отдельный ключ по типу не нужен.
  • Redis на dev включён (проект использует Redis 7). Проверить config('cache.default').
  • Флаг ВЫКЛ по умолчанию; откат — флаг в false.

5. Окружение (для следующей сессии)

  • Воркстри: c:/моя/проекты/портал crm/Документация/.claude/worktrees/avtopodbor (ветка worktree-avtopodbor). Правки — ТОЛЬКО тут.
  • PHP: C:/tools/php83/php.exe. Тесты: php artisan test tests/Unit/Autopodbor tests/Feature/Autopodbor (последовательно).
  • Дев-БД liderra (postgres@127.0.0.1:5432). Тесты — liderra_testing. НЕ прод.
  • Study-воркер запущен: queue:work --queue=autopodbor-study (перезапускать после правки кода — держит бут в памяти).
  • Коммит только по «эскейп»/«коммит», по явным путям. Диагностика (scripts/*, _poll35.php), .env, чужие файлы (XfetchClient*, 2026-07-05-...session-status.md) — НЕ коммитить.
  • Прод liderra.ru — не трогать без разрешения владельца.