goon/alembic/versions/20260813_0028_phash_backfill_queue.py
goon-foss a16883d2f3 feat(dedup): backfill phasha z zapisanych miniaturek
Root cause zgloszenia 9d272101 ("5 duplikatow, czemu nie zmergowane?"):
phash mialo tylko 37,8% grywalnych scen. Licza go WYLACZNIE niektore
konektory w momencie ingestu, wiec najwieksze zrodla maja miniaturke dla
~100% scen, a odcisk dla 1-2% (xvideos 1,8%, xnxx 1,4%, eporner 1,0%).
Material jest zapisany, brakowalo tylko obliczenia.

W zglaszanym przypadku 7 wierszy o identycznej dlugosci 2324 s, a dwa
kafelki z IDENTYCZNA okladka nie mialy phasha wcale, wiec jedyny sygnal,
ktory by je sklail, nigdy nie powstal.

Zakres CELOWANY, nie caly katalog: sceny w klastrze (performer, dokladna
dlugosc) o licznosci >1, bez phasha, ale z zapisana miniaturka = 238 704
pozycje (wobec ~1,4 mln scen bez phasha). Reszta nie ma sie z czym skleic.

Kolejka jako TABELA, nie zapytanie w locie: wyznaczenie kandydatow to
ciezki agregat po calym katalogu, ktorego nie chcemy powtarzac przy kazdej
paczce, a kolejka przezywa restart workera i widac z niej postep.

Weryfikacja end-to-end na zglaszanej scenie: ce81a110 dostal phash
85c1bac2f8c60ecf, czyli DOKLADNIE ten sam, ktory mial juz 8373bbb5 (dwa
z kafelkow uzytkownika) -> istniejacy phash_exact je teraz sklei.

Nie zapisujemy wartosci z phash_blacklist (zaslepki dzielone przez setki
scen). Throttling swiadomy: 800 co 15 min, ~3 dni na nadrobienie.

Dwie pulapki SQL zapisane w kodzie: `referer` ma 255 znakow wiec origin
wycinamy w SQL (pelny page_url wywalal planowanie), a rzutowanie musi isc
przez CAST() bo text() SQLAlchemy nie podstawia parametru za ktorym stoi
`::`.
2026-08-13 10:49:12 +02:00

47 lines
1.7 KiB
Python

"""phash_backfill_queue: kolejka scen do policzenia phasha z zapisanej miniaturki
Uzasadnienie i liczby: `app/models/phash_backfill_queue.py`.
Revision ID: 0028_phash_backfill_queue
Revises: 0027_scenes_title_trgm
Create Date: 2026-08-13
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0028_phash_backfill_queue"
down_revision: str | None = "0027_scenes_title_trgm"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"phash_backfill_queue",
sa.Column("scene_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("thumbnail_url", sa.String(length=1024), nullable=False),
sa.Column("referer", sa.String(length=255), nullable=True),
sa.Column("attempts", sa.Integer(), nullable=False, server_default="0"),
sa.Column("done_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.ForeignKeyConstraint(["scene_id"], ["scenes.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("scene_id"),
)
# Jedyne zapytanie ścieżki gorącej: „weź N nieprzetworzonych". Indeks częściowy,
# bo przetworzone wiersze zostają w tabeli jako ślad, ale nigdy ich nie czytamy.
op.create_index(
"ix_phash_backfill_pending",
"phash_backfill_queue",
["attempts"],
postgresql_where=sa.text("done_at IS NULL"),
)
def downgrade() -> None:
op.drop_index("ix_phash_backfill_pending", table_name="phash_backfill_queue")
op.drop_table("phash_backfill_queue")