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 fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||||
from pydantic import BaseModel, Field
|
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.dialects.postgresql import insert as pg_insert
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
@ -247,39 +247,47 @@ def _visible_clauses(device_id: str, bl: dict[str, set[str]]) -> list:
|
||||||
return clauses
|
return clauses
|
||||||
|
|
||||||
|
|
||||||
def _candidates(session: Session, ids: list[uuid.UUID], clauses: list) -> list[cf.Candidate]:
|
_CAND_COLS = (Clip.id, Clip.creator, Clip.likes, Clip.published_at, Clip.niches)
|
||||||
if not ids:
|
|
||||||
return []
|
|
||||||
rows = session.execute(select(Clip).where(Clip.id.in_(ids), *clauses)).scalars().all()
|
def _rows_to_candidates(session: Session, rows, *, with_tags: bool) -> list[cf.Candidate]:
|
||||||
tags = _tags_for(session, [r.id for r in rows])
|
"""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 [
|
return [
|
||||||
cf.Candidate(
|
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 []))
|
||||||
id=r.id, creator=r.creator, likes=r.likes, published_at=r.published_at,
|
|
||||||
tags=tags.get(r.id, []), niches=list(r.niches or []),
|
|
||||||
)
|
|
||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _explore_window(session: Session, pk: uuid.UUID) -> tuple[list[uuid.UUID], bool]:
|
def _candidates(session: Session, ids: list[uuid.UUID], clauses: list) -> list[cf.Candidate]:
|
||||||
"""Kolejne `_EXPLORE_WINDOW` id po `pk`; gdy okno się kończy, zawija do zera."""
|
if not ids:
|
||||||
ids = [
|
return []
|
||||||
r[0]
|
rows = session.execute(select(*_CAND_COLS).where(Clip.id.in_(ids), *clauses)).all()
|
||||||
for r in session.execute(
|
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)
|
select(Clip.id).where(Clip.id > pk, Clip.dead_at.is_(None)).order_by(Clip.id).limit(_EXPLORE_WINDOW)
|
||||||
)
|
).subquery()
|
||||||
]
|
# Postgres nie ma max(uuid); tekstowo porządek jest ten sam (małe hex, stały format).
|
||||||
wrapped = False
|
end_txt, n = session.execute(
|
||||||
if len(ids) < _EXPLORE_WINDOW and pk != _UUID_ZERO:
|
select(func.max(cast(window.c.id, String)), func.count()).select_from(window)
|
||||||
wrapped = True
|
).one()
|
||||||
more = [
|
if not n:
|
||||||
r[0]
|
return [], _UUID_ZERO, True
|
||||||
for r in session.execute(
|
end_id = uuid.UUID(end_txt)
|
||||||
select(Clip.id).where(Clip.dead_at.is_(None)).order_by(Clip.id).limit(_EXPLORE_WINDOW - len(ids))
|
rows = session.execute(
|
||||||
)
|
select(*_CAND_COLS).where(Clip.id.in_(select(window.c.id)), *clauses).order_by(Clip.id).limit(limit)
|
||||||
]
|
).all()
|
||||||
ids.extend(more)
|
last = n < _EXPLORE_WINDOW
|
||||||
return ids, wrapped
|
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]:
|
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}
|
taken = {c.id for c in picked_aff}
|
||||||
|
|
||||||
# Explore: okno po PK; gdy w oknie brak widocznych, przesuwamy sie dalej (max 3 okna).
|
# 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] = []
|
picked_explore: list[cf.Candidate] = []
|
||||||
wrapped = False
|
wrapped = False
|
||||||
next_pk = pk
|
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):
|
for _ in range(3):
|
||||||
ids, w = _explore_window(session, next_pk)
|
pool, next_pk, last = _explore_candidates(session, next_pk, clauses, limit=max(60, want * 15))
|
||||||
wrapped = wrapped or w
|
pool = [c for c in pool if c.id not in taken]
|
||||||
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]
|
|
||||||
picked_explore += cf.pick_explore(pool, want - len(picked_explore), rng)
|
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
|
break
|
||||||
|
|
||||||
page_items = cf.compose_page(picked_aff, picked_explore, rng)
|
page_items = cf.compose_page(picked_aff, picked_explore, rng)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue