Appearance
Премортем: ADR-016 geo suggest engine
Контекст
- Предмет: ADR-016 — архитектурное решение для /store/geo/suggest. Meilisearch v1.10 primary + DaData fallback decorator. PHP Slim 4 backend. MVP single-node. Pipeline: SQL suggest_normalize → expand geo-synonyms.yaml → Meilisearch search → fallback при 0 hits. Index sync 2×/неделю.
- Аудитория: backend dev (DEV-386 имплементация), DevOps (Meilisearch в prod), архитектор (review/Accept), конечные пользователи маркетплейса автозапчастей СПб (продавцы в форме регистрации, покупатели в каталог-фильтре)
- Успех: 50/50 gold-set hit-rate top-3 + P95 ≤ 200ms @ 100 RPS (DEV-428 SLO)
- Reference class: ADR-009 — Meilisearch для product search (тот же проект, Phase 2, не реализован), DEV-430 spike — benchmark pg_trgm vs Meilisearch на 4.45M ГАР записей
Дыры
H-001: DaData fallback нелегитимен — центральный механизм цели мертворождён
- Угол зрения: Клиент, Исполнитель, Допущения
- Найдена: 2026-06-03 (запуск 1)
- Важность: высокая
- Уверенность: подкреплено контекстом
- Статус: ПРИНЯТО
- Режим: полный
- Описание: ADR-016 закладывает DaDataSuggestFallback decorator как механизм закрытия gap 38/50 → 50/50 (heavy typos, mix-script, metro, district queries). Проектная политика — free/OSS only, DaData нелегитимен. Без fallback hit rate остаётся 38/50 by design, UX страдает в форме регистрации и каталог-фильтре, деплой блокируется на ревью политики. Помощники H1-Клиент / H2-Исполнитель / H3-Допущения нашли одну дыру с трёх углов — root cause общая.
- Решение: Г: закрыть gap через city rows (DEV-379) + metro_stations (DEV-427) + mix-script normalization (DEV-386) — внутри Meilisearch без fallback. 2 heavy-typo queries вынести в явно документированное 'known limitation' с acceptance.
- Зачем выбрали: Сохраняем политику (без DaData, без Nominatim как нового сервиса), достигаем 48/50 — почти DoD, оставшиеся 2 = осознанный compromise. Закрывает критический root issue не вводя новые риски.
- Первые шаги (prevent):
- Переписать ADR-016 §«Эскалация» и §«Действия»: убрать DaDataSuggestFallback decorator. Заменить раздел: gap closure через scope expansion в DEV-379/386/427
- Пересмотреть DoD DEV-430: 48/50 как MVP gate с явным списком 2 known limitations (heavy typos в коротких словах ≥2 edit distance в 6-char)
- Обновить cost model: убрать DaData строку (1-2K ₽/мес), убрать дозу alert на dadata_fallback_count метрику
- Исполнитель: оба
- Сигнал успеха (detect): На k6 прогоне DEV-428 + повторе gold-set после DEV-379+386+427: hit rate ≥ 48/50 без обращений к external services
- Стоп-условие: Если после DEV-379+386+427 hit rate < 45/50 → пересмотреть либо DoD (down к 45/50 для MVP), либо ввести OSS-альтернативу (Nominatim self-hosted) как отдельный ADR
- Если уже случилось (limit damage): Если случилось в прод — добавить в FAQ маркетплейса 'почему не находит мой адрес' с инструкцией для пользователя (city + улица как 2 поля вместо одного). Параллельно расширить geo-synonyms.yaml weekly review топ-100 пустых запросов
- Открытые вопросы:
- Принимает ли продакт DoD 48/50 или требует 50/50 absolute
- Что если архитектор настаивает на DaData (политика непонятна или ослаблена)
- История статуса:
- 2026-06-03: ПРИНЯТО (запуск 1) — Принято решение Г: закрыть gap через scope expansion без external fallback
H-002: Конкурентный gap — metro/districts/stale sync vs Avito/Cian/Yandex
- Угол зрения: Конкурент
- Найдена: 2026-06-03 (запуск 1)
- Важность: высокая
- Уверенность: требует проверки
- Статус: ПРИНЯТО
- Режим: полный
- Описание: ADR-016 на старте не включает metro_stations (DEV-427) и district rows (DEV-379), sync 2×/неделю даёт stale до 3.5 суток. Avito/Cian/Yandex.Маркет закрывают метро, районы и новые ЖК 'из коробки'. Для маркетплейса автозапчастей в СПб метро критично как ориентир — без него suggest выглядит обрезанно vs прямые конкуренты.
- Решение: Г: daily sync (вместо 2×/неделю) + включить metro_stations и districts в MVP scope перед prod-лончем
- Зачем выбрали: Без метро/районов СПб-маркетплейс выглядит обрезанно vs конкурентов на ключевом UX-элементе. Daily sync дёшев — Meilisearch indexing 50 секунд на 4.45M проверено на спайке. Совместимо с H-001 решением (там же используем DEV-379/427)
- Первые шаги (prevent):
- DEV-379 ETL: добавить city-level + district-level rows в addresses (уже было в roadmap, поднять приоритет в blocking для launch)
- DEV-427 metro_stations: индексировать в Meilisearch отдельным index 'metro' или совместить в 'addresses' с level='metro' (решить в DEV-386)
- Cron daily sync вместо 2×/неделю — корректировка sync hook strategy в DEV-386
- Исполнитель: оба
- Сигнал успеха (detect): Сравнительный gold-set v1.2 с топ-50 СПб запросов содержащих метро/районы — hit rate ≥ 80%
- Стоп-условие: Если ГАР API rate-limit или Meilisearch indexing > 5 минут на полном корпусе — откатить на 2×/неделю + on-demand для критичных update events
- Если уже случилось (limit damage): Если в проде заметили потерю запросов — hot patch: добавить топ-50 СПб улиц/метро вручную в geo-synonyms.yaml как stop-gap до full DEV-379
- Открытые вопросы:
- metro_stations индексировать в один Meilisearch index или отдельный — решение в DEV-386
- Срок: можно ли DEV-379+427 параллелить с DEV-386 без блокировки launch
- История статуса:
- 2026-06-03: ПРИНЯТО (запуск 1) — Принято Г: daily sync + metro+districts в MVP перед prod
H-003: Synonyms YAML без versioning + observability — дебаг suggest невозможен через 6 мес
- Угол зрения: Будущий поддерживающий
- Найдена: 2026-06-03 (запуск 1)
- Важность: высокая
- Уверенность: требует проверки
- Статус: ПРИНЯТО
- Режим: краткий
- Описание: Pipeline suggest_normalize → expand YAML → Meilisearch имеет 57 пар × ожидаемый ×3-×5 рост на 89 регионах. В ADR-016 нет: версионирования словаря, логирования какая пара сработала, связки query→expanded_terms→matched_doc_id в traces. Через 6-12 мес maintainer на тикет 'почему по X вернулся Y' не имеет данных для дебага. WYSIATI: нет голоса support-инженера в комнате.
- Решение: А: минимальный структурный лог {query, normalized, expanded_terms[], hit_id, score, synonyms_version} на каждый suggest. + version hash YAML включить в начало лога pipeline + sample 10% запросов в полный trace.
- Почему: Минимум достаточный для дебага без overkill. Версия YAML обязательна — позволяет понять какой словарь был активен. Sampling 10% — баланс между volume и observability.
- Первый шаг: Добавить в DEV-386 PHP middleware: structured log на каждый /store/geo/suggest call + version hash YAML вычислять при load (lru cache, не каждый call)
- Исполнитель: агент
- История статуса:
- 2026-06-03: ПРИНЯТО (запуск 1) — Принято А: минимум структурный лог
H-004: Публичный endpoint без rate limiting — scraping геобазы + DoS на single-node
- Угол зрения: Противник
- Найдена: 2026-06-03 (запуск 1)
- Важность: высокая
- Уверенность: подкреплено контекстом
- Статус: ПРИНЯТО
- Режим: полный
- Описание: /store/geo/suggest публичный (GET, без CSRF/session), возвращает address_id для resolve, rate limiting не упомянут в ADR-016. Перебором префиксов вытягивается полный геосправочник + mapping query→address_id за часы (4.45M MVP → 12M Year 1). Параллельно single-node Meilisearch (SPOF) не тестировался под concurrent load — спайк прогонял single-threaded 50 queries.
- Решение: Г: rate limit per IP (~30 req/min sliding window через Nginx или middleware) + добавить к DEV-428 k6 нагрузочный тест на 100 RPS concurrent на single-node Meilisearch.
- Зачем выбрали: Минимум для prod — A решает scraping/DoS базово. K6 проверяет что P95 ≤ 200ms holds под concurrent load (не только single-threaded gold-set). Без проверки Plan B/В preferred позже когда вырастем.
- Первые шаги (prevent):
- Nginx location /store/geo/suggest: limit_req zone=suggest burst=10 nodelay; (30/min per IP)
- DEV-428 k6 scenario: 100 concurrent users × 60 секунд на /store/geo/suggest со случайными query из gold-set + случайных префиксов 1-3 символа. Verify P95 ≤ 200ms
- Метрика geo.suggest.rate_limited_count + alert при росте > 5% от total traffic (сигнал scraping attempt)
- Исполнитель: оба
- Сигнал успеха (detect): K6 проходит SLO (P95 ≤ 200ms на 100 RPS) на single-node Meilisearch. Production alert: rate_limited_count > 5%
- Стоп-условие: Если k6 показал P95 > 200ms на 100 RPS — пересмотреть Meilisearch sizing (CPU/RAM single-node) или перейти к 3-node replica в раннем roadmap
- Если уже случилось (limit damage): Если scraping случился — анализ access log, временный block source ASN/IP range. Долгосрочно: soft auth (anonymous session cookie, Plan Б) + pre-aggregated cache на топ-prefixes (Plan В)
- Открытые вопросы:
- Where to apply rate limit: Nginx (DevOps) или PHP middleware (backend) — решит archi review
- Метрика scraping detection — какой threshold rate_limited_count считать аномальным
- История статуса:
- 2026-06-03: ПРИНЯТО (запуск 1) — Принято Г: rate limit + k6 load test в DEV-428
Топ 1–3 — с чего начать
- H-001 — DaData fallback нелегитимен — центральный механизм цели мертворождён. Первый шаг: Переписать ADR-016 §«Эскалация» и §«Действия»: убрать DaDataSuggestFallback decorator. Заменить раздел: gap closure через scope expansion в DEV-379/386/427
- H-002 — Конкурентный gap — metro/districts/stale sync vs Avito/Cian/Yandex. Первый шаг: DEV-379 ETL: добавить city-level + district-level rows в addresses (уже было в roadmap, поднять приоритет в blocking для launch)
- H-004 — Публичный endpoint без rate limiting — scraping геобазы + DoS на single-node. Первый шаг: Nginx location /store/geo/suggest: limit_req zone=suggest burst=10 nodelay; (30/min per IP)
Bias-проверка топа
- Главный риск искажения: availability bias — пользователь только что озвучил 'DaData нельзя', что подняло H-001 в топ; могла быть переоценена критичность относительно scaling на 12M записей
- Коррекция: явно проверить с продактом: достаточно ли 48/50 для MVP gate (downward DoD adjustment), или 50/50 absolute — тогда сценарий H-001 решения Г требует пересмотра
Reverse премортем (если запущен)
Заполняется только если финальная рекомендация — отложить/отказаться.
«Прошёл горизонт. Не сделали. Оказалось зря — почему?»
- Если ADR-016 не принят и spike-DEV-430 + DEV-386/379/427 заморожены: маркетплейс не получает adress autocomplete вообще, продавцы вводят адрес text-only и допускают опечатки → невалидные адреса в базе, не работают city-фильтры. Через 3 месяца churn на форме регистрации продавцов 30-50%.
- Если откладываем pending новый ADR (DaData replacement / Nominatim): идём в прод с pg_trgm (33/50, P95 10s) или вовсе без suggest. Latency P95 10s ломает каталог-фильтр на каждой странице, bounce rate растёт, маркетплейс становится unusable.
- Если abort и переключаемся на DaData primary вопреки политике: 7-10K ₽/мес на 100K req/day становится 30-50K при росте до 500K/day Year 2; vendor lock-in делает migration позже невозможной без переделки контракта.
Что перевешивает: Идти вперёд с принятыми Г-решениями: 48/50 — приемлемый MVP gate, scope DEV-379+427 в blocking path для launch оправдан конкурентным gap (H-002) и решает H-001 одновременно.