fix(dedup): samozlaczenie po phashu odsiewa zdegenerowane wartosci

`_pairs_exact_phash` laczylo scene_fingerprints po wartosci BEZ zadnego
ograniczenia. Do tej pory uchodzilo, bo zdegenerowanych wartosci bylo
malo i blacklista je kasowala. Zmienia to backfill: dokladamy ~240 tys.
odciskow z xvideos/xnxx/epornera, a te serwuja zaslepki przy usunietych
filmach.

Dwa problemy naraz:

1. Blacklista odswieza sie RAZ NA DOBE, wiec swiezo powstala zaslepka ma
   pelna dobe okna na wpuszczenie do potoku tysiecy niepowiazanych scen.
2. Samozlaczenie jest KWADRATOWE po liczbie scen dzielacych wartosc:
   zaslepka przy 5 tys. scen to 12,5 mln par i job pada na pamieci,
   zanim cokolwiek zdazy ja zablacklistowac.

Teraz zapytanie samo odsiewa wartosci dzielone przez >=10 scen (ten sam
prog co phash_blacklist) ORAZ te juz zablacklistowane, wiec dziala
natychmiast, nie po dobie.

NIE jest to atrapa: 4% w backfill jedna taka wartosc juz istnieje
(12 scen) i blacklista jej jeszcze nie widziala. Po zmianie samozlaczenie
daje 23 555 par w 8,3 s.
This commit is contained in:
goon-foss 2026-08-13 14:50:46 +02:00
parent 91c2ce6e0a
commit 276fb4c24d

View file

@ -24,6 +24,7 @@ from sqlalchemy.orm import Session
from app.config import get_settings
from app.db import session_scope
from app.scheduler.phash_blacklist import DEFAULT_MIN_SCENES
from app.models.merge_candidate import MergeCandidate, MergeKind, MergeStatus
from app.models.scene import Scene, SceneFingerprint, ScenePerformer
from app.resolve.scene_merge import merge_scenes
@ -187,13 +188,37 @@ def _pairs_exact_phash(session: Session) -> Iterable[tuple[uuid.UUID, uuid.UUID]
tylko ≥auto_t, więc przypadkowe kolizje z innym performerem/duration wpadną w review).
Skalowalne dla setek tys. phashy gdzie Hamming-fuzzy O(N²) jest nierealne."""
from sqlalchemy import text
# Wartości ZDEGENEROWANE (zaślepka, czarna klatka, intro studia) odsiewamy TUTAJ,
# nie tylko przez `phash_blacklist`. Dwa powody:
#
# 1. Blacklista jest odświeżana raz na dobę, więc świeżo wpisana zaślepka ma
# całą dobę okna. Do tej pory to nie bolało, ale backfill z sierpnia 2026
# dokłada ~240 tys. odcisków z xvideos/xnxx/epornera, a te serwują zaślepki
# przy usuniętych filmach. Wystarczy jedna taka wartość, żeby wpuścić do
# potoku tysiące scen, których nic nie łączy poza obrazkiem.
# 2. To samozłączenie jest KWADRATOWE po liczbie scen dzielących wartość:
# zaślepka przy 5 tys. scen to 12,5 mln par, czyli job pada na pamięci,
# zanim cokolwiek zdąży ją zablacklistować.
#
# Próg jest ten sam, co w `phash_blacklist` (≥10 scen = nie identyfikuje sceny),
# więc obie ścieżki mówią to samo, tylko ta działa natychmiast.
rows = session.execute(text("""
WITH degenerate AS (
SELECT value
FROM scene_fingerprints
WHERE kind = 'phash'
GROUP BY value
HAVING count(DISTINCT scene_id) >= :min_scenes
)
SELECT DISTINCT a.scene_id AS sa, b.scene_id AS sb
FROM scene_fingerprints a
JOIN scene_fingerprints b
ON a.value = b.value AND a.scene_id < b.scene_id
WHERE a.kind = 'phash' AND b.kind = 'phash'
"""))
AND NOT EXISTS (SELECT 1 FROM degenerate d WHERE d.value = a.value)
AND NOT EXISTS (SELECT 1 FROM phash_blacklist b2 WHERE b2.value = a.value)
"""), {"min_scenes": DEFAULT_MIN_SCENES})
for sa, sb in rows:
yield (sa, sb)