Skip to content
Kumquat
Esc
↑↓navigate↵open⌘Jpreview
On this page

Mongo-схема

Колекції products → tasks → creatives, дзеркало users із Clerk і версіонування документів.

Три пов’язані колекції: products → tasks → creatives. Каталог користувачів (users) дзеркалить Clerk (синк через webhook). Enum-словники — джерело правди в kumquat_core.enums; $jsonSchema в БД перевіряє лише структуру/типи, тож додати мову чи гео — це деплой, не міграція. На кожному документі є schema_version для лінивих міграцій.

PRODUCTS — колекція products

Фіксований набір із 4 продуктів; _id — це код продукту (він же вживається в неймінгу креативів), тож зовнішній ключ на задачах/креативах читабельний і не потребує денормалізованої копії.

  • _id (text) — код продукту, UNIQUE: PLT, CNI, RKI, SPD
  • name (text) — повна назва: PlantIn, CoinIn, RockIn, SportDex
  • schema_version, created_at, updated_at

TASK — колекція task__tasks

Одна задача — одна одиниця виробництва, що групує свої креативи.

━━━ ОСНОВНІ ━━━

  • name (text) — технічна назва картки
  • number (number) — авто-номер задачі
  • generated_name (text) — обчислювана назва задачі

━━━ ТИП ЗАДАЧІ / ТАКСОНОМІЯ ━━━

  • product_id (→ products._id) — відношення до продукту
  • task_type: NEW, VAR, LOC, WHT, NEWDES, VARDES, DES, CLN, VARAI, ADPT, RSZ
  • work_stream: Performance, Product, Web, Brand, SMM, HR
  • media_type: VID, STA, ANI, WEB, LND, ENDC
  • execution_complexity (number 1–5) — складність виконання
  • creation_complexity (number 1–5) — складність креативного задуму
  • priority (number 1–3)

━━━ СТАТУСИ ━━━

  • status (TaskStatus): – To-do: backlog, ready_for_production – In progress: in_progress, tech_check, creative_check – Complete: done, archived
  • test_status: not_tested, in_test, tested, paused

━━━ КРЕАТИВНІ ПАРАМЕТРИ ━━━

  • count (number) — к-сть Creative-карток
  • resolution (без дефолту — задачу без нього прогон відхиляє, крім LOC/WHT/ADPT, які беруть розмір з оригіналу): 1080x1920, 1920x1080, 640x768, 800x800, 320x50, 1080x1350, 250x250, 300x250, 336x280, 200x200, 300x600, 1242x2688, 1080x1080, 600x800, 800x600
  • source (дефолт META): META, TIK, APL, GGL, SNA, SKN, TAB, UNT
  • geo: US, FR, DE, CA, UA, ES, PT, BG, TW, KR, GR, CL, ZA, NZ
  • language: EN, FR, DE, PT, ES, UA, CA-FR, IT, UK, TR, PL, JA, HE, MX, BR, DA, NO, CH, SV, NL, BE, IE, AT, CS, RO, HI, BG, ZH, KO, EL
  • varied_elements (multi): hook, cta, body_concept, background, hook_script, voice, music, persona
  • var_description (text)

━━━ ВІДНОШЕННЯ ━━━

  • origin_creative_id (→ creatives._id) — вихідний креатив (для VAR/VARDES)
  • source_creative_ids (→ creatives._id[]) — вихідні креативи (для LOC/WHT/ADPT/RSZ)

━━━ ROLLUP / FORMULA ━━━ (rollups, кеші, оновлює sync/rollup-джоб)

  • rollups.derived_task_count (count) — к-сть похідних креативів
  • rollups.creation_points, rollups.execution_points (formula) — таблична залежність лише від власної складності (creation_complexity/execution_complexity): Level 1 → 0.5, Level 2 → 1.0, Level 3 → 2.0, Level 4 → 3.0, Level 5 → 5.0. null, поки відповідна складність не заповнена
  • comment_count (number, дефолт 0) — живі коментарі задачі; не в rollups, бо це єдине таке число, яке малює картка дошки, а TaskCard весь блок rollups відкидає. Тримає його актуальним сам запис коментаря ($inc разом із позначкою __last_comment_id), лагодить kumquat-cli backfill-comment-counts — див. коментарі

━━━ ЛЮДИ ━━━ (→ users._id)

  • assignee_id — виконавець
  • task_owner_id — постановник

━━━ ДАТИ ━━━ (усі, крім deadline_at, — тільки воркфлоу, по HTTP не виставити)

  • deadline_at — бажана дата здачі; час — опційний, і 00:00 UTC означає «час не задано» (див. kumquat_core.optional_time)
  • started_at — вхід у in_progress
  • submitted_at — віддано на Tech/Creative Check
  • completed_at — прийнято на done

━━━ ІНШЕ ━━━

  • is_legacy (bool) — старий формат назви
  • manual_launch (bool)
  • legacy_remap_list (text)
  • drive_url (url) — папка задачі

━━━ PRODUCT STACK / MARKETING FUNNEL ━━━ (заповнюються вручну при створенні брифу; free-text поля — заглушки до появи власних колекцій; subject/topic/audience беруться зі словників продукту, Product.dictionaries)

  • funnel (text), hypothesis (text)
  • subject (text), topic (text), subtopics (text[])
  • traffic_flow: W2A, WEB
  • landing (text — пізніше Relation → Landing DB)

━━━ VARIANTS (variants, list of TaskVariant) ━━━ — по одному запису на майбутній креатив, для NEW/NEWDES/VAR/VARDES/VARAI. Це те, що раніше називалося Creative Stack і заповнювалося один раз на всю задачу — тепер кожна майбутня варіація отримує власний бриф, і creative_factory._scratch_card бере ці поля з variants[n - 1], а не з полів задачі. LOC/ WHT/ADPT/RSZ не мають variants взагалі — їхня картка будується з оригінального креативу.

  • key (text) — клієнтський ідентифікатор блоку, не показується в назві й ні на що не впливає на бекенді
  • variation_num (int, 1..count) — яку картку заповнює цей блок
  • primary_anchor: pain_led, desire_led
  • audience (text — зі словника продукту)
  • awareness_stage: unaware, problem_aware, solution_aware, product_aware, most_aware
  • messaging_angle: pattern_interrupt, proof_driven, trust_driven
  • hook_trigger: pattern_interrupt, identity_call_out, pain_agitation, curiosity_gap, social_proof_credibility, contrarian_myth_busting, aspiration_desire, urgency_stakes
  • hook_type: 24 значення (бриф-девайс хука; не плутати з VLM hook.format/hook.script)
  • ad_format: ~86 значень (формат подачі креативу)
  • notes (text) — коментар креативного маркетолога
  • tag_slugs (text[]) — довільні теги цієї варіації, зі спільного словника creatives__tags (див. нижче)
  • description (text, BlockNote JSON) — ТЗ саме до цієї варіації, окремо від загального ТЗ задачі (var_description)

Задача, створена до появи variants (або будь-яка задача, для якої це поле лишили порожнім), досі має старі скалярні поля primary_anchor .. notes прямо на задачі — вони не видалені, _scratch_card читає їх як фолбек, коли variants порожній. Нове створення завжди пише в variants; ці скалярні поля існують лише для сумісності зі старими документами.

━━━ CREATIVE TAGS — колекція creatives__tags ━━━ — довідник тегів, на які посилається variants[].tag_slugs/Creative.tag_slugs. Будь-хто може завести новий тег прямо з поля Tags (потрапляє як proposed); адмін підтверджує його (approved) або відхиляє (archived) зі сторінки /creative-tags.

  • slug (text, UNIQUE) — похідний від label, те, що зберігається в tag_slugs. Правило одне (kumquat_creatives.tag_slug.normalize_tag_slug, дзеркало — frontend/src/modules/creative-tags/model/slug.ts): lowercase, і кожна послідовність усього, що не літера й не цифра (пробіли, пунктуація, дефіси, підкреслення), стає одним _; кінці обрізаються; максимум 32 символи, з обрізанням по межі слова. Літери будь-якої мови лишаються, тож «Зимова акція» → зимова_акція. Тому ugc-hook, UGC hook і ugc_hook — один тег, а не три. Нормалізує сервер (і в тілі, і в шляху), не клієнт
  • label (text, UNIQUE без урахування регістру) — назва для показу. Унікальний, бо поле Tags пропонує саме лейбли й мапить обраний назад у slug: два однакових лейбли зробили б один із тегів невибираним. Лейбл мусить містити хоч одну літеру чи цифру, інакше 422 — з нього нічого було б вивести
  • Перейменування рухає slug разом із лейблом (TagsService.set_label), бо slug — похідний, і розійтися вони не можуть. Тому це міні-міграція: переписуються creatives.tag_slugs і task__tasks.variants[].tag_slugs усіх носіїв. 409, якщо новий лейбл або похідний від нього slug уже чужі (два різних лейбли можуть давати один slug). Зміна лише регістру slug не рухає, тож нічого й не переписує. Атомарності немає (ні транзакцій, ні arrayFilters у тестах): посилання їдуть першими, тож збій на півдорозі лікується повторним тим самим запитом
  • status: proposed, approved, archived
  • created_by (→ users._id)

Підтвердження тега (зміна status) — це запис лише в creatives__tags: жоден креатив чи варіація, що вже посилається на цей slug, не переписується. Це стосується саме статусу — перейменування, навпаки, переписує (див. вище).

Адмін також може завести тег прямо зі сторінки /creative-tags — такий одразу approved, бо саме додавання і є тим рішенням, на яке чекає proposed — і видалити тег назовсім. Видалення дозволене лише поки жоден креатив не несе цей slug (інакше 409): схема фільтрів креативів пропонує як tag_slugs саме цю колекцію, тож видалений тег зробив би кожен креатив із ним незнаходжуваним. Тег, який уже використовували, виводять з обігу через archived.

CREATIVE — колекція creatives

Один документ на один креатив-варіант.

━━━ ОСНОВНІ ━━━

  • name (text, UNIQUE) — канонічна назва (джерело правди, будується скриптом)
  • product_id (→ products._id) — rollup продукту з задачі
  • task_type: NEW, VAR, LOC, WHT, NEWDES, VARDES, DES, CLN, VARAI, ADPT, RSZ
  • work_stream: Performance, Product, Web, Brand, SMM, HR
  • media_type: VID, STA, ANI, WEB, LND, ENDC
  • execution_complexity (number 1–5) — складність виконання
  • creation_complexity (number 1–5) — складність креативного задуму
  • variation_num (number) — номер варіації в задачі
  • modifier: HK1, HK2, HK3, UC, AI, RSIZ, CTA1, CTA2, PX
  • resolution: 1080x1920, 1920x1080, 640x768, 800x800, 320x50, 1080x1350, 250x250, 300x250, 336x280, 200x200, 300x600, 1242x2688, 1080x1080, 600x800, 800x600
  • source: META, TIK, APL, GGL, SNA, SKN, TAB, UNT
  • geo: US, FR, DE, CA, UA, ES, BG, TW, KR, GR, CL, ZA, NZ
  • language: EN, FR, DE, PT, ES, UA, CA-FR, IT, UK, TR, PL, JA, HE, MX, BR, DA, NO, CH, SV, NL, BE, IE, AT, CS, RO, HI, BG, ZH, KO, EL

━━━ СТАТУСИ ━━━

  • test_status: not_tested, in_test, tested, paused
  • performance_tier: promising, potential, pre_scale, scale, top_performer
  • flagged_for_review (bool)
  • is_whitened (bool) — потребує вайтлістінгу
  • (робочий статус — rollup зі статусу задачі, резолвиться через task_id; is_legacy живе на задачі)

━━━ МЕТРИКИ ━━━ (metrics — останній снапшот; історії поки немає, див. creative_metrics_ts нижче)

Блок пише цілком тільки батч kumquat-creative-metrics (ClickHouse → Mongo), атомарним $set на весь піддокумент. Усі поля nullable: документи, старші за скоркард, мусять валідуватися, бо їх читає kumquat-ch-sync. Інваріанти, заміряне покриття джерела й семантика для людей — README пакета kumquat-creative-metrics.

Легасі-скаляри (чинний контракт UI-карток, фільтрів і сортів) — кожен є проєкцією вікна нижче, ставки завжди частки 0–1, не відсотки:

  • metrics.ctr = w.d7.ctr.v, metrics.hook_rate = w.d7.hook.v, metrics.hold_rate = w.d7.hold.v — vv15 під ними означає 15-секундний перегляд у картки з атрибуційного фіда і 6-секундний у картки з source = TIK (TikTok 15-секундних не публікує), тож одне поле несе два трохи різних питання
  • metrics.spend = w.life.spend, metrics.purchases = w.life.purchases
  • metrics.cpi, metrics.ipm — від life
  • metrics.updated_at = computed_at

Три вікна metrics.w.{d7,t7,life}, кожне однакової форми:

  • лічильники: imp, vv, vv15, clicks, spend, purchases, installs
  • ставки з Wilson-інтервалом: hook, hold, ctr — кожна {v, lo, hi, hw, n}, v може бути null
  • cpm, cpa (cpa — від purchases, тієї самої колонки, що й сторінка аналітики)
  • лише t7: hook_delta, hold_delta (проти попередніх 7 днів)
  • лише life: ltv, roas (модельний прогноз, не факт), trials
  • лише life і лише TikTok: likes, comments, shares, follows, profile_visits та власна атрибуція TikTok tt_app_install, tt_registration, tt_purchase, tt_conversion, tt_skan_conversion. У картки, якої звіт TikTok не бачив, усі вони null, а не 0: нуль читався б як «поміряли й вийшло нічого». Префікс tt_ навмисний — tt_purchase і purchases рахують ту саму подію різними правилами й не складаються

d7 прибите до старту креатива (порівнюване між креативами), t7 — до watermark (для kill/scale), life — облік, не сигнал якості.

Оцінки й контекст:

  • metrics.status: learning, mature, stale
  • metrics.confidence.{hook,hold,ctr}: ok, thin, none
  • metrics.bench: {hook_med, hold_med, cohort_n, hook_index, hold_index} — d7-ставки проти медіани 90-денної когорти свого продукту, 1.0 = медіана. Усе null, якщо d7_truncated або когорти нема
  • metrics.d7_truncated (bool) — креатив уже крутився до початку імпортованої історії, тож його «перші 7 днів» не перші; з бенчмарку виключений. У TikTok-картки історій дві (фід і звіт TikTok) і прапорець ставиться за пізнішою з них
  • metrics.product — продукт із найбільшими показами в CH; може розходитись із product_id, і саме за ним береться когорта
  • metrics.first_seen, last_seen, days_live, active_days

Походження й свіжість:

  • metrics.defn_version — версія контракту документа, metrics.run_id
  • metrics.computed_at (коли крутився DAG) і metrics.data_through (останній день даних) — це різні речі; злиття їх ховає застряглий імпорт за свіжим таймстемпом
  • metrics.conv_provisional_from — конверсії й ltv після цієї дати ще попливуть
  • metrics.media_sources — скоуп run-у ([] = всі джерела)

━━━ VLM (vlm, nullable до аналізу) ━━━

  • vlm.analyzer_version, vlm.model, vlm.analyzed_at, vlm.notes
  • vlm.rev — фінгерпринт дайджеста кроку аналізу (як media.rev): охоронець проєкції від повторного запису, не аналітичний вміст

Майже кожен закритий словник має other_unrecognized: JSON-схема Gemini не дозволяє значення поза списком, тож без цього виходу модель була б змушена вгадувати найближче — і підрахунки «який елемент спрацював» тихо збирали б у найпопулярніше значення все, що насправді туди не належить. Виняток — cast (no_person) і visual_style (mixed), де прогалину вже покрито. null означає інше: ознаки немає в цьому сегменті взагалі.

HOOK (vlm.hook):

  • format: pov_selfie_talking, ugc_testimonial, person_reaction, green_screen_reaction, object_close_up, screen_record_demo, social_screen, text_animated, text_on_screen_static, before_after_reveal, unboxing_reveal, split_screen, vo_visuals, other_unrecognized
  • script: question, fact, problem, claim, challenge, warning, teaser, instruction, fomo, story_anecdote, other_unrecognized
  • marketing_angles (multi): pain_callout, curiosity_gap, bold_claim_stat, question_hook, social_proof_open, direct_address, other_unrecognized
  • visual_triggers (multi): surprising_object_reveal, shock_claim_text, before_after_contrast, pattern_interrupt_motion, question_overlay, relatable_scenario, other_unrecognized
  • cast: woman, man, couple, kid_family, no_person, multiple_people
  • camera_angle: selfie_handheld_pov, static_tripod, macro_close_up, screen_recording, other_unrecognized
  • setting: home_interior, outdoor_nature, store_shop, car, studio_plain_bg, screen_only, other_unrecognized
  • subject (text — відкритий словник), face_visible (bool), duration_sec (number)
  • description, on_screen_text, pain_point, vo (text)

BODY (vlm.body) — усі поля null для відео без окремого body-сегмента:

  • format: demo_walkthrough, feature_carousel, testimonial_montage, before_after_result, educational_explainer, reviews_social_proof_screen, other_unrecognized
  • script: problem_solution, how_it_works, facts_tips, benefits_list, before_after, tutorial_steps, social_proof, statistics, urgency, other_unrecognized
  • concept (text — складена таксономія Scan:ID тощо)
  • marketing_beats (multi): pain_problem, agitation, solution_intro, feature_benefit, rationale_credibility, social_proof, urgency_scarcity, cta, other_unrecognized
  • cast, camera_angle, setting (ті самі словники, що й у hook), face_visible (bool)
  • description, vo (text)
  • cta_type: download_free, free_trial, limited_time_offer, direct_benefit_cta, other_unrecognized

CONTENT (vlm.content):

  • app_features_shown (text[]), proof_types (text[])
  • solution_framing (text), primary_pain_point (text)
  • emotional_driver: curiosity, fear_of_missed_value, relief, pride_collection_completion, urgency, nostalgia, surprise_delight, other_unrecognized
  • visual_realism: photorealistic_live_action, realistic_ai_generated, cartoon_2d, cgi_3d, stylized_flat_graphic, mixed_hybrid, other_unrecognized
  • visual_style: ugc, screen_demo, motion_graphics, ai_video, text_based, mixed
  • pacing: fast_cut, medium, slow_cinematic, other_unrecognized

━━━ ВІДНОШЕННЯ ━━━

  • task_id (→ tasks._id) — обов’язково, 1 задача на креатив
  • origin_ref ({_id → creatives._id, name}) — розширене посилання на оригінал (LOC/WHT/ADPT/RSZ)

━━━ PRODUCT STACK / MARKETING FUNNEL ━━━ (rollup з task; той самий набір полів і той самий словник значень, що й у TASK вище)

  • funnel, hypothesis, subject, topic, subtopics, traffic_flow, landing

━━━ CREATIVE STACK / TAGS / BRIEF ━━━ — rollup з відповідного TaskVariant (task.variants [variation_num - 1]), не з полів задачі — кожна варіація копіює свій власний блок, а не всі однаковий. Для задачі без variants — rollup зі старих скалярних полів задачі (той самий фолбек, що й у _scratch_card).

  • primary_anchor, audience, awareness_stage, messaging_angle, hook_trigger, hook_type, ad_format, notes
  • tag_slugs (text[]) — з TaskVariant.tag_slugs; переживає перехід тега з proposed у approved без жодної зміни в цьому масиві
  • description (text, BlockNote JSON) — з TaskVariant.description

━━━ ЛЮДИ ━━━ (→ users._id)

  • assignee_id, task_owner_id (успадковуються із задачі)

━━━ ДАТИ ━━━

  • deadline_at, in_work_at, done_at
  • launched_at — коли test_status → in_test
  • ad_id (text) — id кампанії в рекламному кабінеті, який назвала платформа разом із запуском (див. Інтеграційний API); пишеться один раз, Kumquat його не генерує. Те саме поле є на паку (tasks.ad_id) — звіт по паку розкладає його на всі картки.

━━━ ІНШЕ ━━━

  • schema_version, created_at, updated_at

USERS — колекція users

Локальне дзеркало Clerk (синк через webhook apply_webhook_event); директорні читання йдуть звідси.

  • _id (text) — Clerk user id (user_...), не ObjectId
  • email, name, image_url (text)
  • role (text)
  • groups (text[]) — частини команди, до яких належить людина; одна людина може бути в кількох. Дзеркалиться з Clerk publicMetadata.groups, як і role: upsert замінює документ на кожному вебхуці, тож поле, записане лише в Mongo, зникає. Дошка capacity малює колонку кожному з груп, обраних у фільтрі groups; хто не в жодній групі — колонки не отримує ніколи
  • created_at, updated_at

creative_metrics_ts — time-series

Ще не існує. Колекцію ніхто не створює і ніхто в неї не пише: скоркард (kumquat-creative-metrics) пише тільки creatives.metrics і creatives__metrics_runs, історія метрик поки не зберігається взагалі. Форма нижче — це задекларована модель MetricsSnapshot (kumquat_creatives.models), тобто план, а не факт.

Append-only снапшоти метрик (timeField=ts, metaField=meta); останній снапшот денормалізується в creatives.metrics. Поля: ts, meta ({creative_id, source}), ctr, cpi, ipm, hook_rate, hold_rate, spend, purchases.

COMMENTS — колекція comments

Коментарі до задач. Узагальнена схема (entity_type + entity_id) замість task_id, щоб інші сутності могли отримати коментарі без міграції; сьогодні підключено лише task.

  • entity_type (text) — task (єдине значення поки що)
  • entity_id (→ tasks._id)
  • author_id (→ users._id, Clerk id — рядок user_..., не ObjectId)
  • body (text) — зберігається нормалізованим: NFKC, без невидимих символів (категорії Cc/Cf/ Zl/Zp, окрім \t і \n), прогони з 3+ переводів рядка стиснуті до одного порожнього рядка, краї обрізані. Максимум 2000 символів після нормалізації — NFKC вміє подовжувати рядок (fi → fi), тож ліміт на вхідному запиті сам собою не гарантує розмір збереженого
  • mentioned_user_ids (→ users._id[]) — Clerk id користувачів, передані фронтендом явно; звалідовані на існування, без автора
  • reactions (map emoji → users._id[]) — {"👍": ["user_a", "user_b"]}. Ключі — закритий набір ReactionEmoji (30 емодзі), бо ключ тут це ім’я поля; емодзі без реакторів не лишається порожнім масивом, а видаляється з мапи
  • created_at
  • updated_at (nullable) — проставляється при редагуванні автором; null = ніколи не редагували
  • deleted_at (nullable) — м’яке видалення; null = живий коментар. Документ лишається в колекції разом із body, але API віддає видалений коментар із порожніми body, mentioned_user_ids і reactions і не рахується в task__tasks.comment_count

Відповідей (тредів) немає: коментарі плоскі. GET /tasks/{id}/comments повертає хронологічний список, включно з видаленими — сортування завжди за created_at, тож редагування не міняє порядок.

AUDIT__LOGS — колекція audit__logs

Єдиний append-only журнал змін: хто, що і коли змінив. Пишеться декоратором @audited на методах запису репозиторіїв (пакет kumquat-audit), а не викликами по бізнес-логіці.

  • at (date) — коли
  • action (text) — task.created, task.status_changed, comment.mention_notified, …
  • entity_type / entity_id (text) — entity_id завжди рядок: типи id різні по колекціях (ObjectId у задач і креативів, код продукту в products, Clerk id у users, S3-ключ в assets)
  • actor_id (→ users._id, Clerk id; null для системних дій), actor_kind — user / system / webhook / cli
  • source — api / worker / webhook / cli; request_id — id HTTP-запиту або Celery-таски
  • changes (object | null) — застосований $set; на рядку створення це повний документ, що і є якорем для відновлення стану. null там, де подія нічого локально не змінила (спроба надіслати DM) — згортка дельт такі рядки пропускає
  • metadata (object | null) — решта аргументів методу; тільки BSON-скаляри

Дві колекції згорнуті сюди й більше не пишуться:

Було Стало
task__transitions action = "task.status_changed", changes.status = куди, metadata.expected_status = звідки
mention_notification_logs action = "comment.mention_notified", metadata.recipient_id + metadata.status (sent / skipped_no_email), changes = null

sent означає “спробу зроблено”, а не “Slack підтвердив доставку”: SlackNotifier ковтає помилки Slack API і лише логує warning.

creatives__metrics_runs — стан між run-ами скоркарду

Один документ на run пакета kumquat-creative-metrics. Не історія метрик — саме стан, потрібний наступному run-у: гард на обвал кількості рядків порівнюється з останнім успішним не-dry run-ом, а модальна defn_version по колекції — це те, що перевіряє doctor. Індекси: finished_at (спадний), run_id.

Поля: run_id, defn_version, wm, dry_run (bool), media_sources, rows, matched, modified, missing, match_ratio, swept, started_at, finished_at, ok (bool), error (лише коли ok: false). Dry-run-и не пишуться взагалі.


Індекси і ліміт 64 на колекцію

MongoDB не дає колекції більше 64 індексів, і creatives стоїть рівно на цій межі — 64/64, включно з _id_. Практичний наслідок: нового індексу на creatives додати не можна, не прибравши інший. Специфікації живуть у indexes.py кожного feature-пакета, композуються в kumquat_api/indexes.py.

Дві речі, які варто знати перед тим, як щось тут міняти.

ensure_indexes тільки створює, ніколи не видаляє. Перейменоване поле лишає свій індекс на колекції назавжди, і той тихо тримає слот. Саме так це й зламалось: complexity колись розділили на дві оцінки — одна sort-опція стала двома, тобто декларований набір виріс з 63 до 64, а забутий complexity_1__id_1 зробив 65. Тому кожне перейменування поля, яке має індекс, тягне за собою явний dropIndex поруч зі створенням (_drop_legacy_unique_name у kumquat-tasks, _drop_redundant_metrics_status_index у kumquat-creatives) — щоб кожна база лікувала себе сама, а не чекала ручного dropIndex в кожному середовищі. Такий дропер живе рівно доти, доки всі середовища його не проковтнули; після цього його прибирають разом з одноразовою міграцією, яка перейменувала поле.

Помилка про ліміт вказує не на винного. Mongo репортує CannotCreateIndex на тому індексі, який трапився останнім у цикл створення, — у тому випадку це був marketing_complexity, який ні до чого не причетний. Тому створення йде через kumquat_core.db.indexes.create_indexes(): на коді 67 вона перечитує колекцію і каже, що насправді сталося — ліміт, скільки індексів є проти скількох задекларовано, і перелік тих, яких у коді вже немає.

Стеля перевіряється тестом (tests/core/test_db_indexes.py), щоб набір не доріс до 64 мовчки. Підняти число в тесті — це рішення: спершу варто прочитати CREATIVE_INDEX_FIELDS і зняти те, чого жоден запит не потребує.

Was this page helpful?