goon/alembic/versions/20260818_0029_scenes_max_quality.py
goon-foss 0fed952c5c feat(filtr): minimalna jakosc w filtrze scen (720p/1080p/4K)
Prosba uzytkownika (zgloszenie 17376139). Backend mial juz parametr `min_quality_p`,
ale w praktyce byl NIEUZYWALNY i to zajelo wiekszosc pracy.

Wydajnosc, mierzona na produkcji przez EXPLAIN ANALYZE, prog 1080p:

  EXISTS na playback_sources (stan zastany)   32 s   3 556 612 odwiedzonych scen
  + indeks funkcyjny per-scena                24 s
  JOIN z DISTINCT po jakosci                   1,1 s W IZOLACJI
  ten sam JOIN + reszta filtrow feedu         52 s   planer wraca do skanu po dacie
  kolumna scenes.max_quality_p (0029)          2,0-2,6 s
  + indeksy czesciowe pod progi (0030)         0,09-0,11 s

Wniosek, ktory kosztowal najwiecej: ksztaltem zapytania sie tego NIE dalo zalatwic.
Koszt to LICZBA odwiedzonych scen, nie pojedynczy test, a przy `ORDER BY created_at
DESC LIMIT 24` planer i tak wracal do przechodzenia scen po dacie, gdy tylko doszly
pozostale filtry (bramka wertykalow, heurystyka szkieletow, prog dlugosci). Stad
denormalizacja + indeksy czesciowe z predykatem progu, ktore daja gotowe sortowanie.

Semantyka SUROWA, swiadomie: `quality` zapisujemy oportunistycznie, zna ja 6,5% scen
(210 302 z 3 236 368), najlepsze zrodlo ma 26% pokrycia. Wariant lagodny, przepuszczajacy
sceny o nieznanej jakosci, odrzucony po zmierzeniu: odsiewalby 88 647 z 3,2 mln, czyli
nie realizowalby intencji "nie chce ogladac ziemniaka". Filtr pokazuje wiec tylko jakosc
POTWIERDZONA, a UI mowi to wprost podpisem pod przelacznikiem, bo inaczej gwaltowny
spadek liczby wynikow wyglada na zepsuta aplikacje.

Warianty tekstowe 4K/UHD usuniete z warunku: 0 wystapien na 238 452 zywe zrodla,
a `OR ilike` blokowal uzycie indeksu.

Odswiezanie w taxonomy_counts (co 3h), z czyszczeniem do NULL gdy scena traci zrodlo
z jakoscia. Backfill: 210 302 sceny w 2m56s.

Zweryfikowane na emulatorze: sekcja MIN QUALITY renderuje sie z podpisem ostrzegawczym.
Lista wyszla pusta, ale to build debugowy dostajacy 403 (podpis debug spoza allowlisty),
nie blad filtra: ten sam URL z poprawnym podpisem oddaje 50 wynikow w 0,12 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:06:34 +02:00

50 lines
2.1 KiB
Python

"""scenes.max_quality_p: zdenormalizowana najwyzsza znana jakosc zrodel sceny
Po co, skoro filtr dalo sie napisac zapytaniem: NIE dalo sie go napisac szybko.
Zmierzone na produkcji 2026-08-18 (EXPLAIN ANALYZE, prog 1080p):
EXISTS na playback_sources 32 s (3 556 612 odwiedzonych scen)
+ indeks funkcyjny per-scena 24 s
JOIN z DISTINCT po jakosci 1,1 s W IZOLACJI, ale
ten sam JOIN + reszta filtrow 52 s (planer wraca do przechodzenia scen)
Ostatni wiersz jest sednem: w polaczeniu z bramka wertykalow, heurystyka szkieletow
i progiem dlugosci planer i tak wybiera przejscie `scenes` po `created_at` i testowanie
warunku scena po scenie. Kaztaltem zapytania sie tego nie ominie, bo koszt to LICZBA
odwiedzonych scen, a nie pojedynczy test. Skalarna kolumna zdejmuje problem: staje sie
zwyklym filtrem, takim jak `duration_sec`, ktory jest szybki.
NULL = nie znamy jakosci zadnego zywego zrodla (93,5% katalogu: `quality` zapisujemy
oportunistycznie, tylko gdy listing zrodla ja podaje). Filtr celowo takie sceny ODSIEWA,
patrz komentarz przy QUALITY_OPTIONS w mobile/src/screens/ScenesFilterModal.tsx.
Revision ID: 0029_scenes_max_quality
Revises: 0028_phash_backfill_queue
Create Date: 2026-08-18
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision: str = "0029_scenes_max_quality"
down_revision: str | None = "0028_phash_backfill_queue"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("scenes", sa.Column("max_quality_p", sa.SmallInteger(), nullable=True))
# (max_quality_p, created_at DESC): filtr to zakres po pierwszej kolumnie, a sort
# feedu idzie po drugiej. Partial, bo 93,5% scen ma NULL i nie ma po co ich indeksowac.
op.create_index(
"ix_scenes_max_quality_created",
"scenes",
["max_quality_p", sa.text("created_at DESC")],
postgresql_where=sa.text("max_quality_p IS NOT NULL"),
)
def downgrade() -> None:
op.drop_index("ix_scenes_max_quality_created", table_name="scenes")
op.drop_column("scenes", "max_quality_p")