perf(scenes): indeks trigramowy na title + ranking kandydatow do przegladu
Kazde szukanie slowa w tytulach bylo sekwencyjnym skanem 3,46 mln wierszy. Blokowalo
to ranking over-attribution: metryka "ile scen ma te nazwe w tytule, ale NIE jest do
niej przypisanych" wymaga jednego skanu NA KANDYDATA, a kandydatow jest ~800. Proba
zostala ubita po 10 minutach.
GIN + gin_trgm_ops obsluguje LIKE, ILIKE i regexy, a pg_trgm sam normalizuje wielkosc
liter, wiec indeks na surowym title wystarcza. Efekt: 26 ms zamiast sekund na
zapytanie, ranking 800 kandydatow w 48 sekund zamiast godzin. Indeks 315 MB.
Zbudowany CONCURRENTLY w autocommit_block: zwykle CREATE INDEX trzymaloby lock
blokujacy zapisy na scenes przez cala budowe i zatrzymalo ingest.
Ranking od razu pokazuje dwie rozne rzeczy. Czyste smieci ("Pornhub" 105.9, "Monster"
92, "Pretty" 63, "Nice" 42) oraz SAME NAZWISKA (Devine, Starr, Dior, Cruz, West,
Knight, Johnson, James) - wpisy powstale z rozbicia imienia i nazwiska, ktore potem
lapia kazda scene z tym nazwiskiem, nalezaca do zupelnie innej osoby.
Usuniete przy okazji: "Pornhub" (108 scen, 0 z kanonu, wszystkie to "full video on
pornhub") i "Precious" (119 scen, 0 z kanonu, sam przymiotnik).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
470d70c11e
commit
ef8c255e1f
2 changed files with 90 additions and 4 deletions
46
alembic/versions/20260727_0027_scenes_title_trgm.py
Normal file
46
alembic/versions/20260727_0027_scenes_title_trgm.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""indeks trigramowy na scenes.title
|
||||||
|
|
||||||
|
Revision ID: 0027_scenes_title_trgm
|
||||||
|
Revises: 0026_scene_performer_source
|
||||||
|
Create Date: 2026-07-27
|
||||||
|
|
||||||
|
Bez tego każde szukanie słowa w tytułach to sekwencyjny skan 3,46 mln wierszy. Blokowało
|
||||||
|
to ranking kandydatów do przeglądu over-attribution
|
||||||
|
(`scripts/review_performer_attributions.py`): metryka „ile scen ma tę nazwę w tytule, ale
|
||||||
|
NIE jest do niej przypisanych" wymaga jednego skanu NA KANDYDATA, a kandydatów jest ~800.
|
||||||
|
Przy skanie sekwencyjnym to godziny obciążania bazy — próba została ubita po 10 minutach.
|
||||||
|
|
||||||
|
GIN + `gin_trgm_ops` obsługuje `LIKE`, `ILIKE` i regexy (`~`, `~*`), a pg_trgm normalizuje
|
||||||
|
wielkość liter sam, więc indeks na surowym `title` wystarcza.
|
||||||
|
|
||||||
|
**CONCURRENTLY i `autocommit_block`**: zwykłe `CREATE INDEX` bierze lock blokujący zapisy
|
||||||
|
na `scenes` na cały czas budowy (kilka minut przy tej wielkości), czyli zatrzymałoby
|
||||||
|
ingest. `CONCURRENTLY` nie może biec w transakcji, a Alembic domyślnie owija migrację w
|
||||||
|
transakcję — stąd `autocommit_block()`.
|
||||||
|
|
||||||
|
Uwaga operacyjna: nieudane `CREATE INDEX CONCURRENTLY` zostawia indeks w stanie INVALID.
|
||||||
|
Sprawdzenie: `SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;` — taki
|
||||||
|
trzeba dropnąć ręcznie i powtórzyć.
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0027_scenes_title_trgm"
|
||||||
|
down_revision: str | None = "0026_scene_performer_source"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
|
||||||
|
op.execute(
|
||||||
|
"CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_scenes_title_trgm "
|
||||||
|
"ON scenes USING gin (title gin_trgm_ops)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
op.execute("DROP INDEX CONCURRENTLY IF EXISTS ix_scenes_title_trgm")
|
||||||
|
|
@ -37,11 +37,13 @@ niewiarygodne i trzeba je przeczytać. Rapture przy 2,5 okazała się realna (zo
|
||||||
sceny), „Precious" przy 3,1 okazała się wyłącznie przymiotnikiem („precious
|
sceny), „Precious" przy 3,1 okazała się wyłącznie przymiotnikiem („precious
|
||||||
stepdaughter", „her ass is precious") i poszła w całości.
|
stepdaughter", „her ass is precious") i poszła w całości.
|
||||||
|
|
||||||
Świadomie NIE ma tu trybu rankingu wszystkich kandydatów: bez indeksu trigramowego na
|
`--candidates` daje ranking, kogo przejrzeć. Działa dzięki indeksowi trigramowemu
|
||||||
`scenes.title` to skan 3,4 mln tytułów RAZY ~800 kandydatów, czyli godziny obciążania
|
(migracja 0027) — przedtem pojedyncze szukanie słowa w tytułach było skanem 3,46 mln
|
||||||
bazy. Dla jednej osoby ten sam skan trwa sekundy.
|
wierszy, więc ranking ~800 kandydatów zajmował godziny i został ubity. Teraz jedno
|
||||||
|
zapytanie to ~26 ms.
|
||||||
|
|
||||||
Użycie (kontener worker):
|
Użycie (kontener worker):
|
||||||
|
python -m scripts.review_performer_attributions --candidates
|
||||||
python -m scripts.review_performer_attributions "Rapture"
|
python -m scripts.review_performer_attributions "Rapture"
|
||||||
python -m scripts.review_performer_attributions "Rapture" --detach-file /tmp/zle.txt
|
python -m scripts.review_performer_attributions "Rapture" --detach-file /tmp/zle.txt
|
||||||
python -m scripts.review_performer_attributions "Rapture" --detach-file /tmp/zle.txt --yes
|
python -m scripts.review_performer_attributions "Rapture" --detach-file /tmp/zle.txt --yes
|
||||||
|
|
@ -79,6 +81,28 @@ WHERE s.title ~* ('\\m' || :name || '\\M')
|
||||||
WHERE sp.scene_id = s.id AND sp.performer_id = :pid)
|
WHERE sp.scene_id = s.id AND sp.performer_id = :pid)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# UWAGA: `ORDER BY 3::numeric / scen` NIE działa — po rzutowaniu Postgres traktuje
|
||||||
|
# trójkę jako literał, nie numer kolumny, i sortuje po 3/scen (czyli po liczbie scen
|
||||||
|
# rosnąco). Wyrażenie trzeba powtórzyć jawnie, stąd osobne CTE.
|
||||||
|
_CANDIDATES_SQL = """
|
||||||
|
WITH kand AS (
|
||||||
|
SELECT pf.id, pf.canonical_name,
|
||||||
|
(SELECT count(*) FROM scene_performers sp WHERE sp.performer_id = pf.id) AS scen
|
||||||
|
FROM performers pf
|
||||||
|
WHERE pf.canonical_name !~ ' ' AND length(pf.canonical_name) >= 4
|
||||||
|
), z AS (
|
||||||
|
SELECT k.canonical_name, k.scen,
|
||||||
|
(SELECT count(*) FROM scenes s
|
||||||
|
WHERE s.title ~* ('\\m' || k.canonical_name || '\\M')
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM scene_performers sp
|
||||||
|
WHERE sp.scene_id = s.id AND sp.performer_id = k.id)) AS obce
|
||||||
|
FROM kand k WHERE k.scen >= :min_scenes
|
||||||
|
)
|
||||||
|
SELECT canonical_name, scen, obce FROM z
|
||||||
|
ORDER BY obce::numeric / scen DESC
|
||||||
|
LIMIT :limit
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _find_performer(session, name: str):
|
def _find_performer(session, name: str):
|
||||||
row = session.execute(
|
row = session.execute(
|
||||||
|
|
@ -94,6 +118,9 @@ def _find_performer(session, name: str):
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
ap.add_argument("performer", nargs="?", help="canonical_name performera")
|
ap.add_argument("performer", nargs="?", help="canonical_name performera")
|
||||||
|
ap.add_argument("--candidates", action="store_true", help="ranking: kogo przejrzeć")
|
||||||
|
ap.add_argument("--min-scenes", type=int, default=60)
|
||||||
|
ap.add_argument("--limit", type=int, default=25)
|
||||||
ap.add_argument("--detach-file", help="plik z prefiksami tytułów do odpięcia")
|
ap.add_argument("--detach-file", help="plik z prefiksami tytułów do odpięcia")
|
||||||
ap.add_argument("--yes", action="store_true", help="wykonaj odpięcie (bez tego dry-run)")
|
ap.add_argument("--yes", action="store_true", help="wykonaj odpięcie (bez tego dry-run)")
|
||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
|
|
@ -103,8 +130,21 @@ def main() -> None:
|
||||||
)
|
)
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.candidates:
|
||||||
|
with session_scope() as s:
|
||||||
|
rows = s.execute(
|
||||||
|
text(_CANDIDATES_SQL),
|
||||||
|
{"min_scenes": args.min_scenes, "limit": args.limit},
|
||||||
|
).fetchall()
|
||||||
|
print("nazwa sceny obce wskaźnik")
|
||||||
|
for nazwa, scen, obce in rows:
|
||||||
|
print(f" {nazwa[:20]:22} {scen:5} {obce:7} {obce/scen:7.1f}")
|
||||||
|
print("\nWskaźnik = obce sceny ze słowem w tytule / przypisania tej osoby.")
|
||||||
|
print("Wysoki = nazwa jest zwykłym słowem; NIE znaczy, że osoba jest fikcyjna.")
|
||||||
|
return
|
||||||
|
|
||||||
if not args.performer:
|
if not args.performer:
|
||||||
print("podaj canonical_name performera")
|
print("podaj canonical_name performera albo --candidates")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
prefixes: list[str] = []
|
prefixes: list[str] = []
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue