goon/app/resolve/movie_match.py
goon-foss e0f70f2574
Some checks are pending
Backend tests / test (push) Waiting to run
fix(movies): kandydaci do dopasowania po podobienstwie tytulu
find_movie_candidates bral dowolne 50 wierszy z sumy trigramu i bloku
studio+rok. Duze studio ma kilkaset filmow w trzech latach, wiec wierny
duplikat wypadal z puli i powstawal nowy film. Teraz kolejnosc po
similarity(). Skrypt remerge dostal tryb --same-title-studio (scalono
930 par o tym samym tytule i studiu).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:04:14 +02:00

66 lines
2.5 KiB
Python

"""Wyszukiwanie kandydatów do dedup movies, blocking + fuzzy title prefilter.
Strategia: nie chcemy O(N) score'ować wszystkich filmów dla każdego nowego.
Blocking: kandydat musi mieć title trigram similarity ≥0.4 (pg_trgm) z incoming title,
ALBO (same studio AND year w oknie ±1). Plus pierwsze X (np. 50) filmów z każdego
zbioru, wystarczająco szeroko żeby nie zgubić matchów, dostatecznie wąsko żeby
score'ować szybko.
"""
from __future__ import annotations
import uuid
from datetime import date
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from app.models.movie import Movie
def find_movie_candidates(
session: Session,
*,
title_normalized: str,
studio_id: uuid.UUID | None,
release_year: int | None,
limit: int = 50,
) -> list[Movie]:
"""Zwraca kandydatów do score'owania.
Trigram threshold: pg_trgm `%` operator domyślnie ma similarity ≥0.3, zniżamy
do 0.4 dla jakości. Plus secondary blocking po (studio_id, year ±1) jako safety
net dla SEO tytułów (np. mangoporn często ma "Watch Cece adult..." vs paradisehill
"Cece", trigram złapie, ale gdyby nie, studio+year wystarczają).
"""
from sqlalchemy import func, literal
# Kolejnosc po podobienstwie tytulu: bez niej LIMIT bral dowolne 50 z sumy trigramu i
# bloku studio+rok, a duze studio ma kilkaset filmow w trzech latach. Wtedy wierny
# duplikat (ten sam tytul, rok, studio i obsada) wypadal z puli i powstawal nowy film
# (2026-10-16: 13 z 228 nowych filmow z pandamovies, po scaleniu blizniaczych studiow
# bloki urosly).
q = session.execute(
select(Movie)
.where(
or_(
# pg_trgm `%`, wymagamy minimum bound 0.4 (set per-session pewnie low default).
# Bezpieczniej użyć similarity() funkcji bezpośrednio z literalnym threshold.
Movie.title_normalized.op("%")(literal(title_normalized)),
# Studio + year ±1 jako fallback blocking
_studio_year_block(studio_id, release_year),
)
)
.order_by(func.similarity(Movie.title_normalized, literal(title_normalized)).desc())
.limit(limit)
).scalars()
return list(q)
def _studio_year_block(studio_id: uuid.UUID | None, release_year: int | None):
from sqlalchemy import and_, false
if studio_id is None or release_year is None:
return false()
return and_(
Movie.studio_id == studio_id,
Movie.release_year.between(release_year - 1, release_year + 1),
)