Tabele per urzadzenie (migracja 0037): clip_seen (wykluczenie, wiersz przy
podaniu klipu), clip_feed_events (append-only: impression z dwell, skip,
complete, like, unlike, not_interested, autoswipe), clip_affinity (wagi
tag/nisza/tworca w JSONB), clip_favorites, clip_blacklist. GIN na clips.niches.
GET /clips/feed: kursor base64 {seed, pk, page} zamiast offsetu; 80% z puli
afinicznosci (skany po tagu/niszy/tworcy od losowego uuid, punktacja
sum wag + 0,3 ln(1+lajki) + swiezosc, max 2 klipy tworcy na strone) + 20%
explore (okno 2000 id po PK, losowanie wazone lajkami, zawijanie na koncu).
Bez wag 100% explore. POST /clips/events: zapis, podniesienie outcome,
ulubione, delty wag (like +5, complete +3, dwell>=10 s +1, skip<2 s -1,
not_interested -4 i -8 na wskazany cel). Onboarding 3 tapow, opcje,
blacklista tag/nisza/tworca, reset feedu, ulubione. Import miedzy
urzadzeniami kopiuje clip_seen, clip_favorites i clip_blacklist.
Job clips-maintenance co 24 h: decay wag x0,97, retencja zdarzen 90 dni,
reaper (HEAD na 2000 najdawniej sprawdzanych). Logika bez bazy w
app/clips_feed.py z testami (kursor, delty, cap/decay, punktacja, sklad).
86 lines
4.2 KiB
Python
86 lines
4.2 KiB
Python
"""Feed Swipe, stan per urządzenie: clip_seen, clip_feed_events, clip_affinity,
|
|
clip_favorites, clip_blacklist. Plus GIN na clips.niches pod skan po niszy.
|
|
|
|
Uzasadnienie w docstringu `app/models/clip_feed.py`.
|
|
|
|
Revision ID: 0037_clip_feed
|
|
Revises: 0036_clips
|
|
Create Date: 2026-09-14
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision = "0037_clip_feed"
|
|
down_revision: str | None = "0036_clips"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"clip_seen",
|
|
sa.Column("device_id", sa.String(64), nullable=False),
|
|
sa.Column("clip_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("outcome", sa.String(16), nullable=False, server_default="served"),
|
|
sa.Column("seen_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.ForeignKeyConstraint(["clip_id"], ["clips.id"], name="fk_clip_seen_clip_id_clips", ondelete="CASCADE"),
|
|
sa.PrimaryKeyConstraint("device_id", "clip_id", name="pk_clip_seen"),
|
|
)
|
|
op.create_table(
|
|
"clip_feed_events",
|
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("device_id", sa.String(64), nullable=False),
|
|
sa.Column("session_id", sa.String(32), nullable=True),
|
|
sa.Column("clip_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column("kind", sa.String(16), nullable=False),
|
|
sa.Column("dwell_ms", sa.Integer(), nullable=True),
|
|
sa.Column("reason", sa.String(160), nullable=True),
|
|
sa.Column("slot", sa.String(12), nullable=True),
|
|
sa.Column("position", sa.SmallInteger(), nullable=True),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.PrimaryKeyConstraint("id", name="pk_clip_feed_events"),
|
|
)
|
|
op.create_index("ix_clip_feed_events_device_created", "clip_feed_events", ["device_id", "created_at"])
|
|
op.create_index("ix_clip_feed_events_created", "clip_feed_events", ["created_at"])
|
|
op.create_table(
|
|
"clip_affinity",
|
|
sa.Column("device_id", sa.String(64), nullable=False),
|
|
sa.Column("tag_weights", postgresql.JSONB(), nullable=False, server_default="{}"),
|
|
sa.Column("niche_weights", postgresql.JSONB(), nullable=False, server_default="{}"),
|
|
sa.Column("creator_weights", postgresql.JSONB(), nullable=False, server_default="{}"),
|
|
sa.Column("events_seen", sa.Integer(), nullable=False, server_default="0"),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.PrimaryKeyConstraint("device_id", name="pk_clip_affinity"),
|
|
)
|
|
op.create_table(
|
|
"clip_favorites",
|
|
sa.Column("device_id", sa.String(64), nullable=False),
|
|
sa.Column("clip_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.ForeignKeyConstraint(["clip_id"], ["clips.id"], name="fk_clip_favorites_clip_id_clips", ondelete="CASCADE"),
|
|
sa.PrimaryKeyConstraint("device_id", "clip_id", name="pk_clip_favorites"),
|
|
)
|
|
op.create_table(
|
|
"clip_blacklist",
|
|
sa.Column("device_id", sa.String(64), nullable=False),
|
|
sa.Column("kind", sa.String(8), nullable=False),
|
|
sa.Column("value", sa.String(128), nullable=False),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.PrimaryKeyConstraint("device_id", "kind", "value", name="pk_clip_blacklist"),
|
|
)
|
|
# Skan kandydatów po niszy (`niches && ARRAY[...]`) bez pełnego przebiegu tabeli.
|
|
op.execute("CREATE INDEX IF NOT EXISTS ix_clips_niches_gin ON clips USING gin (niches)")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP INDEX IF EXISTS ix_clips_niches_gin")
|
|
op.drop_table("clip_blacklist")
|
|
op.drop_table("clip_favorites")
|
|
op.drop_table("clip_affinity")
|
|
op.drop_index("ix_clip_feed_events_created", table_name="clip_feed_events")
|
|
op.drop_index("ix_clip_feed_events_device_created", table_name="clip_feed_events")
|
|
op.drop_table("clip_feed_events")
|
|
op.drop_table("clip_seen")
|