18 KiB
type, work_item_id, adr_id, title, status, created_at, authors, supersedes, superseded_by, labels
| type | work_item_id | adr_id | title | status | created_at | authors | supersedes | superseded_by | labels | ||
|---|---|---|---|---|---|---|---|---|---|---|---|
| adr | ET-008 | ADR-005 | ADR-005: Хранение публичных GPS-треков — отдельная БД data/gps_tracks.sqlite, SQLite+Spatialite, общая схема для всех источников, sources как JSON-массив | accepted | 2026-06-01 |
|
|
ADR-005 — Схема хранения публичных GPS-треков
Статус
Accepted
Контекст
ET-008 вводит новый класс данных в проект — публичные GPS-треки, агрегированные офлайн-pipeline'ом из ≥ 3 внешних источников по региону MVP (ЦФО + Чувашия). По BRD §3 целевой объём — ≥ 5000 треков, по BRD §6 предел — несколько ГБ на регион при дальнейшем расширении. По BRD §1 модель данных не пересекается с существующими сущностями:
- vector-tile слой
trails(data/centralfederal.sqlite) — OSM-дороги/тропы, отдельный формат, отдельный pipeline (osm2pgsql-like); - личные GPX-треки (ET-006) — живут только в памяти браузера (
window.gpxTracks), на сервере не хранятся; - POI и маршруты (PH-1/2) — другие сущности
centralfederal.sqlite.
Архитектурно нужно решить:
- Где хранить — в существующей
centralfederal.sqliteили отдельным файлом. - Как организовать схему — одна таблица на все источники или партиционирование по источнику.
- Как хранить мульти-источник (трек найден в N платформах после дедупа) — нормализованная таблица
track_sourcesили JSON-массив в основной таблице. - Какие индексы дают приемлемый p95 ≤ 300 мс на bbox-запрос с фильтрами.
- Совместимость с MVT-generation pipeline'ом, уже существующим в
src/api/main.pyдля/api/tiles/{z}/{x}/{y}.mvt.
Рассмотренные варианты
Вариант D (Database) — где хранить
-
D-A — отдельный файл
data/gps_tracks.sqlite(выбран, совпадает с BRD §7 и TRZ §8 ADR-001-recommendation). Плюсы:- Pipeline пишет в свою БД — нет блокировок write на
centralfederal.sqlite, который активно читается API под нагрузкой раздачи MVT. - Независимый цикл бэкапа (см.
07-infra-requirements.md§4):gps_tracks.sqliteбэкапится ежедневно,centralfederal.sqlite— после редкой ребилд-сессии OSM-данных. - Независимая ротация: ретеншн 5 лет (REQ-NF-03) применяется только к одной БД;
centralfederal.sqliteпересобирается из OSM по своему графику. - Изоляция риска при ошибке pipeline — нельзя случайно повредить OSM-данные.
- В будущем (BRD §6 риск роста до миллионов треков) переход на PostGIS затрагивает один файл, а не корневую БД. Минусы:
- Второй коннект из FastAPI (мелкая сложность, ~10 строк в
main.py). - При совместных запросах «дороги OSM × публичные треки рядом» (PH-3 Smart Route) — кросс-БД JOIN неэффективен. Принято: на горизонте MVP таких запросов нет; в PH-3 решается отдельным ADR (вариант:
ATTACH DATABASEили денормализация в материализованную таблицу).
- Pipeline пишет в свою БД — нет блокировок write на
-
D-B — в существующую
centralfederal.sqlite, отдельные таблицыgps_tracks_*. Отклонён:- Pipeline writer и MVT reader конкурируют за один файл; SQLite WAL смягчает, но не устраняет.
- Backup-цикл становится зависимым: невозможно ребилдить OSM-данные не «остановив» pipeline.
- Сценарий «удалить весь gps-датасет и пересобрать» (R-3 ниже) требует
DROP TABLEв большой production-БД; в отдельном файле —rm data/gps_tracks.sqlite && python scripts/gps_collect.py.
-
D-C — PostGIS. Отклонён:
- BRD §1 «SQLite по умолчанию, PostgreSQL когда нужно». ≥ 5000 треков для ЦФО легко влезают в SQLite (оценочно ≤ 500 МБ при средней геометрии 1240 точек × 16 байт). Spatialite даёт BLOB+R-tree, чего хватает для всех запросов TRZ.
- Введение PostgreSQL — новый класс инфры (контейнер + бэкап + миграции через alembic). Это
arch:major-changeуровня всего проекта; ET-008 такого не требует.
Вариант T (Table layout) — одна или несколько таблиц
- T-A — единая таблица
tracks(выбран). Поля per-источник денормализованы в JSON-колонки. Все источники приводятся к общему контракту вmodels.py::Track(TRZ §7). Плюсы:- Самый простой bbox-запрос: один SELECT с одним bbox-фильтром.
- Дедупликация на уровне БД через UNIQUE-индекс по
dedup_key(TRZ REQ-F-08). - MVT-генерация на низком зуме — одно сканирование R-tree → одна
LineString → MVTпетля.
- T-B — таблица на источник + view
tracks_all UNION ALL .... Отклонён:- Дедупликация между источниками превращается в кросс-таблицу процедуру.
- Изменение списка источников требует DDL-миграции, что блокирует «расширяемость на новый регион ≤ 30 строк YAML без правки кода» (BRD-метрика).
Вариант S (Sources field) — как хранить N источников у одного трека
- S-A — JSON-массив в колонках
sources_json,external_urls_json(выбран, совпадает с TRZ REQ-F-09). Плюсы:- Запись/чтение трека — атомарная операция.
- При мерже дубликата
UPDATE sources_json = json_array_union(...)через Python-сторону (без JSON1-функций SQLite, чтобы не зависеть от SQLite-версии). - Фильтр API «source=osm,ttrails» работает через bbox-prefetch + Python-постфильтр (≤ 500 треков на bbox — это O(500) проверка
'osm' in sources, ничтожно). Минусы: - Невозможно индексировать массив без JSON1; нет нативного
WHERE 'osm' = ANY(sources). Принято: на BRD-объёме это не узкое место.
- S-B — нормализованная таблица
track_sources(track_id, source_id, ext_url). Отклонён:- JOIN на каждый bbox-запрос (1 → N запись на трек) +30–60% к p95.
- Усложняет API: GeoJSON-формирование требует aggregate-функции (
group_concat) → лишний SQL. - Не даёт значимого выигрыша на BRD-объёме (≤ 5–10 источников на трек после дедупа в худшем случае).
Вариант I (Indexes) — как ускорить bbox-фильтр
- I-A — Spatialite R-tree через виртуальную таблицу
idx_tracks_geom+ обычный B-tree наactivity_type(выбран).- R-tree даёт O(log n) на bbox-prefetch.
idx_tracks_activityускоряет fallback-фильтр.created_at— обычный B-tree для GC и для health-отчёта.
- I-B — четыре B-tree-индекса на
min_lon,max_lon,min_lat,max_lat(вариант из TRZ REQ-F-09). Отклонён:- SQLite-оптимизатор не комбинирует 4 индекса в bbox-плане; в лучшем случае использует один (по
min_lon), что даёт линейный полу-скан. - R-tree через Spatialite — стандартный паттерн для spatial-запросов; уже используется в
centralfederal.sqlite(idx_features_geom).
- SQLite-оптимизатор не комбинирует 4 индекса в bbox-плане; в лучшем случае использует один (по
Вариант W (WAL) — режим записи
- W-A — WAL-mode постоянно (выбран). При запуске pipeline
PRAGMA journal_mode=WAL. Даёт читателям (FastAPI) видеть консистентный снэпшот пока pipeline пишет. - W-B — DELETE-mode + блокировка читателей на время прогона. Отклонён: означает простой
/api/gps-tracksна 1–6 часов в неделю.
Решение
Принимается комбинация: D-A + T-A + S-A + I-A + W-A.
-
Отдельная БД
data/gps_tracks.sqlite(Spatialite-extension загружается при коннекте). Путь в окружении —GPS_TRACKS_DB_PATH=/app/data/gps_tracks.sqlite(см.07-infra-requirements.md§5). -
Единая таблица
tracksсо схемой, зафиксированной в08-data-requirements.md§3. Уточнения относительно TRZ REQ-F-09:points_countиlength_m— посчитанные на pipeline (НФТ Endpoint p95 ≤ 300 мс не оставляет бюджета считать длину на лету).min_lon/max_lon/min_lat/max_latсохраняются денормализованно вместе с R-tree (избыточно, но ускоряет MVT-генерацию: можно отбросить трек доwkb_to_coords()если bbox целиком вне тайла).tags_json,description— допускается NULL (не все источники их отдают).user(имя автора) сохраняется только если ADR licensing соответствующего источника явно разрешает (см. ADR-009/010/011). Иначе — NULL.
-
sources_jsonиexternal_urls_json— JSON-массивы строк, длина ≤ 8 элементов (дополнительные источники после дедупа). Порядок — стабильный (поgps_sources.yaml), что фиксирует «первый» источник для MVT-фичиproperties.source(используется для цветовой палитры по умолчанию, REQ-F-16). -
Индексация:
- Spatialite R-tree
idx_tracks_geomчерезCreateSpatialIndex('tracks', 'geom'). - B-tree
idx_tracks_activity(activity_type). - B-tree
idx_tracks_created(created_at)для GC и health. - UNIQUE
idx_tracks_dedup(dedup_key)— критичен для ON CONFLICT логики dedup (ADR-006). - Дополнительный bbox-индекс из TRZ REQ-F-09 (
min_lon, max_lon, min_lat, max_lat) не создаётся — R-tree его покрывает; B-tree на 4 колонки даст overhead на INSERT без выгоды на SELECT.
- Spatialite R-tree
-
WAL-mode включается в
db.py::open_connection()черезPRAGMA journal_mode=WALпри первом запуске; повторно команда no-op. Pipeline пишет в WAL, читатели видят последний checkpoint. После каждого(region, source)pipeline вызываетPRAGMA wal_checkpoint(PASSIVE)для контроля размера WAL-файла. -
Размер БД оценивается ≤ 2 ГБ для ЦФО+Чувашии при ≥ 5000 треков (REQ-NF-03). Метрика
db_size_mb— в/api/gps-tracks/health(REQ-F-12), порог-алерт > 2 ГБ — в10-tech-risks.mdR-4. -
Pipeline-история — таблица
pipeline_runs(TRZ REQ-F-09) в той же БД. Используется только для health-эндпоинта и оператора. Не индексируется по region/source — её объём ≤ 10⁴ строк за годы. -
Совместимость с MVT-pipeline в
main.py. Утилитарные функцииtile_to_bbox,wkb_to_coords,simplify_coordsуже существуют вsrc/api/main.pyдля слояtrails. ET-008 не рефакторит их (out of scope, риск регрессии слояtrails). Вместо этого:- В
src/api/gps_tracks/mvt.pyфункции_tile_to_bbox/_wkb_to_coordsдублируются с TODO-комментарием и ссылкой на тех-долг (10-tech-risks.mdR-7). - Если в будущей фазе появится третий MVT-источник (BRD §1 «Видеть реальные дороги/тропы»), перед ним вводится shared-модуль
src/api/tiles_util.pyотдельным work item.
- В
-
Cross-DB запросы (PH-3) — out of scope. Принципиальный путь, если понадобится в Smart Route:
ATTACH DATABASE 'data/gps_tracks.sqlite' AS gpsв коннекте main-API. Это решение откладывается до конкретной задачи PH-3.
Последствия
Положительные
- Pipeline пишет, не блокируя API-чтения OSM-данных.
- Бэкап и ротация независимы — оператор управляет каждой БД отдельно.
- Расширение списка источников (BRD F-04) или регионов (BRD F-12) не требует DDL — только обновление YAML.
- При ошибке pipeline (повреждение БД) —
rm data/gps_tracks.sqlite && python scripts/gps_collect.pyвосстанавливает за один прогон (≤ 6 часов, REQ-NF-02). Это закрывает риск «pipeline испортил продакшен-данные». - Spatialite R-tree обеспечивает p95 ≤ 300 мс на bbox-запросах без необходимости PostgreSQL.
Отрицательные / ограничения
- Денормализация
sources_json/external_urls_jsonне позволяет нативногоWHERE 'osm' = ANY(sources). Фильтр source — постфильтр на Python после bbox-prefetch (приемлемо: BRD §6 показывает ≤ 500 треков на bbox). - Дублирование
tile_to_bbox/wkb_to_coordsмеждуmain.pyиgps_tracks/mvt.py— технический долг (10-tech-risks.mdR-7). При следующем добавлении MVT-источника обязательно вынести в shared util. - Cross-DB запросы между OSM-данными и GPS-треками невозможны без
ATTACH DATABASE. На горизонте MVP таких запросов нет, но это блокер для будущей фичи «маршрут предпочитает реально-езженые дороги» (PH-3). - Дублирование bbox-полей (
min_lon/max_lon/min_lat/max_lat) в строке трека + R-tree-индексе — избыточные ~32 байта на трек; на 5000 треков ничтожно, осознанный compromise ради быстрого «бросить трек до парсинга WKB».
Технический долг
- Если объём вырастает > 2 ГБ (расширение на всю РФ), перевод на PostGIS. Контракт API
/api/gps-tracks/*стабилен; меняется толькоdb.py. Backend-код, фронтенд, миграции — без изменений. - Возможный future-rewrite на shared
src/api/tiles_util.py(см. §8 решения).
Классификация изменения
Major change. Введение новой БД на сервере явно перечислено в правилах для агентов (CLAUDE.md, эскалация: «новый сервис, новая БД → arch:major-change»). Лейбл arch:major-change выставлен. Обязательный архитектурный approve — да.
Связанные документы
docs/work-items/ET-008/01-brd.md§7 «БД»docs/work-items/ET-008/02-trz.mdREQ-F-09 «Схема БД»docs/work-items/ET-008/06-adr/ADR-006-dedup-algorithm.mddocs/work-items/ET-008/06-adr/ADR-007-pipeline-architecture.mddocs/work-items/ET-008/07-infra-requirements.md§4 «Хранилища данных»docs/work-items/ET-008/08-data-requirements.md§3 «Серверные данные»docs/work-items/ET-008/10-tech-risks.mdR-3, R-4, R-7