fix(api): bez planow ogolnych i szybkie listy serwisow, ktore stoja
Some checks failed
Backend tests / test (push) Has been cancelled

- db: prepare_threshold=None. psycopg przygotowywal zapytanie po 5 wykonaniach na
  polaczeniu, a Postgres przechodzil potem na plan ogolny. Lista duzego tagu na takim
  planie: "pov" 44 s zamiast 0,46 s, "masturbation" 46 s zamiast 0,18 s. Stad listy
  ladowaly sie raz szybko, raz minute, zaleznie od polaczenia z puli (tez GOON-73/75).
- Lista jednego serwisu bez frazy: przejscie od najnowszych z limitem 1 s, a po nim
  start od scen serwisu (do 150 tys.). Serwis, ktory stoi, mial sceny gleboko:
  superporn 72-92 s -> 1,7 s, yespornvip 15 s -> 1,5 s, hentaiplay 9 s -> 0,06 s.

Porownanie ze starym kodem na 88 listach serwisow: wyniki identyczne przy
zapytaniach puszczonych po sobie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-09-23 09:45:05 +02:00
parent 3349700f72
commit 2a2c0d7e9d
2 changed files with 42 additions and 8 deletions

View file

@ -251,6 +251,16 @@ _ORIGIN_DRIVEN_MAX_SCENES = 60_000
# a przy sort=studio przejscie po studiach szuka trafien w kazdym z 33 tys. studiow
# ("sunny leone" 15 min), podczas gdy trigramy + sort trafien to 0,37 s.
_RARE_PHRASE_HITS = 4
# Lista jednego serwisu bez frazy (ekran serwisu w apce). Planer idzie od najnowszych scen
# calego katalogu i sprawdza, czy scena jest z tego serwisu. Dla serwisu, ktory stoi,
# jego sceny sa gleboko: superporn (37 tys., ostatnia 20.08) 92 s na strone 1, bo
# przejscie mijalo 1,1 mln nowszych scen; yespornvip 23 s (pomiar 2026-09-23). Dlatego
# przejscie dostaje limit czasu, a po nim lista zaczyna od scen serwisu. Limit 1 s, bo
# u zywych srednich serwisow przejscie trwa ok. 1 s (hdporngg), a start od scen serwisu
# kosztuje wtedy wiecej (5 s na zimno). Powyzej 150 tys. scen (porndoe 279 tys.) start
# od serwisu jest wolniejszy niz przejscie, wiec zostaje zwykly plan.
_ORIGIN_LIST_WALK_BUDGET_MS = 1000
_ORIGIN_LIST_MAX_SCENES = 150_000
_TRGM_RUN_RE = re.compile(r"[^\W_]{3}")
_TITLE_SAMPLE_TTL = 3600.0
_TITLE_SAMPLE: dict = {"ts": 0.0, "vals": ()}
@ -334,10 +344,7 @@ def _search_walk_rows(
if ids is None:
# Bez trigramu zwykly plan czyta caly indeks (44 s), wiec lepiej pusto.
return None if budget == _SEARCH_WALK_BUDGET_MS else []
if not ids:
return []
by_id = {s.id: s for s in session.execute(select(Scene).where(Scene.id.in_(ids))).scalars()}
return [by_id[i] for i in ids if i in by_id]
return _scenes_in_order(session, ids)
def _rare_title_phrase(q: str, sample) -> bool:
@ -356,7 +363,9 @@ def _origin_driven_worth(q: str, sample) -> bool:
return not _rare_title_phrase(q, sample)
def _origin_scene_ids(session: Session, origin: str) -> list[uuid.UUID] | None:
def _origin_scene_ids(
session: Session, origin: str, max_scenes: int = _ORIGIN_DRIVEN_MAX_SCENES
) -> list[uuid.UUID] | None:
"""Id scen z zywym zrodlem serwisu albo None, gdy serwis jest za duzy na te sciezke.
Wielkosc bierzemy z source_stats (liczone co 6 h), zeby dla xvideos nie ciagnac
@ -365,17 +374,25 @@ def _origin_scene_ids(session: Session, origin: str) -> list[uuid.UUID] | None:
from app.models.source_stats import SourceStats
size = session.execute(select(SourceStats.scenes).where(SourceStats.origin == origin)).scalar()
if size is not None and size > _ORIGIN_DRIVEN_MAX_SCENES:
if size is not None and size > max_scenes:
return None
ids = list(
session.execute(
select(PlaybackSource.scene_id)
.where(PlaybackSource.origin == origin, PlaybackSource.dead_at.is_(None))
.distinct()
.limit(_ORIGIN_DRIVEN_MAX_SCENES + 1)
.limit(max_scenes + 1)
).scalars()
)
return ids if len(ids) <= _ORIGIN_DRIVEN_MAX_SCENES else None
return ids if len(ids) <= max_scenes else None
def _scenes_in_order(session: Session, ids: list[uuid.UUID]) -> list[Scene]:
"""Pelne wiersze scen dla listy id, w kolejnosci tej listy."""
if not ids:
return []
by_id = {s.id: s for s in session.execute(select(Scene).where(Scene.id.in_(ids))).scalars()}
return [by_id[i] for i in ids if i in by_id]
def _split_csv(raw: str | None) -> list[str]:
@ -1037,6 +1054,17 @@ def list_scenes(
Scene.id
== any_(bindparam("origin_scene_ids", origin_ids, type_=ARRAY(PG_UUID(as_uuid=True))))
)
if rows is None and origin and not q and sort in ("created_at", "release_date"):
walked = _search_walk_ids(session, ordered, _off, _want, _ORIGIN_LIST_WALK_BUDGET_MS)
if walked is not None:
rows = _scenes_in_order(session, walked)
else:
origin_ids = _origin_scene_ids(session, origin, _ORIGIN_LIST_MAX_SCENES)
if origin_ids is not None:
ordered = ordered.where(
Scene.id
== any_(bindparam("origin_scene_ids", origin_ids, type_=ARRAY(PG_UUID(as_uuid=True))))
)
if rows is None and origin_ids is None and q and (
sort == "created_at" or (sort == "release_date" and tag_sort_id is None)
):

View file

@ -8,10 +8,16 @@ from app.config import get_settings
_settings = get_settings()
# prepare_threshold=None: psycopg domyslnie przygotowuje zapytanie po 5 wykonaniach na
# polaczeniu, a Postgres po kolejnych 5 potrafi przejsc na plan ogolny, niezalezny od
# parametrow. Dla list tagu to katastrofa: plan dobry dla malego tagu na duzym ("pov")
# trwal 44 s zamiast 0,46 s (pomiar 2026-09-23), wiec lista ladowala sie raz szybko, raz
# minute, zaleznie od tego, ktore polaczenie z puli trafilo. Planowanie od nowa to kilka ms.
engine = create_engine(
_settings.database_url,
pool_pre_ping=True,
future=True,
connect_args={"prepare_threshold": None},
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False, future=True)