perf(clips): explore po podzapytaniu okna, kandydaci bez ORM i bez tagow, max(uuid) przez tekst
Strona feedu spadla z 310-470 ms do 50-130 ms (srednio 85 ms na 30 stronach, 0 powtorek). Postgres nie ma max(uuid): koniec okna liczony po rzutowaniu na tekst (ten sam porzadek dla malych hex w stalym formacie).
This commit is contained in:
parent
0df2236b37
commit
538a72107e
1 changed files with 45 additions and 37 deletions
|
|
@ -23,7 +23,7 @@ from datetime import UTC, datetime
|
|||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select, text
|
||||
from sqlalchemy import String, cast, func, select, text
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
|
@ -247,39 +247,47 @@ def _visible_clauses(device_id: str, bl: dict[str, set[str]]) -> list:
|
|||
return clauses
|
||||
|
||||
|
||||
def _candidates(session: Session, ids: list[uuid.UUID], clauses: list) -> list[cf.Candidate]:
|
||||
if not ids:
|
||||
return []
|
||||
rows = session.execute(select(Clip).where(Clip.id.in_(ids), *clauses)).scalars().all()
|
||||
tags = _tags_for(session, [r.id for r in rows])
|
||||
_CAND_COLS = (Clip.id, Clip.creator, Clip.likes, Clip.published_at, Clip.niches)
|
||||
|
||||
|
||||
def _rows_to_candidates(session: Session, rows, *, with_tags: bool) -> list[cf.Candidate]:
|
||||
"""Wiersze `_CAND_COLS` → Candidate. Tagi tylko na życzenie: pula explore punktuje
|
||||
wyłącznie lajkami, więc ciągnięcie tagów dla 300 kandydatów było czystym kosztem."""
|
||||
tags = _tags_for(session, [r[0] for r in rows]) if with_tags else {}
|
||||
return [
|
||||
cf.Candidate(
|
||||
id=r.id, creator=r.creator, likes=r.likes, published_at=r.published_at,
|
||||
tags=tags.get(r.id, []), niches=list(r.niches or []),
|
||||
)
|
||||
cf.Candidate(id=r[0], creator=r[1], likes=r[2], published_at=r[3], tags=tags.get(r[0], []), niches=list(r[4] or []))
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def _explore_window(session: Session, pk: uuid.UUID) -> tuple[list[uuid.UUID], bool]:
|
||||
"""Kolejne `_EXPLORE_WINDOW` id po `pk`; gdy okno się kończy, zawija do zera."""
|
||||
ids = [
|
||||
r[0]
|
||||
for r in session.execute(
|
||||
def _candidates(session: Session, ids: list[uuid.UUID], clauses: list) -> list[cf.Candidate]:
|
||||
if not ids:
|
||||
return []
|
||||
rows = session.execute(select(*_CAND_COLS).where(Clip.id.in_(ids), *clauses)).all()
|
||||
return _rows_to_candidates(session, rows, with_tags=True)
|
||||
|
||||
|
||||
def _explore_candidates(
|
||||
session: Session, pk: uuid.UUID, clauses: list, limit: int
|
||||
) -> tuple[list[cf.Candidate], uuid.UUID, bool]:
|
||||
"""Widoczni kandydaci z okna `_EXPLORE_WINDOW` id po `pk` (podzapytanie, bez
|
||||
przenoszenia 2000 uuid przez SQL). Zwraca (kandydaci, koniec okna, czy okno było
|
||||
ostatnie). Ostatnie okno = katalog wyczerpany od `pk`, następna strona zawija do zera."""
|
||||
window = (
|
||||
select(Clip.id).where(Clip.id > pk, Clip.dead_at.is_(None)).order_by(Clip.id).limit(_EXPLORE_WINDOW)
|
||||
)
|
||||
]
|
||||
wrapped = False
|
||||
if len(ids) < _EXPLORE_WINDOW and pk != _UUID_ZERO:
|
||||
wrapped = True
|
||||
more = [
|
||||
r[0]
|
||||
for r in session.execute(
|
||||
select(Clip.id).where(Clip.dead_at.is_(None)).order_by(Clip.id).limit(_EXPLORE_WINDOW - len(ids))
|
||||
)
|
||||
]
|
||||
ids.extend(more)
|
||||
return ids, wrapped
|
||||
).subquery()
|
||||
# Postgres nie ma max(uuid); tekstowo porządek jest ten sam (małe hex, stały format).
|
||||
end_txt, n = session.execute(
|
||||
select(func.max(cast(window.c.id, String)), func.count()).select_from(window)
|
||||
).one()
|
||||
if not n:
|
||||
return [], _UUID_ZERO, True
|
||||
end_id = uuid.UUID(end_txt)
|
||||
rows = session.execute(
|
||||
select(*_CAND_COLS).where(Clip.id.in_(select(window.c.id)), *clauses).order_by(Clip.id).limit(limit)
|
||||
).all()
|
||||
last = n < _EXPLORE_WINDOW
|
||||
return _rows_to_candidates(session, rows, with_tags=False), (end_id if not last else _UUID_ZERO), last
|
||||
|
||||
|
||||
def _affinity_candidate_ids(session: Session, aff: cf.Affinity, rng) -> set[uuid.UUID]:
|
||||
|
|
@ -344,19 +352,19 @@ def clip_feed(
|
|||
taken = {c.id for c in picked_aff}
|
||||
|
||||
# Explore: okno po PK; gdy w oknie brak widocznych, przesuwamy sie dalej (max 3 okna).
|
||||
# Braki z afinicznosci (za malo kandydatow po filtrach) dopelnia explore.
|
||||
picked_explore: list[cf.Candidate] = []
|
||||
wrapped = False
|
||||
next_pk = pk
|
||||
want = n_explore + max(0, n_aff - len(picked_aff)) # braki z afinicznosci dopelnia explore
|
||||
want = n_explore + max(0, n_aff - len(picked_aff))
|
||||
for _ in range(3):
|
||||
ids, w = _explore_window(session, next_pk)
|
||||
wrapped = wrapped or w
|
||||
if not ids:
|
||||
break
|
||||
next_pk = ids[-1] if not w else _UUID_ZERO
|
||||
pool = [c for c in _candidates(session, ids, clauses) if c.id not in taken]
|
||||
pool, next_pk, last = _explore_candidates(session, next_pk, clauses, limit=max(60, want * 15))
|
||||
pool = [c for c in pool if c.id not in taken]
|
||||
picked_explore += cf.pick_explore(pool, want - len(picked_explore), rng)
|
||||
if len(picked_explore) >= want or len(ids) < _EXPLORE_WINDOW:
|
||||
taken.update(c.id for c in picked_explore)
|
||||
if last:
|
||||
wrapped = True
|
||||
if len(picked_explore) >= want or last:
|
||||
break
|
||||
|
||||
page_items = cf.compose_page(picked_aff, picked_explore, rng)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue